一、Lecture 01 计算机图形学概述
(一)什么是图形学
Computer Graphics,使用计算机来合成和处理视觉信息的技术 / 领域。
(二)为什么学图形学
1、图形学主要应用
- 电子游戏(游戏画面、渲染等)
- 电影(特效、面部捕捉、渲染等)
- 设计(CG/概念图)
- 可视化(操纵视觉信息的方法)
- 虚拟现实VR 与 增强现实 AR
- 数字插画
- 模拟仿真
- 图形用户接口 GUI
- 字体设计 Typography
2、图形学的难点
- 实现虚拟世界逼真的交互
- 对真实世界物理规律的深刻理解
- 新的计算方法、显示方法、新技术
3、技术挑战
- 透视、投影、曲线、曲面相关数学原理
- 光照与着色的物理机制
- 三维形状的表示与运算
- 动画与仿真
- 三维图形软件编程与硬件
4、为什么要学
Computer Graphics is AWESOME!
(三)主要课程
- 光栅化
- 曲线和曲面
- 光线追踪
- 动画 / 模拟 / 仿真
(四)这门课不说什么
- OpenGL / DirectX / Vulcan(图形学 API)
- Shader(着色器)
- Maya / 3DS / Blender(3D建模)
- Unity / Unreal(游戏开发引擎)
- Computer Vision / Deep Learning(计算机视觉/深度学习)
- 两者都不属于计算机图形学范畴
- 计算机视觉:一切需要猜测的内容,比如识别,分析理解东西
- 深度学习:比如人脸生成,是在图像层面上的操作
计算机图形学与计算机视觉
- 图形学不是计算机视觉。
- 计算机视觉:猜测、预测、分析处理
- 计算机视觉是理解这个世界,计算机图形学是创造这个世界

