很多情况下,我们需要打交道的只是一些基础数学运算。
4.1 背景:农场游戏
4.2 笛卡尔坐标系
4.2.1 二维笛卡尔坐标系
一个二维笛卡尔坐标系包含两部分:
- 原点
- 两条过原点互相垂直的矢量(基矢量),即 x 轴和 y 轴
4.2.2 三维笛卡尔坐标系
4.2.3 左手坐标系和右手坐标系
- 所有二维笛卡尔坐标系都是等价的,但三维不是
- 若两个坐标系具有相同的
旋向性,那么就可以通过旋转的方法来让它们的坐标轴指向重合
- 坐标系旋转的
正方向定义,在左手坐标系中由左手法则来定义,而在右手坐标系中则是由右手法则来定义
4.2.4 Unity 使用的坐标系
模型空间和世界空间,使用的是左手坐标系观察空间,用的是右手坐标系
4.3 点和矢量
- 点 是 n 维空间中的一个
位置,没有大小等概念 - 矢量 是指 n 维空间中一种包含了
模(长度)和方向的有向线段
4.3.1 点和矢量的区别
- 矢量通常用于描述偏移量,因此它可用于描述相对位置
- 矢量的头尾代表两个位置(点)
- 若矢量的尾固定在坐标系原点,那么这个矢量的表示就和点的表示重合了
- 任何一个点都可以表示成一个从原点出发的矢量
4.3.2 矢量运算
1. 矢量和标量的乘法/除法
乘法:
除法,等于与标量的倒数相乘(注意只能是矢量被标量除):
几何意义:
- 对矢量进行缩放
2. 矢量的加法和减法
公式如下:
- 注意:矢量不可和标量相加/减,或者和不同维度的矢量进行运算
几何意义:
- 加法:偏移叠加
- 减法:a - b 得到点 a 相对于点 b 的位置,即点 b 到点 a 的矢量
3. 矢量的模
一个标量,可理解为矢量的长度
三维矢量模的计算公式:
4. 单位矢量
模为 1 的矢量,亦被称为归一化矢量。在不关心矢量长度的情况下使用
公式:
- 零矢量(矢量的每个分量都为 0)是不可被归一化的
5. 矢量的点积(内积)
公式一
- 矢量点积满足交换律,即
几何意义:
- 投影:点积 得到 b 在 方向上的有符号投影
- 投影结果有正负号与 和 b 的方向有关
- 小于 0:它们方向相反(夹角大于 90 度)
- 等于 0:它们方向互相垂直(夹角为 90 度)
- 大于 0:它们方向相同(夹角小于 90 度)
- 投影结果有正负号与 和 b 的方向有关
点积具有一些重要的性质,在 Shader 的计算中,我们会经常利用到
- 性质一:点积可结合标量乘法
点积操作数之一可以是另一个运算的结果
也就是说,对点积中其中一个矢量进行缩放,相当于对最后的点积结果进行缩放
公式:
- 性质二:点积可结合矢量加法和减法,和性质一类似
公式:
- 性质三:一个矢量和本身进行点积的结果,是该矢量的模的平方
可以利用来直接比较(不必开平方)两矢量的长度
公式二
利用这个公式我们还可以求得两向量间的夹角:
6. 矢量的叉积(外积)
公式:
- 叉乘不满足交换律,但
满足反交换律,即 - 结果是一个矢量
- 它的
模: (即它们构建成的平行四边形的面积) - 它的
方向:跟使用的坐标系(左手还是右手)有关
- 它的
- 注意:无论使用哪种坐标系,都不会对计算结果产生影响。影响的只是计算结果在三维空间中的
视觉表现而已
4.4 矩阵
4.4.1 矩阵的定义
4.4.2 和矢量联系起来
- 我们可以用矩阵来表示矢量(nx1 的列矩阵或 1xn 的行矩阵)
4.4.3 矩阵运算
1. 矩阵和标量的乘法
2. 矩阵和矩阵的乘法
-
前一个矩阵的
列数必须等于后一个矩阵的行数,否则两矩阵不能相乘 -
C=AB,那么 C 中的每个元素 等于 A 的第 i 行和 B 的第 j 列点乘的结果
-
性质一:不满足交换律
- 性质二:满足结合律
4.4.4 特殊的矩阵
1. 方块矩阵
行数和列数相等的矩阵
-
对角矩阵:除
对角元素外的所有元素都是 0
2. 单位矩阵(相当于数值 1)
特殊的对象矩阵,对角元素都为 1,用 表示
任何矩阵和它相乘的结果还是原来的矩阵
3. 转置矩阵
- 性质一:矩阵转置的转置等于原矩阵
- 性质二:矩阵串接的转换,等于反向串接各个矩阵的转置
4. 逆矩阵(相当于数值的倒数)
有逆矩阵的矩阵(可逆/非奇异的):
- 必须是
方阵 行列式不为 0
一个方阵 M,它的逆矩阵用 来表示,那 M 和 相乘的结果是一个单位矩阵
- 性质一:逆矩阵的逆矩阵是原矩阵本身
- 性质二:单位矩阵的逆矩阵是它本身
- 性质三:转置矩阵的逆矩阵是逆矩阵的转置
- 性质四:矩阵串接相乘后的逆矩阵,等于反向串接各个矩阵的逆矩阵(跟转置的性质二类似)
几何意义: 还原变换(撤销)
5. 正交矩阵
- 正交是矩阵的一种
属性 - 若一个方阵 M 和它的转置矩阵的乘积是单位矩阵的话,就说这个矩阵是
正交的
- 和上面的逆矩阵公式很像。结合起来可得到一个重要的性质:若一个矩阵是正交的,那么它的
转置矩阵和逆矩阵是一样的。这个性质用来优化还原变换的性能——用转置矩阵替代逆矩阵
-
根据一个矩阵的构造过程来判断它是否是正交的
- 矩阵的每一行代表的矢量,都是
单位矢量。只有这样它们与自己的点积才能是 1 - 矩阵的第一行代表的矢量,它们
互相垂直。只有这样它们之间的点积才能是 0 - 上述两条对矩阵的每一列同样适用。因为若 M 是正交的话, 也会是正交的
- 矩阵的每一行代表的矢量,都是
-
一组
标准正交基可以精确满足上述条件。与正交基不同,标准正交基每个基矢量的长度为 1 -
即:使用标准正交基构建的矩阵是正交的,但使用正交基来构建的矩阵未必是正交的
4.4.5 行矩阵还是列矩阵
- 矩阵与矢量对应的
行矩阵和列矩阵相乘的结果,里面的元素是不一样的 - 在和矩阵相乘时选择行矩阵还是列矩阵来表示矢量
非常重要 - 在 Unity 中,常规做法是把矢量(
列矩阵)放在矩阵的右侧,即右乘(矩阵右乘矢量)右乘定义
4.5 矩阵的几何意义:变换
4.5.1 什么是变换
指的是把一些数据,如点、矢量甚至是颜色等,通过某种方式进行转换的过程
线性变换
指的是那些可以保留
矢量加和标量乘的变换
用数学公式来表示这两个条件
缩放就是一种线性变换。如 ,可以表示一个大小为 2 的统一缩放旋转也是线性变换(平移不是)。如果要对一个三维矢量进行变换,仅使用 3x3 的矩阵就可以表示所有线性变换- 平移变换,如 。它就不是线性变换,它
满足标量乘法,但不满足矢量加法
仿射变换
合并线性变换和平移变换的变换类型。用来
处理平移变换
- 可以使用一个 4x4 矩阵来表示,这就是
齐次坐标空间
常见的变换种类和特性
4.5.2 齐次坐标
为方便计算增加到指定维度(泛指 4x4)的矩阵,并没有其他神秘之处
方法:
三维坐标转换成齐次坐标是把其 w 分量设为 1- 而对于
方向矢量,需要把 w 分量设为 0 - 即 w 分量可以理解为点/方向矢量的开关——当用 4x4 矩阵对点进行变换有效,对
方向矢量无效(方向矢量只表示方向,无需位置、旋转)
4.5.3 分解基础变换矩阵
-
把表示纯平移、旋转和缩放的变换矩阵叫做
基础变换矩阵。我们把一个基础变换矩阵分解成 4 个部分:- 左上角矩阵 表示旋转和缩放
- 右上角矩阵 表示平移
- 是零矩阵
- 右下角就是标量 1
4.5.4 平移矩阵
如果我们对一个方向矢量进行平移变换,可以发现不会对其产生任何影响
4.5.5 缩放矩阵
与平移矩阵不一样,缩放矩阵对方向矢量有效
4.5.6 旋转矩阵
绕着 x 轴旋转 度
绕 y 轴旋转的
绕 z 轴旋转的
4.5.7 复合变换
可以将平移、旋转和缩放组合起来,形成一个复杂的变换
-
由于 是列矩阵,因此阅读顺序是
从右到左,先缩放后旋转最后平移(SRT) -
旋转的变换顺序也需要注意。将点分别绕 x,y,z 旋转 时,在 Unity 中,旋转顺序是
zxy(注意矩阵的顺序不是从右往左)
4.6 坐标空间
渲染游戏的过程可以理解为将一个个顶点经层层处理最终转化到屏幕上的过程
4.6.1 为什么要这么多不同的坐标空间
我们需要在不同情况下使用不同的坐标空间
4.6.2 坐标空间的变换
- 在渲染流水线中,我们需要把一个点或方向矢量从一个坐标空间转换到另一个中
- 所有坐标空间都是相对的,每个坐标空间都是另一个坐标空间的子空间,反过来说,每个空间都有一个父坐标空间
子空间到父空间的转换矩阵
- 是子空间坐标轴向量在父空间中的表示,把它们依次放入矩阵的前 3 列,把
原点矢量放到最后一列,再用 0 和 1 填充最后一行 - 反过来,可以从这个变换矩阵中获取子坐标空间的
原点和坐标轴方向 - 对
方向矢量的坐标空间变换,不需要平移变换:
父空间到子空间的转换矩阵
-
。若 是正交的,则 。也就是说:
-
父坐标空间的坐标轴方向在子坐标空间中的表示 x,y,z,它们对应的就是 的
每一行
结论:
- 若知 A 空间坐标轴矢量在 B 空间中的表示
- 想得到 的话,将 放到列上
- 想得到 的话,将 放到行上
4.6.3 顶点的坐标空间变换过程
4.6.4 模型空间
- 也可称为
对象空间或局部空间 - 每个模型都有自己独立的坐标空间,当它移动或旋转时,空间也会跟着移动和旋转
- 我们在
顶点着色器中访问到的模型顶点信息,其中的顶点坐标都是相对于模型空间中的原点 - 由于顶点变换中往往包含了
平移变换,所以得把其扩展到齐次坐标系下
4.6.5 世界空间
一个特殊的坐标系,它建立了我们所关心的最大的空间
顶点变换的第一步,就是将顶点坐标从模型空间变换到世界空间中。这个变换通常叫做模型变换(因为是用的是模型的 Transform 组件信息来构建变换矩阵,所以叫模型变换。后面的变换同理)
- 根据模型 Transform 组件上的信息构建出
模型变换的变换矩阵 - 将模型空间中的顶点坐标转换到世界空间下:
4.6.6 观察空间
- 也称摄像机空间,使用的是
右手坐标系(+z 方向为摄像机的后方,即从屏幕里向屏幕外) - 顶点变换的第二步,就是将顶点坐标从
世界空间变换到观察空间中。这个变换通常叫做观察变换 - 求出变换矩阵的方法:
-
1、构建出观察空间到世界空间的变换矩阵,再求逆
-
2、想象平移整个观察空间,让摄像机原点位于世界坐标的原点,坐标轴与世界空间中的重合。步骤(根据摄像机的 Transform 信息):
- 先逆平移
- 再逆旋转
- 由于观察空间使用的是右手坐标系,故需要对 z 分量进行取反。通过乘以一个特殊的矩阵:
-
- 至此,已得出变换矩阵,可将世界空间下的顶点坐标变换为观察空间下
4.6.7 裁剪空间
- 也称为
齐次裁剪空间。用于变换的矩阵叫做裁剪矩阵,也称为投影矩阵 - 目的是能够方便地对渲染图元进行裁剪:与这块空间边界相交的图元会被裁剪
- 裁剪空间由
视锥体来决定- 视锥体指的是空间中的一块区域,决定了摄像机可以看到的空间
- 由六个平面包围而成,这些平面被称为
裁剪平面 - 有两种类型:
正交投影和透视投影
- 想要判断一个顶点是否处于一个“金字塔”是比较麻烦的,我们要用一种更加通用、方便和整洁的方式来进行判断——通过一个
投影矩阵将顶点转换到一个裁剪空间中 - 投影矩阵有两个目的
- 首先是为(真正的)投影做准备,它发生在后面的
齐次除法过程中。经过这里的投影矩阵变换后,顶点的w 分量将具特殊意义 - 其次是对 x,y,z 分量进行缩放。经投影矩阵缩放后,我们可以直接使用 w 分量作为一个范围值,若分量在此范围内,就说明该顶点在裁剪空间内
- 首先是为(真正的)投影做准备,它发生在后面的
1.透视投影
- 可通过 Camera 组件的 Field of View 来改变视锥体竖直方向的张开角度,再结合 Clipping Planes 中的 Near 和 Far 参数来算出近裁剪平面和远裁剪平面的
高度
-
再通过摄像机
横纵比得到两裁剪平面的宽度 -
投影矩阵推导过程(略)
-
顶点从观察空间变换到裁剪空间的结果,本质就是对 x,y,z 分量进行了不同程度的缩放(z 分量还做了一个平移),目的是为了方便裁剪
- w 分量不再是 1,而是原先 z 分量取反的结果
- 可按如下不等式来判断一个变换后的顶点是否在视锥体内
- 任何不满足上述条件的图元都需要被剔除或裁剪
2. 正交投影
- Camera 组件的 Size 属性影响的是视锥体竖直方向上高度的一半
- 与透视投影不同,使用正交投影的投影矩阵对顶点进行变换后,其 w 分量仍然为 1
- 判断变换后的顶点是否位于视锥体内使用的不等式跟透视投影中的一样
4.6.8 屏幕空间
-
当经过投影矩阵的变换并进行裁剪后,就需要进行真正的投影了——把视锥体投影到
屏幕空间,得到真正的像素位置 -
屏幕空间是一个
二维空间,步骤:- 标准
齐次除法,也被称为透视除法,将坐标从齐次裁剪空间转换到归一化设备坐标(NDC)。操作非常简单,就是用齐次坐标系的 w 分量去除以 x,y,z 分量
对于正交投影来说,它的裁剪空间实际已经是一个立方体了,且经过投影变换后的顶点 w 分量为 1,因此齐次除法并不会对顶点的 x,y,z 分量产生影响
- 根据变换后的 x 和 y 坐标来映射输出窗口的对应
像素坐标。z 分量被用于深度缓冲
- 标准
-
在 Unity 中,从裁剪空间到屏幕空间的转换是由 Unity 帮我们完成的,顶点着色器只需要将顶点转换到裁剪空间即可
4.6.9 总结
4.7 法线变换
-
法线,也称为法矢量,是需要我们特殊处理的另一种方向矢量,以便在后续(如片元着色器)中计算光照等 -
点和绝大部分方向矢量都可以使用同一个 4x4 或 3x3 的变换矩阵 把其从空间 A 变换到空间 B 中。但使用这个矩阵来变换法线的话,可能会无法确保法线的垂直性(进行非统一缩放时)
-
使用原变换矩阵的逆转置矩阵 来变换法线就可得到正确的结果
-
若变换矩阵 只包含旋转变换,即它是
正交的,那么 ,继续推导 (转置的转置等于本身),意味着可以直接使用 来变换法线 -
若变换矩阵 只包含旋转和统一缩放,我们利用统一
缩放系数 k来得到变换矩阵的逆转置矩阵 。这样就可避免计算逆矩阵的过程
4.8 Unity Shader 的内置变量(数学篇)
4.8.1 变换矩阵
4.8.2 摄像机和屏幕参数
4.9 答疑解惑
4.9.1 使用 3x3 还是 4x4 的变换矩阵
- 对于线性变换(如旋转和缩放),和对方向矢量的变换,3x3 就足够了
4.9.2 CG 中的矢量和矩阵类型
- CG 使用行优先方法,一行一行地填充矩阵
4.9.3 Unity 中的屏幕坐标:ComputeScreenPos / VPOS / WPOS
- 想得到片元在屏幕上的像素位置,有两种方法
- VPOS 或 WPOS 语义
- Unity 提供的 ComputeScreenPos 函数