TinyRenderer笔记2:透视投影

前面渲染模型时候,没有考虑每个点的z坐标,这种方式叫做正交投影Orthographic projection,模型看起来偏胖,因为我们平时在3d世界看到的物体都是近大远小的。透视投影Perspective projection就是用近大远小的方式投影。

两种投影对比:

正交 透视

线性变换从几何直观有三个要点:

  • 变换前是直线的,变换后依然是直线
  • 直线比例保持不变
  • 变换前是原点的,变换后依然是原点

说白了就是缩放、裁切和旋转, 不包括平移:可以看这个文章

平面上的线性变换都可以用一个二维矩阵计算:

$$ \begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} ax + by \\ cx + dy \end{bmatrix} $$

说简单点就是线性变换加上平移,用矩阵计算:

$$ \begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} + \begin{bmatrix} e \\ f \end{bmatrix} = \begin{bmatrix} ax + by + e\\ cx + dy + f \end{bmatrix} $$

把2x2的变换矩阵加上一行一列,变成3x3,并且把等待变换的向量加上一个总是1的坐标:

$$ \begin{bmatrix} a & b & e \\ c & d & f \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} ax + by + e\\ cx + dy + f \\ 1 \end{bmatrix} $$

这样就实现了和仿射变换一样的效果!这个想法非常简单。平移在二维空间中不是线性的。所以我们将2D嵌入到3D空间中(通过简单地为第三个分量加1)。这意味着二维空间是三维空间中z=1的平面。然后我们执行一个3D线性变换,并将结果投影到我们的2D物理平面上。

将3d投射到2d只需要除以3d分量:

$$ \begin{bmatrix} x \\ y \\ z \end{bmatrix} \rightarrow \begin{bmatrix} x/z \\ y/z \end{bmatrix} $$

如果z无限逼近0代表被投影后的点在无穷远处:

  • 被投影的点 -> 投影到平面z=?的2d坐标
  • (x,y,1) -> (x,y)
  • (x,y,1/2) -> (2x,2y)
  • (x,y,1/4) -> (4x,4y)

https://blog-1256556944.file.myqcloud.com/public/tinyrenderer/2-0.png

可以看到,随着平面的下降,投影后的点越来越远,所以当z=0时,表示的是一个向量而不是3d空间中的一个点。

2d的仿射变换可以通过吧2d嵌入3d,转换成3d中的线性变换,再投影回2d。同样的道理:3d的仿射变换,可以通过吧3d嵌入4d,转换成4d中的线性变换,在投影回3d!

使用齐次坐标: 点(x,y,z) -> (x,y,z,1),用下面的矩阵试着把它在4d空间中进行变换:

$$ \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & r & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} x \\ y \\ z \\ rz+1 \end{bmatrix} $$

再投影回3d:

$$ \begin{bmatrix} x \\ y \\ z \\ rz+1 \end{bmatrix} \rightarrow \begin{bmatrix} \frac{x}{rz+1} \\ \frac{y}{rz+1} \\ \frac{z}{rz+1} \end{bmatrix} $$

先把这个结果放一边。来看一个模拟现实中人眼将3d中一个点投影到平面上的例子:

有一个点P=(x,y,z),我们要把它投影到z=0的平面上,摄像机(也就是人的眼睛)在z轴上(0,0,c)的位置

https://blog-1256556944.file.myqcloud.com/public/tinyrenderer/2-1.png

根据初中还是高中的知识,三角形ABC和ODC是相似三角形,所以$\frac{AB}{AC}=\frac{OD}{OC}$,进而得出$\frac{x}{c-z}=\frac{x^{'}}{c}$

所以:

$$ x^{'}= \frac{x}{1-z/c} $$
同理:
$$ y^{'}= \frac{y}{1-z/c} $$

回到矩阵,让r=-1/c:

$$ \begin{bmatrix} x \\ y \\ z \\ rz+1 \end{bmatrix} \rightarrow \begin{bmatrix} \frac{x}{rz+1} \\ \frac{y}{rz+1} \\ \frac{z}{rz+1} \end{bmatrix} \rightarrow \begin{bmatrix} \frac{x}{1-z/c} \\ \frac{y}{1-z/c} \\ \frac{z}{1-z/c} \end{bmatrix} $$

如果我们想用位于z轴上距离原点为c的摄像机计算一个中心投影,分三步:

  1. 将3d嵌入到4d中
  2. 在4d中进行线性变换
  3. 投影回3d
$$ \begin{aligned} \begin{bmatrix} x \\ y \\ z \end{bmatrix} \rightarrow \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} &\qquad(1)\\ \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/c & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} x \\ y \\ z \\ 1-z/c \end{bmatrix} &\qquad(2)\\ \begin{bmatrix} x \\ y \\ z \\ 1-z/c \end{bmatrix} \rightarrow \begin{bmatrix} \frac{x}{1-z/c} \\ \frac{y}{1-z/c} \\ \frac{z}{1-z/c} \end{bmatrix} &\qquad(3) \end{aligned} $$
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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