当今各个学科结合越来越紧密,边界越来越模糊。
二、Lecture 02 向量与线性代数
(一)图形学依赖的知识
- 基础数学 —— 线性代数、微积分、统计学
- 基础物理 —— 光学、力学
- 其他 —— 信号处理、数值分析
- 以及一些美感
(二)向量
1、向量
具有大小和方向的量,没有绝对的起点。记作 \vec{a} 或 粗体 \boldsymbol{a}。用矩阵表示为 \mathbf{A} = \begin{pmatrix} x \\ y \end{pmatrix}、\mathbf{A}^T = (x,\ y)。
2、向量的模长
向量的模长(长度),记作 ∥a∥,计算公式为 \|\mathbf{A}\| = \sqrt{x^2 + y^2}。
3、向量归一化
长度为1的向量,计算方式为 \hat{\boldsymbol{a}} = \dfrac{\boldsymbol{a}}{|\boldsymbol{a}|}。
4、向量加法
使用三角形法则或者平行四边形法则。
5、笛卡尔坐标系
让向量起点为坐标原点,这样可以使用基向量 X、Y 线性组合表示一个向量。X 和 Y 通常为正交单位向量(互相垂直的单位向量)。
6、向量点乘(Dot)
- 说明: \vec{a} \cdot \vec{b} 可以看作是 \vec{a} 与 \vec{b} 在 \vec{a} 上的投影 的长度的乘积。
- 几何定义:\vec{a} \cdot \vec{b} = \|\vec{a}\| \|\vec{b}\| \cos\theta。
- 点积求夹角余弦:\cos\theta = \frac{\vec{a} \cdot \vec{b}}{\|\vec{a}\| \|\vec{b}\|},单位向量则为 \cos\theta = \hat{a} \cdot \hat{b}。
- 笛卡尔坐标系下的点乘:\vec{a} \cdot \vec{b} = \begin{pmatrix} x_a \\ y_a \end{pmatrix} \cdot \begin{pmatrix} x_b \\ y_b \end{pmatrix} = x_a x_b + y_a y_b
- 性质:
- 交换律: \vec{a} \cdot \vec{b} = \vec{b} \cdot \vec{a}
- 分配律: \vec{a} \cdot (\vec{b} + \vec{c}) = \vec{a} \cdot \vec{b} + \vec{a} \cdot \vec{c}
- 数乘结合律: (k\vec{a}) \cdot \vec{b} = \vec{a} \cdot (k\vec{b}) = k(\vec{a} \cdot \vec{b})
- 应用:
- 计算两个向量之间的夹角
- 计算一个向量在另一个向量上的投影
- 衡量两个方向的接近程度(越接近\|\vec{a}\| \|\vec{b}\|,两向量越同向)
- 判断方向是向前还是向后
7、向量叉乘(Cross)
- 说明:叉乘结果与两个原向量正交(垂直),右手坐标系下,方向由右手螺旋定则确定:以 \vec{a} \times \vec{b} 为例,右手四指从a指向b并握紧,最终拇指朝向就是叉乘结果向量的方向。
- 模长公式:\|\vec{a} \times \vec{b}\| = \|\vec{a}\| \|\vec{b}\| \sin\phi,表示 ab 两向量所组成的平行四边形的面积。
- 性质:
- 坐标轴单位向量的叉乘:\vec{x} \times \vec{y} = +\vec{z}(排列组合共6种)
- 反交换律:\vec{a} \times \vec{b} = -\vec{b} \times \vec{a}
- 自身叉乘为零向量:\vec{a} \times \vec{a} = \vec{0}
- 分配律:\vec{a} \times (\vec{b} + \vec{c}) = \vec{a} \times \vec{b} + \vec{a} \times \vec{c}
- 数乘结合律:\vec{a} \times (k\vec{b}) = k(\vec{a} \times \vec{b})
- 笛卡尔坐标系下的叉乘:
- 分量形式: \vec{a} \times \vec{b} = \begin{pmatrix} y_a z_b - y_b z_a \\ z_a x_b - x_a z_b \\ x_a y_b - y_a x_b \end{pmatrix}
- 矩阵形式:\vec{a} \times \vec{b} = A^* \vec{b} = \begin{pmatrix} 0 & -z_a & y_a \\ z_a & 0 & -x_a \\ -y_a & x_a & 0 \end{pmatrix} \begin{pmatrix} x_b \\ y_b \\ z_b \end{pmatrix}
- 应用:
- 判断两个向量左右关系。
- 判断一个点位于凸多边形的“内外”关系。
8、标准正交基 / 坐标系
坐标系对表示点、位置、空间坐标至关重要,核心的问题就是不同坐标系 / 基之间的变换。
-
标准正交坐标系:满足以下条件的 3 个向量构成标准正交基
- 单位向量: \|\vec{u}\| = \|\vec{v}\| = \|\vec{w}\| = 1
- 正交: \vec{u} \cdot \vec{v} = \vec{v} \cdot \vec{w} = \vec{u} \cdot \vec{w} = 0
- 第三个向量由前两个叉乘得到: \vec{w} = \vec{u} \times \vec{v} \quad
-
向量分解(投影分解):
- 任意向量都可以用标准正交基线性表示,本质是在三个基向量上的投影之和
- \vec{p} = (\vec{p} \cdot \vec{u})\vec{u} + (\vec{p} \cdot \vec{v})\vec{v} + (\vec{p} \cdot \vec{w})\vec{w}
(三)矩阵
1、矩阵
可以理解为一个由数字组成的二维数组,在图形学中被广泛用于变换,比如平移、旋转、错切、缩放等。
2、矩阵加法与标量乘法
这个比较简单,对应元素分别操作即可,这里就不讲了。
3、矩阵乘法
- 最终的矩阵大小:(M \times N) \cdot (N \times P) = (M \times P)
- 性质:
- 不满足交换律:大部分情况下 AB \neq BA
- 结合律:(AB)C = A(BC)
- 分配律:A(B+C) = AB + AC 、 (A+B)C = AC + BC
4、矩阵的转置
- 意义:交换矩阵行与列 \begin{pmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{pmatrix}^T = \begin{pmatrix} 1 & 3 & 5 \\ 2 & 4 & 6 \end{pmatrix}
- 性质:(AB)^T = B^T A^T
5、单位矩阵
特征:I_{3\times3} = \begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix}
6、逆矩阵
- 必须满足:AA^{-1} = A^{-1}A = I
- 矩阵乘积的逆: (AB)^{-1} = B^{-1}A^{-1}
7、向量乘法转换为矩阵乘法:
- 将向量视为 m 行 1 列的列矩阵。
- 点乘:\vec{a} \cdot \vec{b} = \vec{a}^T \vec{b} = \begin{pmatrix} x_a & y_a & z_a \end{pmatrix} \begin{pmatrix} x_b \\ y_b \\ z_b \end{pmatrix} = \begin{pmatrix} x_a x_b + y_a y_b + z_a z_b \end{pmatrix}
- 叉乘:\vec{a} \times \vec{b} = A^* \vec{b} = \begin{pmatrix} 0 & -z_a & y_a \\ z_a & 0 & -x_a \\ -y_a & x_a & 0 \end{pmatrix} \begin{pmatrix} x_b \\ y_b \\ z_b \end{pmatrix}
三、Lecture 03 变换
(一)为什么学变换
因为它从应用来看很重要
- 模型变换:在静态场景中移动摄像机、控制机器人手臂旋转、电影入场标题字母的缩放变换。
- 视图变换:摄像机拍照中将 3D 转成 2D 的一个投影变换。
(二)二维变换
1、缩放变换 Scale Matrix
分为统一的缩放变换和非统一的缩放变换。统一表示各个分量缩放都是一致的,非统一则表示各个分量缩放不一致,如下图就是非统一的缩放。
M_{scale} =
\begin{bmatrix}
s_x & 0 \\
0 & s_y
\end{bmatrix}

