一个二维的笛卡儿坐标系包含了两个部分的信息:
在模型空间中,一个物体的右侧(right)、上侧(up)和前侧(forward)分别对应了x轴、y轴和z轴的正方向。
矢量的乘法有两种最常用的种类:点积(dot product,也被称为内积,inner product)和叉积(cross product,也被称为外积,outer product)
点积: a ·b=(ax,ay,az)·(bx,by,bz)=axbx+ayby+azbz a·b=|a||b|cosθ
矢量的点积满足交换律,即a·b=b·a
需要注意的是,叉积不满足交换律,即a×b≠b×a。实际上,叉积是满足反交换律的,即a×b=-(b×a)。而且叉积也不满足结合律,即 (a×b) ×c≠a×(b×c)。
|a×b|=|a||b|sinθ 注意,是绝对值的公式,方向还需要另外考虑
矢量可以看成是n×1的列矩阵(column matrix)或1×n的行矩阵(row matrix),其中n对应了矢量的维度。例如,矢量v=(3,8,6)可以写成行矩阵 [3 8 6] 或者列矩阵
一个r×n的矩阵A和一个n×c的矩阵B相乘,它们的结果AB将会是一个r×c大小的矩阵
C中的每一个元素cij等于A的第i行所对应的矢量和B的第j列所对应的矢量进行矢量点乘的结果,即
对于每个元素cij,我们找到A中的第i行和B中的第j列,然后把它们的对应元素相乘后再加起来,这个和就是cij。
AB≠BA (AB)C=A(BC) ABCDE=((A(BC))D)E=(AB)(CD)E
方块矩阵(square matrix),简称方阵,是指那些行和列数目相等的矩阵。
如果一个矩阵除了对角元素外的所有元素都为0,那么这个矩阵就叫做对角矩阵(diagonal matrix)。
单位矩阵(identity matrix),用In来表示
任何矩阵和它相乘的结果还是原来的矩阵。
转置矩阵(transposed matrix)实际是对原矩阵的一种运算,即转置运算。给定一个r×c的矩阵M,它的转置可以表示成MT,这是一个c×r的矩阵。转置矩阵的计算非常简单,我们只需要把原矩阵翻转一下即可。也就是说,原矩阵的第i行变成了第i列,而第j列变成了第j行。数学公式是:
矩阵串接的转置,等于反向串接各个矩阵的转置
逆矩阵(inverse matrix)
不是所有的矩阵都有逆矩阵,第一个前提就是,该矩阵必须是一个方阵。
给定一个方阵M,它的逆矩阵用M-1来表示。逆矩阵最重要的性质就是,如果我们把M和M-1相乘,那么它们的结果将会是一个单位矩阵。也就是说
如果一个矩阵有对应的逆矩阵,我们就说这个矩阵是可逆的(invertible)或者说是非奇异的(nonsingular);相反的,如果一个矩阵没有对应的逆矩阵,我们就说它是不可逆的(noninvertible)或者说是奇异的(singular)。
如果一个矩阵的行列式(determinant)不为0,那么它就是可逆的
正交矩阵(orthogonal matrix)。正交是矩阵的一种属性。如果一个方阵M和它的转置矩阵的乘积是单位矩阵的话,我们就说这个矩阵是正交的(orthogonal
我们可以得到以下结论:
一个坐标空间需要指定一组基矢量,也就是我们理解的坐标轴。如果这些基矢量之间是互相垂直的,那么我们就把它们称为是一组正交基(orthogonal basis)。但是,它们的长度并不要求一定是1。如果它们的长度的确是1的话,我们就说它们是一组标准正交基(orthonormal basis)
在Unity中,常规做法是把矢量放在矩阵的右侧,即把矢量转换成列矩阵来进行运算,我们的矩阵乘法通常都是右乘
变换(transform),指的是我们把一些数据,如点、方向矢量甚至是颜色等,通过某种方式进行转换的过程。
线性变换指的是那些可以保留矢量加和标量乘的变换。用数学公式来表示这两个条件就是:
缩放(scale)就是一种线性变换。例如,f(x)=2x,可以表示一个大小为2的统一缩放,即经过变换后矢量x的模将被放大两倍
旋转(rotation)也是一种线性变换。对于线性变换来说,如果我们要对一个三维的矢量进行变换,那么仅仅使用3×3的矩阵就可以表示所有的线性变换。
仿射变换(affine transform)。仿射变换就是合并线性变换和平移变换的变换类型。仿射变换可以使用一个4×4的矩阵来表示,为此,我们需要把矢量扩展到四维空间下,这就是齐次坐标空间(homogeneous space)。
那么,我们如何把三维矢量转换成齐次坐标呢?
对于一个点,从三维坐标转换成齐次坐标是把其w分量设为1,而对于方向矢量来说,需要把其w分量设为0。这样的设置会导致,当用一个4×4矩阵对一个点进行变换时,平移、旋转、缩放都会施加于该点。但是如果是用于变换一个方向矢量,平移的效果就会被忽略。
可以使用一个4×4的矩阵来表示平移、旋转和缩放。我们把表示纯平移、纯旋转和纯缩放的变换矩阵叫做基础变换矩阵。这些矩阵具有一些共同点,我们可以把一个基础变换矩阵分解成4个组成部分:
其中,左上角的矩阵M3×3用于表示旋转和缩放,t3×1用于表示平移,01×3是零矩阵,即01×3=[00 0],右下角的元素就是标量1。
我们可以把平移、旋转和缩放组合起来,来形成一个复杂的变换过程
变换的结果是依赖于变换顺序的,由于矩阵乘法不满足交换律,因此矩阵的乘法顺序很重要
在绝大多数情况下,我们约定变换的顺序就是先缩放,再旋转,最后平移
每个空间都有一个父(parent)坐标空间。对坐标空间的变换实际上就是在父空间和子空间之间对点和矢量进行变换。
假设,现在有父坐标空间 P 以及一个子坐标空间 C 。我们知道在父坐标空间中子坐标空间的原点位置以及3个单位坐标轴。我们一般会有两种需求:一种需求是把子坐标空间下表示的点或矢量Ac转换到父坐标空间下的表示Ap,另一个需求是反过来,即把父坐标空间下表示的点或矢量Bp转换到子坐标空间下的表示Bc。我们可以使用下面的公式来表示这两种需求:
其中,Mc→p表示的是从子坐标空间变换到父坐标空间的变换矩阵,而Mp→c是其逆矩阵(即反向变换)
如何求出从子坐标空间到父坐标空间的变换矩阵Mc→p。
现在,我们已知子坐标空间C的3个坐标轴在父坐标空间P下的表示xc、yc、zc,以及其原点位置Oc。当给定一个子坐标空间中的一点Ac=(a, b,c),我们同样可以依照上面4个步骤来确定其在父坐标空间下的位置Ap:
其中的“ | ”符号表示按列展开
我们把上面的式子扩展到齐次坐标空间中,得到:
当我们已知从模型空间到世界空间的一个4×4的变换矩阵,可以提取它的第一列再进行归一化后(为了消除缩放的影响)来得到模型空间的x轴在世界空间下的单位矢量表示。同样的方法可以提取y轴和z轴。我们可以从另一个角度来理解这个提取过程
而现在,我们不仅可以根据变换矩阵Mc→p反推出子坐标空间的坐标轴方向在父坐标空间中的表示xc、yc 和 zc,还可以反推出父坐标空间的坐标轴方向在子坐标空间中的表示xp、yp 和 zp,这些坐标轴对应的就是Mc→p的每一行!也就是说,如果我们知道坐标空间变换矩阵MA→B是一个正交矩阵,那么我们可以提取它的第一列来得到坐标空间 A 的x轴在坐标空间 B 下的表示,还可以提取它的第一行来得到坐标空间 B 的x轴在坐标空间 A 下的表示。反过来,如果我们知道坐标空间B的x轴、y轴和z轴(必须是单位矢量,否则构建出来的就不是正交矩阵了)在坐标空间 A 下的表示,就可以把它们依次放在矩阵的每一行就可以得到从 A 到 B 的变换矩阵了。
模型空间(model space),是和某个模型或者说是对象有关的。有时模型空间也被称为对象空间(object space)或局部空间(local space)。每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转。
由于顶点变换中往往包含了平移变换,因此需要把其扩展到齐次坐标系下,得到顶点坐标是(0, 2, 4, 1)
世界空间(world space)
世界空间可以被用于描述绝对位置
在Unity中,世界空间同样使用了左手坐标系。但它的x轴、y轴、z轴是固定不变的
顶点变换的第一步,就是将顶点坐标从模型空间变换到世界空间中。这个变换通常叫做模型变换(model transform)。
观察空间(view space)也被称为摄像机空间(camera space)
观察空间可以认为是模型空间的一个特例
Unity中观察空间的坐标轴选择是:+x轴指向右方,+y轴指向上方,而+z轴指向的是摄像机的后方
观察空间是一个三维空间,而屏幕空间是一个二维空间。从观察空间到屏幕空间的转换需要经过一个操作,那就是投影(projection)。
顶点变换的第二步,就是将顶点坐标从世界空间变换到观察空间中。这个变换通常叫做观察变换(view transform)。
顶点接下来要从观察空间转换到裁剪空间(clip space,也被称为齐次裁剪空间)中,这个用于变换的矩阵叫做裁剪矩阵(clip matrix),也被称为投影矩阵(projection matrix)。
视锥体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。视锥体由六个平面包围而成,这些平面也被称为裁剪平面(clip planes)。视锥体有两种类型,这涉及两种投影类型:一种是正交投影(orthographic projection),一种是透视投影(perspective projection)
在视锥体的6块裁剪平面中,有两块裁剪平面比较特殊,它们分别被称为近剪裁平面(near clip plane)和远剪裁平面(far clip plane)。它们决定了摄像机可以看到的深度范围。
投影矩阵有两个目的。
视锥体的意义在于定义了场景中的一块三维空间。所有位于这块空间内的物体将会被渲染,否则就会被剔除或裁剪。我们已经知道,这块区域由6个裁剪平面定义,那么这6个裁剪平面又是怎么决定的呢?在Unity中,它们由Camera组件中的参数和Game视图的横纵比共同决定
我们可以通过Camera组件的Field of View(简称FOV)属性来改变视锥体竖直方向的张开角度,而Clipping Planes中的Near和Far参数可以控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近。这样,我们可以求出视锥体近裁剪平面和远裁剪平面的高度
经过投影矩阵的变换后,我们可以进行裁剪操作。当完成了所有的裁剪工作后,就需要进行真正的投影了,也就是说,我们需要把视锥体投影到屏幕空间(screen space)中
法线(normal),也被称为法矢量(normal vector)
切线(tangent),也被称为切矢量(tangent vector)
由于切线是由两个顶点之间的差值计算得到的,因此我们可以直接使用用于变换顶点的变换矩阵来变换切线。因为切线和法线都是方向矢量,不会受平移的影响
其中 TA和TB 分别表示在坐标空间A下和坐标空间B下的切线方向
这里的函数都是Unity 5.2版本的,可能会与现在使用的不同,此处仅供参考
对于线性变换(例如旋转和缩放)来说,仅使用3×3的矩阵就足够表示所有的变换了。但如果存在平移变换,我们就需要使用4×4的矩阵。因此,在对顶点的变换中,我们通常使用4×4的变换矩阵。当然,在变换前我们需要把点坐标转换成齐次坐标的表示,即把顶点的w分量设为1。而在对方向矢量的变换中,我们通常使用3×3的矩阵就足够了,这是因为平移变换对方向矢量是没有影响的。
在CG中,矩阵类型是由float3x3、float4x4等关键词进行声明和定义的。而对于float3、float4等类型的变量,我们既可以把它当成一个矢量,也可以把它当成是一个1 x n的行矩阵或者一个n x 1的列矩阵。这取决于运算的种类和它们在运算中的位置。例如,当我们进行点积操作时,两个操作数就被当成矢量类型
- float4 a = float4(1.0, 2.0, 3.0, 4.0);
- float4 b = float4(1.0, 2.0, 3.0, 4.0);
- // 对两个矢量迚行点积操作
- float result = dot(a, b);
但在进行矩阵乘法时,参数的位置将决定是按列矩阵还是行矩阵进行乘法
- float4 v = float4(1.0, 2.0, 3.0, 4.0);
- float4x4 M = float4x4(1.0, 0.0, 0.0, 0.0,
- 0.0, 1.0, 0.0, 0.0,
- 0.0, 0.0, 1.0, 0.0,
- 0.0, 0.0, 0.0, 1.0);
- // 把v当成列矩阵和矩阵M迚行右乘
- float4 column_mul_result = mul(M, v);
- // 把v当成行矩阵和矩阵M迚行左乘
- float4 row_mul_result = mul(v, M);
- // 注意:column_mul_result不等于row_mul_result,而是:
- // mul(M, v) == mul(v, tranpose(M))
- // mul(v, M) == mul(tranpose(M), v)
在CG中,对float4x4等类型的变量是按行优先的方式进行填充的
在顶点/片元着色器中,有两种方式来获得片元的屏幕坐标。
一种是在片元着色器的输入中声明VPOS或WPOS语义(关于什么是语义,可参见5.4节)。VPOS是HLSL中对屏幕坐标的语义,而WPOS是CG中对屏幕坐标的语义。两者在Unity Shader中是等价的。我们可以在HLSL/CG中通过语义的方式来定义顶点/片元着色器的默认输入,而不需要自己定义输入输出的数据结构
另一种方式是通过Unity提供的 ComputeScreenPos 函数。这个函数在UnityCG.cginc里被定义。通常的用法需要两个步骤,首先在顶点着色器中将ComputeScreenPos的结果保存在输出结构体中,然后在片元着色器中进行一个齐次除法运算后得到视口空间下的坐标