Three.js是否应将顶点w值设为深度?透视投影畸变疑问
w值设置为其深度吗? 将物体投影到屏幕时,远离视点的物体会向屏幕中心偏移。在齐次坐标中,这种透视畸变效果是通过将点的(x, y, z)坐标除以其到视点的距离w来实现的。但我研究Three.js的投影矩阵后发现,它似乎并没有这么做。
请看以下场景:
// src/main.ts import { AmbientLight, BoxGeometry, DirectionalLight, Mesh, MeshPhongMaterial, PerspectiveCamera, Scene, WebGLRenderer } from "three"; const canvas = document.getElementById('canvas') as HTMLCanvasElement; canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; const renderer = new WebGLRenderer({ alpha: false, antialias: false, canvas: canvas, depth: true }); const scene = new Scene(); const camera = new PerspectiveCamera(45, canvas.width / canvas.height, 0.01, 100); camera.position.set(0, 0, 10); const light = new DirectionalLight(); light.position.set(-1, 0, 3); scene.add(light); const light2 = new AmbientLight(); scene.add(light2); const cube = new Mesh(new BoxGeometry(1, 1, 1, 1), new MeshPhongMaterial({ color: `rgb(0, 125, 125)` })); scene.add(cube); cube.position.set(3.42, 3.42, 0); renderer.render(scene, camera);
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> <div id="app" style="width: 600px; height: 600px;"> <canvas id="canvas" style="width: 100%; height: 100%;"></canvas> </div> <script type="module" src="/src/main.ts"></script> </body> </html>
这段代码生成如下图像:
注意青绿色立方体的边缘与画布边缘完全平行,但它的前右上顶点比前左下顶点离眼睛更远,难道这个右上顶点不该向中心轻微畸变吗?
我了解WebGL会在顶点后处理阶段自动将顶点除以w坐标。Three.js难道不该用深度来设置w以实现这种畸变效果吗?
我预想的效果如下例所示:
import { AmbientLight, BoxGeometry, Mesh, MeshBasicMaterial, PerspectiveCamera, Scene, ShaderMaterial, SpotLight, TextureLoader, Vector3, WebGLRenderer } from "three"; const canvas = document.getElementById('canvas') as HTMLCanvasElement; canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; const loader = new TextureLoader(); const texture = await loader.loadAsync('bricks.jpg'); const renderer = new WebGLRenderer({ alpha: false, antialias: true, canvas: canvas, depth: true }); const scene = new Scene(); const camera = new PerspectiveCamera(45, canvas.width / canvas.height, 0.01, 100); camera.position.set(0, 0, -1); camera.lookAt(new Vector3(0, 0, 0)); const light = new SpotLight(); light.position.set(-1, 0, -1); scene.add(light); const light2 = new AmbientLight(); scene.add(light2); const box = new Mesh(new BoxGeometry(1, 1, 1, 50, 50, 50), new MeshBasicMaterial({ map: texture })); scene.add(box); box.position.set(-0.6, 0, 1); const box2 = new Mesh(new BoxGeometry(1, 1, 1, 50, 50, 50), new ShaderMaterial({ uniforms: { tTexture: {value: texture } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; vec4 clipSpacePos = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); clipSpacePos.w = length(clipSpacePos.xyz); gl_Position = clipSpacePos; } `, fragmentShader: ` varying vec2 vUv; uniform sampler2D tTexture; void main() { gl_FragColor = texture2D(tTexture, vUv); } `, })); scene.add(box2); box2.position.set(0.6, 0, 1); renderer.render(scene, camera);
该代码生成如下输出:
注意左侧立方体的边缘有明显的向中心畸变效果。
首先得明确:Three.js的透视投影矩阵已经正确设置了w值,你看到的“无畸变”是场景设置导致透视效果太弱,而非Three.js没做透视处理。
核心逻辑
标准OpenGL/Three.js的透视投影中,透视矩阵会把顶点转换到裁剪空间,此时w值被设置为视点空间中的-z值(Three.js用右手坐标系,相机看向-z方向)。WebGL后续会自动执行透视除法(除以w),完成透视畸变——让远处物体更小、向中心收缩。
第一个场景看不到畸变的原因
- 比例问题:相机在z=10位置,立方体在z=0,距离10个单位,但x/y仅偏移3.42,这个偏移量相对于距离来说极小,透视畸变的程度本来就非常弱,肉眼几乎察觉不到。
- 立方体尺寸太小:1x1的立方体在600px画布上渲染后,边缘的偏移量可能只有几个像素,远低于人眼分辨阈值。
自定义Shader的效果为什么不一样
你手动把w设为length(clipSpacePos.xyz),这不是标准透视投影,而是额外叠加了一次非线性径向畸变。标准透视的w是基于视点空间z值的线性值,而你用的是距离模长,会让畸变呈放射状夸张放大,所以效果和标准透视完全不同。
验证Three.js透视效果的方法
把立方体的x/y位置拉远,比如设为cube.position.set(8, 8, 0),或者把相机靠近到z=2的位置,你就能明显看到立方体边缘不再平行于画布,会向中心收缩——这就是标准的透视畸变。
内容的提问来源于stack exchange,提问作者Michael Langbein