2、反射矩阵 Reflection Matrix
将图像按照坐标轴翻转,分为水平翻转和垂直翻转,如下面就是图像按照 y 轴进行水平翻转的变换矩阵。
M_{refletion} =
\begin{bmatrix}
-1 & 0 \\
0 & 1
\end{bmatrix}

3、切变矩阵 Shear Matrix
分为X方向切变和Y方向切变,如下图就是X方向的切变,图像沿X轴倾斜,具体点就是 y=0 的地方 x 不变,再往上就是 x坐标发生了向右偏移,而 y 坐标始终不变。其中这个 x 变化根据三角函数可以得出是往右偏移了 ay 个单位。
M_{shear} =
\begin{bmatrix}
1 & a \\
0 & 1
\end{bmatrix}

4、旋转矩阵 Rotation Matrix
图像默认绕着原点进行旋转,并且默认都是逆时针旋转。根据下图两个特殊的点进行变换即可推算出这个旋转矩阵。
M_{rotation} =
\begin{bmatrix}
\cos\theta & -\sin\theta \\
\sin\theta & \cos\theta
\end{bmatrix}

5、线性变换
若对任意向量 α,β∈V 与标量 k 满足:
- T(α+β)=T(α)+T(β)
- T(kα)=kT(α)
则称 T 为线性变换,上面提到的这几个变换都是线性变换,写成矩阵的形式就是 x'=Mx,其中这个矩阵需要和向量的维度相同,也就是二维向量对应 2×2 的矩阵,为什么特意强调这个呢?因为接下来需要提到一个概念。
(三)齐次坐标
1、为什么需要齐次坐标
因为平移变换不能用矩阵变换的形式表示出来:
\begin{bmatrix} x' \\ y' \end{bmatrix} =
\begin{bmatrix} a & b \\ c & d \end{bmatrix}
\begin{bmatrix} x \\ y \end{bmatrix} +
\begin{bmatrix} t_x \\ t_y \end{bmatrix}

