前面渲染模型时候,没有考虑每个点的z坐标,这种方式叫做正交投影,模型看起来偏胖,因为我们平时在3d世界看到的物体都是近大远小的。透视投影就是用近大远小的方式投影。
两种投影对比:
正交 |
透视 |
 |
 |
线性变换从几何直观有三个要点:
- 变换前是直线的,变换后依然是直线
- 直线比例保持不变
- 变换前是原点的,变换后依然是原点
说白了就是缩放、裁切和旋转, 不包括平移:可以看这个文章
平面上的线性变换都可以用一个二维矩阵计算:
[acbd][xy]=[ax+bycx+dy]
说简单点就是线性变换加上平移,用矩阵计算:
[acbd][xy]+[ef]=[ax+by+ecx+dy+f]
把2x2的变换矩阵加上一行一列,变成3x3,并且把等待变换的向量加上一个总是1的坐标:
ac0bd0ef1xy1=ax+by+ecx+dy+f1
这样就实现了和仿射变换一样的效果!这个想法非常简单。平移在二维空间中不是线性的。所以我们将2D嵌入到3D空间中(通过简单地为第三个分量加1)。这意味着二维空间是三维空间中z=1的平面。然后我们执行一个3D线性变换,并将结果投影到我们的2D物理平面上。
将3d投射到2d只需要除以3d分量:
xyz→[x/zy/z]
如果z无限逼近0代表被投影后的点在无穷远处:
- 被投影的点 -> 投影到平面z=?的2d坐标
- (x,y,1) -> (x,y)
- (x,y,1/2) -> (2x,2y)
- (x,y,1/4) -> (4x,4y)

可以看到,随着平面的下降,投影后的点越来越远,所以当z=0时,表示的是一个向量而不是3d空间中的一个点。
2d的仿射变换可以通过吧2d嵌入3d,转换成3d中的线性变换,再投影回2d。同样的道理:3d的仿射变换,可以通过吧3d嵌入4d,转换成4d中的线性变换,在投影回3d!
使用齐次坐标: 点(x,y,z) -> (x,y,z,1),用下面的矩阵试着把它在4d空间中进行变换:
10000100001r0001xyz1=xyzrz+1
再投影回3d:
xyzrz+1→rz+1xrz+1yrz+1z
先把这个结果放一边。来看一个模拟现实中人眼将3d中一个点投影到平面上的例子:
有一个点P=(x,y,z),我们要把它投影到z=0的平面上,摄像机(也就是人的眼睛)在z轴上(0,0,c)的位置

根据初中还是高中的知识,三角形ABC和ODC是相似三角形,所以ACAB=OCOD,进而得出c−zx=cx’
所以:
x′=1−z/cx
同理:
y′=1−z/cy
回到矩阵,让r=-1/c:
xyzrz+1→rz+1xrz+1yrz+1z→1−z/cx1−z/cy1−z/cz
如果我们想用位于z轴上距离原点为c的摄像机计算一个中心投影,分三步:
- 将3d嵌入到4d中
- 在4d中进行线性变换
- 投影回3d
xyz→xyz110000100001−1/c0001xyz1=xyz1−z/cxyz1−z/c→1−z/cx1−z/cy1−z/cz(1)(2)(3)
// 4d投影到3d
fn v4p2v3(v: glm::Vec4) -> glm::Vec3 {
glm::vec3(v.x / v.w, v.y / v.w, v.z / v.w)
}
// ...
let camera: glm::Vec3 = glm::vec3(0., 0., 3.);
// 投影变换矩阵,注意gml初始化一行是矩阵中的一列
let projection = glm::mat4(
1., 0., 0., 0.,
0., 1., 0., 0.,
0., 0., 1., -1./camera.z,
0., 0., 0., 1.);
// ...
// 透视投影
let a = v4p2v3(projection * a.extend(1.));
let b = v4p2v3(projection * b.extend(1.));
let c = v4p2v3(projection * c.extend(1.));
详细代码见这里076b31fc4ea69f00e2cee530e5e3e25445189b67