这并不是一个线性变换,需要额外加上一个矩阵才可以表示平移,但是我们不希望让平移变换变成一个特殊情况,因此引入了齐次坐标。
2、齐次坐标
通过增加一个维度可以将平移也写成矩阵相乘的形式。
\begin{bmatrix} x' \\ y' \\ w' \end{bmatrix} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y \\ 1 \end{bmatrix}
- 2D Point = \begin{pmatrix} x, y, \boldsymbol{1} \end{pmatrix}^T
- 2D Vector = \begin{pmatrix} x, y, \boldsymbol{0} \end{pmatrix}^T
这个 w 分量,点为 1 是因为受平移影响,向量为 0 则表示不受平移影响,即平移不变性。同时这个 w 分量在运算中也是有意义的,我们可以计算如下四种情况,注意结果中 w 的值,除了最后一个,其他都是有意义的。
- vector + vector = vector
- point - point = vector
- point + vector = point
- point + point = ?? (无意义)
对于最后一种情况,当 w \neq 0,齐次坐标 (x,y,w) 等价于二维点 \big(\frac{x}{w},\frac{y}{w}\big),即除以 w 归一化,使 w=1。
3、仿射变换
仿射变换是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间
- 仿射映射 = 线性映射 + 平移
引入齐次坐标后,变换就变成了下方这样,其中最下面一行一般是固定的,左上的 abcd 则是线性变换,右上的 t 则是平移变换。
\begin{pmatrix}
x' \\
y' \\
1
\end{pmatrix} =
\begin{pmatrix}
a & b & t_x \\
c & d & t_y \\
0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
x \\
y \\
1
\end{pmatrix}
(四)逆变换
M^{-1} 既是矩阵代数意义上 M 的逆,也是几何变换意义上的逆变换,即反向操作。

(五)复合变换
复合变换可以由多个简单变换组成,同时变换的顺序很重要,因为矩阵乘法不满足交换律。由于默认使用的是列向量,因此矩阵放左边,同时计算顺序是从右往左。

由于矩阵乘法满足结合律,因此可以先计算前面的所有矩阵的复合矩阵,再和向量相乘也是可以的。

(六)三维变换
三维和二维变换其实很像,只是多了一维而已。
- 3D Point = \begin{pmatrix} x, y, z, \boldsymbol{1} \end{pmatrix}^T
- 3D Vector = \begin{pmatrix} x, y, z, \boldsymbol{0} \end{pmatrix}^T
\begin{pmatrix} x' \\ y' \\ z' \\ 1 \end{pmatrix} = \begin{pmatrix} a & b & c & t_x \\ d & e & f & t_y \\ g & h & i & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}
四、Lecture 04 变换(续)
补充
正交矩阵:若矩阵的逆矩阵和转置矩阵一样,则该矩阵为正交矩阵。
(一)三维变换(续)
1、缩放矩阵
M_{scale} =
\begin{bmatrix}
s_{x} & 0 & 0 & 0 \\
0 & s_{y} & 0 & 0 \\
0 & 0 & s_{z} & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
2、平移矩阵
M_{translation} =
\begin{bmatrix}
1 & 0 & 0 & t_{x} \\
0 & 1 & 0 & t_{y} \\
0 & 0 & 1 & t_{z} \\
0 & 0 & 0 & 1
\end{bmatrix}
3、旋转矩阵
旋转矩阵是里面最复杂的了,当然推导和二维是类似的,就是固定某个轴不变,然后其他两个轴的变量进行旋转变换。

当然你肯定会有疑惑,为什么绕 y 轴旋转的这个这么怪异,好像和其他两个是“相反”的?
这里就涉及到循环对称的性质了,就是各个轴由其他两个轴叉乘得来:x × y → z 、$ y × z → x$ 、z × x → y。注意y轴这里,它是 z 和 x 轴叉乘而来,从 y 轴逆向看去,x 是纵轴,z 才是横轴,即 z-x 坐标系,而非 x-z 坐标系,因此算出来才会“相反“。
当然直接理解的话其实就是第3列(z向量)是原本横轴的内容,并且从第3行开始填,即填入 (\cos\alpha, \sin\alpha) ,由于填写 \sin\alpha 时超出了第3行,所以填入了第1行。而第1列(x向量)是原本纵轴的内容,填入的是(-\sin\alpha, \cos\alpha),也从第3行开始填,最终绕 y 轴旋转的矩阵就是这么个”怪异“的答案。

(二)3D旋转
1、欧拉角
是否可以用X/Y/Z单轴旋转矩阵组合出任意三维旋转?
R_{xyz}(\alpha,\beta,\gamma)=R_x(\alpha)\,R_y(\beta)\,R_z(\gamma)
这就是欧拉角(Euler angles),把任意三维旋转拆成先后三次绕坐标轴的单轴旋转相乘。比如对于飞行模拟器常用定义:
- Roll:横滚,机翼上下翻转。
- Pitch:俯仰,机头抬/低头。
- Yaw:偏航,机头左右转向。
缺陷:欧拉角会出现万向锁(Gimbal Lock),丢失一个旋转自由度;并且插值容易抖动。

2、罗德里格斯旋转公式
绕任意单位轴 \boldsymbol{n} 旋转角度 \alpha 的旋转矩阵:
\boldsymbol{R}(\boldsymbol{n},\alpha) =\cos(\alpha)\boldsymbol{I} +(1-\cos(\alpha))\boldsymbol{n}\boldsymbol{n}^T +\sin(\alpha) \underbrace{ \begin{pmatrix} 0 & -n_z & n_y \\ n_z & 0 & -n_x \\ -n_y & n_x & 0 \end{pmatrix} }_{\boldsymbol{N}}
推导繁琐,但思想比较简单。
3、四元数
四元数(Quaternion)的引入更多是为了旋转与旋转之间的插值,这里不过多讲解。
(三)观测变换
观测变换是该课程自己定义的一个概念,全称 Viewing transformation,它包含这两个部分:
- 视图变换:View / Camera transformation
- 投影变换:Projection transformation
下面就来讲解这两个变换。
(四)视图变换
1、概念
视图变换就是 View/Camera Transformation,要理解他就先以拍照为例,我们拍照分三步,也叫做MVP变换:
- 找好场地、摆放物体。(模型变换 Model transformation)
- 找一个合适角度放置相机。(视图变换 View transformation)
- 按下快门拍照,把3D场景拍为2D图像。(投影变换 Projection transformation)
而视图变换就是第二步,负责摆放摄像机的,想要确定摄像机摆放情况,我们需要定义三个参数:
- \vec{e}:相机位置(Position)
- \hat{g}:相机视线/观察方向(Look-at / gaze direction)
- \hat{t}:相机上方向(Up direction)

这里有一个核心思想就是,既然摄像机拍摄的是相对运动,那么为何不让相机固定住呢?相机永远在原点,永远以Y轴为向上,相机永远看向-Z方向(约定俗成),之后将相机变换到上述固定位置,其他物体也进行一样的变换,效果是一样的。
为了计算这个变换矩阵,我们就需要解决这些问题:
- 将相机位置 \vec{e} 平移到原点(平移)
- 将视线方向 \hat{g} 旋转对齐到 -Z,将上方向 \hat{t} 旋转对齐到 Y,将右方向 (\hat{g}\times\hat{t}) 旋转对齐到 X(旋转)
这样我们就可以将问题转换成先应用平移矩阵,再应用旋转矩阵:
M_{view}=R_{view}\cdot T_{view}

2、变换矩阵计算
(1)平移矩阵
这个很好理解,直接将相机移动到原点:
T_{view} =
\begin{bmatrix}
1 & 0 & 0 & -x_e \\
0 & 1 & 0 & -y_e \\
0 & 0 & 1 & -z_e \\
0 & 0 & 0 & 1
\end{bmatrix}
(2)旋转矩阵
我们想要将这三个轴分别变成原点的 XYZ 轴,即:
- 右轴 \hat{g}\times\hat{t} \rightarrow X
- 上轴 \hat{t} \rightarrow Y
- 视线 \hat{g} \rightarrow -Z
但是直接构造这个旋转很难,因此我们思考逆变换,即把标准坐标系 XYZ 转到相机自身坐标系,得出该矩阵后再求逆即可:
- X \rightarrow (\hat{g}\times\hat{t})
- Y \rightarrow \hat{t}
- Z \rightarrow -\hat{g}
这样这个旋转矩阵就很容易得出来了,每一列代表目标轴的向量(比如第一列表示从X轴变成摄像机自身的X轴),这样一个变换就可以将标准坐标系 XYZ 转换成摄像机对应三个轴的坐标系。
R_{view}^{-1}=
\begin{bmatrix}
x_{\hat{g}\times\hat{t}} & x_{t} & x_{-g} & 0 \\
y_{\hat{g}\times\hat{t}} & y_{t} & y_{-g} & 0 \\
z_{\hat{g}\times\hat{t}} & z_{t} & z_{-g} & 0 \\
0 & 0 & 0 & 1 \end{bmatrix}
之后求其逆矩阵即可,并且由于旋转矩阵是正交矩阵,所以逆矩阵就是对应的转置矩阵:
R_{view}=
\begin{bmatrix}
x_{\hat{g}\times\hat{t}} & y_{\hat{g}\times\hat{t}} & z_{\hat{g}\times\hat{t}} & 0 \\
x_{t} & y_{t} & z_{t} & 0 \\
x_{-g} & y_{-g} & z_{-g} & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
(五)投影变换
1、概念
投影变换就是 Projection transformation,分为透视投影和正交投影,这个很好理解,其实就是透视投影有近大远小的一个关系,而正交投影则没有。

2、正交投影
(1)概念
- 特点:平行光线投射,物体大小不会随距离变化,没有透视近大远小效果,常用于工程制图、UI。
- 简单理解:
- 相机放在原点,看向 -Z,上方向为 Y。(看向 -Z 是约定)
- 直接丢弃Z坐标,所有点沿平行于Z轴方向投影到 z=0 平面。
- 投影得到的矩形的 x,y 映射到 [-1,1]^2 的二维标准化设备坐标。
- 通用目标:把一个长方体视锥盒 [l,r]\times[b,t]\times[f,n] 映射到标准正则立方体(canonical cube) [-1,1]^3。前面六个字母就是六个方向的英文缩写。
(2)变换步骤
- 平移 Translate:把长方体中心平移到坐标原点
- 缩放 Scale:把长方体的长宽高缩放至2,刚好填满 [-1,1]^3

和前面简单理解略有差别:不是先丢Z,而是先把整个3D长方体变换到标准立方体,之后再做裁剪,最后才丢弃Z。
(3)正交投影矩阵
变换顺序:先平移,后缩放
M_{ortho}=
\underbrace{
\begin{bmatrix}
\frac{2}{r-l} & 0 & 0 & 0 \\
0 & \frac{2}{t-b} & 0 & 0 \\
0 & 0 & \frac{2}{n-f} & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}}_{\text{Scale矩阵}}
\cdot
\underbrace{
\begin{bmatrix}
1 & 0 & 0 & -\frac{r+l}{2} \\
0 & 1 & 0 & -\frac{t+b}{2} \\
0 & 0 & 1 & -\frac{n+f}{2} \\
0 & 0 & 0 & 1
\end{bmatrix}}_{\text{Translate矩阵}}
- 平移矩阵:把长方体中心点移动到原点。中心点坐标 (\frac{l+r}{2},\frac{b+t}{2},\frac{n+f}{2})
- 缩放矩阵:将每个轴方向长度缩放到2。X轴总长度 r-l,缩放系数 \frac{2}{r-l};Y、Z同理。
3、透视投影
(1)概念
- 特点:在计算机图形学、美术、人眼视觉系统中最常用,有近大远小的效果。
- 目标:和正交投影一样,最终都需要将视锥体内的坐标映射到标准正则立方体 [-1,1]^3 中。
(2)前置知识
齐次坐标性质回顾:齐次向量 (x,y,z,1)、(kx,ky,kz,k),k\neq0、(xz,yz,z^2,z),z\neq0 都代表三维空间中同一个笛卡尔点 (x,y,z)。后面推导会用到。
(3)变换步骤
- 挤压 Squish:把视锥体挤压成长方体,使之变得和正交投影的视锥体一样。
- 执行正交投影 Orthographic Projection:用正交投影的方法再进行变换。

(4)透视投影矩阵
我们主要关心的就是挤压的步骤,也就是求出把视锥体挤压成长方体的矩阵 M_{persp\rightarrow ortho}。
我们能确定的性质有这三条性质:
- 近平面(n所在平面)的所有点变换后位置不变。
- 远平面(f所在平面)的所有点变换后 z 值不变。
- 远平面中心点 (0, 0, f) 变换后位置不变。
可能你有疑问:我知道 xy 会有一定程度缩放,但是中间那些点的 z 值不是固定不变的吗?
答案其实是:除了近平面和远平面的 z 不变外,中间这些点的 z 都会变,并且是非线性变化。为什么?不能让 z 变换后还是 z 吗,就是让所有点 z 值保持不变不行吗?
对于这个问题,这是我的个人理解:如果要让变换后 z 轴保持不变,这样的一个通用矩阵是找不到的,也就是矩阵会随 z 的变化而变化,而这应该就是不能让 z 保持不变的原因吧:我们希望找到一个通用的矩阵,所有情况都可以适用,而不是找一个会不停变化的矩阵,而使用通用矩阵的话只能让 z 变化了。
首先 x 和 y 的变化关系由相似三角形很容易得到:
y'=\frac{n}{z}y,\quad x'=\frac{n}{z}x

然后是 z 的变化关系,上面我已经说了,z 是会变化的,但是至于怎么变化,目前还不知道,所以暂时设为 unknown。
\begin{pmatrix}
x\\
y\\
z\\
1
\end{pmatrix}
\Rightarrow
\begin{pmatrix}
\frac{n}{z}x\\
\frac{n}{z}y\\
unknown\\
1
\end{pmatrix}
由于这样矩阵会带有 z,也就是矩阵会随 z 变化,这是我们不希望看到的,因此我们这里需要用齐次坐标的性质消掉 z。即矩阵全部分量乘以同一个数,它们在三维空间中还是指的同一个点:
\begin{pmatrix}
\frac{n}{z}x\\
\frac{n}{z}y\\
unknown\\
1
\end{pmatrix}
\overset{\times z}{==}
\begin{pmatrix}
nx\\
ny\\
unknown\\
z
\end{pmatrix}
好的,现在这个变换就变成了:
M_{persp\rightarrow ortho}
\begin{pmatrix}x\\y\\z\\1\end{pmatrix}=
\begin{pmatrix}nx\\ny\\unknown\\z\end{pmatrix}
目前就能够确认矩阵的已知部分,除了第三行,其他肯定都能确认:
M_{persp\rightarrow ortho}=
\begin{pmatrix}
n & 0 & 0 & 0 \\
0 & n & 0 & 0 \\
? & ? & ? & ? \\
0 & 0 & 1 & 0
\end{pmatrix}
现在的目标就是求出矩阵第三行,首先前两列一定是0,因为这个输出结果应该和 xy 无关,因此第三行的形式为:\boldsymbol{(0\ \ 0\ \ A\ \ B)}。为了求出 AB,我们需要取一些特殊位置来解,还记得上面的三条性质吗,这就是关键。
- 近平面上所有点,变换后位置不变,近平面的 z = n,因此带入矩阵:
M_{persp\rightarrow ortho} \begin{pmatrix} x\\y\\n\\1 \end{pmatrix} = \begin{pmatrix} x\\y\\n\\1 \end{pmatrix} \overset{\times n}{==} \begin{pmatrix} nx\\ ny\\ n^2\\ n \end{pmatrix}
关注第三行,这样可以获得一个等式:
\begin{pmatrix}
0 & 0 & A & B
\end{pmatrix}
\begin{pmatrix}
x\\y\\n\\1
\end{pmatrix}= An+B = n^2
- 远平面中心点 (0, 0, f) 变换后位置不变。
M_{persp\rightarrow ortho} \begin{pmatrix} 0\\0\\f\\1 \end{pmatrix} = \begin{pmatrix} 0\\0\\f\\1 \end{pmatrix} \overset{\times f}{==} \begin{pmatrix} 0\\ 0\\ f^2\\ f \end{pmatrix}
关注第三行,这样又可以获取一个等式:
\begin{pmatrix}
0 & 0 & A & B
\end{pmatrix}
\begin{pmatrix}
0\\0\\f\\1
\end{pmatrix} = Af+B = f^2
两个等式结合,解得:
A = n+f,\quad B = -nf
因此这个挤压矩阵我们求出来是:
M_{persp\rightarrow ortho}=
\begin{pmatrix}
n & 0 & 0 & 0 \\
0 & n & 0 & 0 \\
0 & 0 & n+f & -nf \\
0 & 0 & 1 & 0
\end{pmatrix}
挤压完成之后,继续执行正交投影 M_{ortho},就可以得到最终透视矩阵:
M_{persp}=M_{ortho}\cdot M_{persp\rightarrow ortho}
(六)思考
结尾闫令琪老师留了一个作业:挤压后的中间的 z 是偏离相机点还是靠近相机点,我算出来是远离相机的,这是计算过程:

附录:课程作业环境搭建
1、准备
我使用的是VS2022+OpenCV+Eigen。需要准备如下三样东西:
- VS2022:官网下载即可
- Eigen:可以在github中下载,我下载的是这个:janm31415/eigen-3.4.0: Mirror of eigen 3.4.0
- OpenCV:下载对应平台的即可,比如Windows:Just a moment…
2、配置Eigen
- 右键解决方案,点击属性

- 选择C/C++ → 常规 → 附加包含目录,选中Eigen所在目录即可。

- 验证,在main中输入如下代码,如果没有报错并且正确打印两个矩阵的信息则说明配置正确。
#include <iostream>
#include <Eigen/Dense>
using namespace std;
using namespace Eigen;
int main() {
cout << "<========================>" << endl;
Matrix<float, 3, 3>matrixA;
matrixA.setZero();
cout << matrixA << endl;
Matrix<float, 3, 3>matrixA1;
matrixA1.setZero();
cout << '\n' << matrixA1 << endl;
cout << "<========================>" << endl;
return 0;
}
注:作业里可能是 #include <eigen3/Eigen/Eigen> 的形式,这时候会报错,找不到头文件,解决办法有两个:
- 删除头文件前面的 ”eigen3/“ 路径。
- 调整 ”附加包含目录“,先将 eigen 所在文件夹命名为 eigen3,然后这个目录选取其父目录即可。
3、配置OpenCV
-
下载好后,先配置环境变量,将
opencv\build\x64\vc16\bin这个路径添加到环境变量的Path中 -
右键解决方案,点击属性,选择VC++目录,配置包含目录和库目录
- 包含目录:
opencv\build\include - 库目录:
opencv\build\x64\vc16\lib
- 包含目录:

- 配置附加依赖项,在刚才的lib目录下找到这个库文件,复制名字

- 然后在 链接器 → 输入 → 附加依赖项 中将刚才复制的名字粘贴进去

- 之后运行如下程序进行测试,如果能正常打开图片代表成功
#include <opencv2/opencv.hpp>
using namespace cv;
int main() {
Mat img = imread("你的图片路径");
imshow("test", img);
waitKey(0);
return 0;
}