本篇讨论渲染过程中的几何数学问题。
参与计算的元素
坐标系:在Unity的Scene视图,可以看到这种Z轴朝里的坐标系。
矩阵,matrix:Mij表示一个有i排j列的矩阵。
在3D几何中,矩阵表示变换,将一个顶点或向量进行平移/旋转/缩放。
M=⎣⎡m11m21m31m12m22m32m13m23m33⎦⎤,取值时,M[0]表示第一排全部元素,M[0][1]表示第一排第2个元素。
三角形参数 π=180°;sin:对边/斜边;cos:临边/斜边;tan:对边/临边;cot:临边/对边。
矢量函数的几何意义
矢量 + 矢量 = 矢量

矢量 · 矢量 = 标量
点积,dot(A,B),可用于求夹角β、验证方向性,结果为∣A∣∗∣B∣∗cosβ。
dot((x,y,z),(a,b,c))=ax+by+cz
矢量 ✖ 矢量 = 矢量
叉积,cross(A,B),可用于求夹角β、验证三角面的朝向,结果长度为∣A∣∗∣B∣∗sinβ。
(x,y,z)×(a,b,c)=(yc−zb,za−xc,xb−ya)
平行的轴避免叉乘;交换叉积顺序导致结果反向。
矩阵 * 标量 = 矩阵
KM=Mk=k⎣⎡m11m21m31m12m22m32m13m23m33⎦⎤=⎣⎡km11km21km31km12km22km32km13km23km33⎦⎤
矩阵A * 矩阵B = 矩阵C
⎣⎡a11a21a31a43a12a22a32a42⎦⎤[b11b21b12b22b13b23b14b24]⎣⎡c11c21c31c41c12c22c32c42c13c23c33c43c14c24c34c44⎦⎤
c11=a11∗b11+a12∗b21
矩阵相乘,记作Mc=mul(Ma,Mb);
矩阵 * 矢量 = 矢量
矢量被转化为矩阵后可以与矩阵相乘,矢量在右边时作为列矩阵,矢量在左边时作为行矩阵。
矩阵与矢量的相乘可实现顶点位移、向量旋转、向量缩放。
顶点位移:
⎣⎡100001000010txtytz1⎦⎤⎣⎡xyz1⎦⎤⎣⎡x+txy+tyz+tz1⎦⎤
第四行是齐次坐标表示法,也可使用3X4矩阵运算。
绕X+轴顺时针旋转角度:
Rx(β)=⎣⎡10000cosβsinβ00−sinβcosβ00001⎦⎤
原空间的X轴(1, 0, 0),旋转后,依然是(1, 0, 0);
原空间的Y轴(0, 1, 0),旋转后是(0, cosβ, sinβ),相当于Y轴向Z轴旋转β角度;
原空间的Z轴(0, 0, 1),旋转后是(0, -sinβ, cosβ),相当于Z轴向-Y轴旋转β角度;
3X3旋转矩阵中的每一列,对应着旧空间的XYZ轴在新空间的矢量。
绕Y+轴顺时针旋转角度:
Ry(β)=⎣⎡cosβ0−sinβ00100sinβ0cosβ00001⎦⎤
绕z轴旋转向量:
Rz(β)=⎣⎡cosβsinβ00−sinβcosβ0000100001⎦⎤
缩放向量:
⎣⎡kx0000ky0000kz00000⎦⎤
复合变换:
Unity中约定变换的顺序为:缩放、旋转Z,旋转X,旋转Y、平移;
复合变换就相当于Ms∗Mz∗Mx∗My∗Mm∗A=B,左右顺序不能乱。
矩阵的构建之竖向填充矩阵:
将一个向量从空间A转换到空间B 使用了3x3的M矩阵
那么将空间A的X轴(1, 0, 0)转换到空间B 得到向量x
同理转换空间A的Y轴和Z轴到空间B 得到向量y和z
x等于M矩阵第一列 y等于M矩阵第二列 z等于M矩阵第三列
我们这样理解:B = mul(M, A)的计算过程中M是由x|y|z构成的竖向填充矩阵
x|y|z分别是空间A的X|Y|Z轴在空间B中的映射
如果用x|y|z进行横向填充构成矩阵N 能实现B = mul(A, N) N是M的转置矩阵
模型空间转换到切线空间:
已知模型空间下的t|b|n三个向量和V向量 需要将模型空间的V向量转换到切线空间
切线空间的X|Y|Z轴对应模型空间的t|b|n向量 转换矩阵为M
那么X = mul(M, t); Y = mul(M, b); Z = mul(m, n); 求M
可以看到直接求M太困难了,换一个思路求M的逆矩阵N。
因为模型空间到切线空间的矩阵M是一个正交矩阵 N等于M的转置矩阵
tbn刚好构成切线空间XYZ轴在模型空间的映射 N等于tbn构成的竖向填充矩阵
则M为tbn构成的横向填充矩阵
特殊矩阵
单位矩阵:记作E,斜对角均为1,任何矩阵和单位矩阵相乘的结果都是原来的矩阵。
⎣⎡100010001⎦⎤
转置矩阵:将原矩阵的行列对调后得到转置矩阵。
正交矩阵:正交矩阵和他的转置矩阵的乘积是单位矩阵。
逆矩阵:B=mul(M,A); A=mul(M−1,B);
正交矩阵的转置矩阵和逆矩阵是一样的,在shader计算中广泛用转置矩阵代替逆矩阵。
计算一次从模型空间到屏幕
推导一次像素在屏幕的位置,熟悉shader计算流程。
模型空间-世界空间
在建模软件设置Cube
这里我使用Blender中的默认Cube,给其中一个面绘制贴图来表示正面;
这个面的右上角就是我想要计算的顶点,这里记作点P。

模型导入Unity
将Cube模型导入Unity,模型导入设置中反选掉Convert Units,并拖入场景,Reset模型GameObject的Transform属性。
可以看到Cube模型的宽度为2个Unit。 通过观察得出点P在世界空间中的位置为(1, -1, 1)。
不同软件之间的坐标轴差异
点P在Blender中的位置是(-1, -1, 1),与Unity中相比,X轴值相反。
参考Blender模型导出设置中的Forward:-Z Forward Up:Y up。
旋转模型
为了让贴图了的那一面正着显示在屏幕上,将Rotation设置为(180, 0, 0);
计算P在世界空间中的新位置:cos180 = -1,sin180 = 0,得到点(1, 1, -1)。
⎣⎡10000cosβsinβ00−sinβcosβ00000⎦⎤⎣⎡1−111⎦⎤⎣⎡11−11⎦⎤
世界空间-观察空间
观察空间是从摄像机的观察角度去描述空间关系,和世界空间类似。
观察空间是一个未经缩放过的三维空间,对世界空间进行旋转、平移、反转Z即可。
新建的默认场景,相机的rotation为(0, 0, 0),position为(0, 1, -10)。
使用相机的Transform构建WorldToView矩阵,算出点P在观察空间位置为得到(1, 0, 9)。
⎣⎡1000010000100−1101⎦⎤⎣⎡11−11⎦⎤⎣⎡1091⎦⎤
相机的模型空间与观察空间Z轴相反:
⎣⎡1000010000−100000⎦⎤
P点其实没有动过,它在不同空间里面有不同的版本只是换了一个描述角度而已。
到这里,我们获得了在观察空间下P点的位置(1, 0, -9)。
观察空间-裁剪空间
裁剪空间进一步模拟视角。
视椎体:描述当前相机观察角度下,可被观察到的空间范围,类似于人眼。
本篇的主旨不在推导公式,这里直接抛出公式,主要讲相关应用。
Aspect:为屏幕的宽度/高度,近屏幕和原平面有相同的Aspect。
透视裁剪矩阵: 透视相机可以模拟出近大远小的效果,XYZ轴不等比例缩放。
Mfrustum=⎣⎡Aspectcot2FOV0000cot2FOV0000−Far−NearFar+Near−100−Far−Near2⋅Near⋅Far0⎦⎤
这个观察空间→裁剪空间的矩阵中对x、y、z分量进行的不同程度的缩放,z分量还做了平移。 这样的缩放的意义在于便于计算一个顶点是否在视椎体内。
左右分别表示观察空间和裁剪空间的视锥体。
除了XYZ轴缩放,原点位置从近平面外侧移2⋅Near到了视锥体内,8个角落点也均可描述。
在裁剪空间中,在视椎体内的顶点的各分量范围均有限制。
−w≤x≤w
−w≤y≤w
−w≤z≤w
当xyz不在范围内时表示这个点不在视锥体内。
正交相机
正交相机下物体不会因为远近变化而改变大小,2 * 相机Size = 远平面高度(Unit)。
Mortho=⎣⎡Aspect⋅Size10000Size10000−Far−Near2−100−Far+NearFar+Near0⎦⎤
应用P矩阵
在上一步中,我得到了点P在观察空间中的位置(1,0,-9),且设置屏幕比例为4:3。
相机是透视模式,Near=5,Far=10,角度为60度,cot30∘=3。
使用透视裁剪矩阵乘以(1, 0, -9, 1)后得到(1.299, 0, 7, 9),这个点在视椎体内。
透视裁剪空间-NDC
接下来需要对透视裁剪空间中的顶点P进行齐次去除,x、y、z分量都除以w分量。
透视裁剪空间此时变成了和正交裁剪空间一样的正方体。
点P的x、y、z分量除以W分量后得到新顶点(0.1443, 0, 0.7778),
新坐标系称为NDC(Normalized Device Coordinates),xyz分量的范围都是[-1, 1]。
NDC-屏幕空间
我们用的显示屏由大量的像素颗粒构成(LCD),这里设置屏幕为400X300像素。
以屏幕左下角为原点建立坐标轴,可以用宽高的百分比、像素为位置描述。
NDC坐标换算成百分比坐标:(x,y)=(0.5∗a+0.5,0.5∗b+0.5)
百分比坐标换算成像素坐标:(x,y)=(a∗400,b∗300)
将NDC坐标(0.1443, 0, 0.7778)带入后得到百分比坐标(0.57215,0.5),像素坐标(229,150):
使用QQ截图可测量像素宽度(由2个像素左右误差),得到计算结果与测量结果一致。


几何计算规律
正交矩阵:如果A⋅AT=E,E为单位矩阵,则A为正交矩阵。
例:B=5E;B⋅BT=E,正交矩阵的3个轴需要为单位向量,去除缩放和平移影响。
转置矩阵定义:将矩阵的横排与列排交换,总能转置。
(A⋅B)T=BT⋅AT
逆矩阵定义:能取消一个矩阵的相乘效果的矩阵,几何上总能逆。
M⋅M−1=M−1⋅M=E
(M−1)−1=M
逆转置矩阵:一个矩阵先求逆再转置,或者转置换再求逆,几何上总能逆转置。
(MT)−1=(M−1)T
(A⋅B)−1=B−1⋅A−1
(A⋅B⋅C⋅D)−1=D−1⋅C−1⋅B−1⋅A−1
转置矩阵的活用
当B是由矢量b转化为的矩阵时,可以横着被右乘或者竖着左乘矩阵。
因为逆矩阵是非常难求的,我们经常用正交矩阵的转置作为逆矩阵,可简化求逆过程。
A⋅b⟹A⋅B=(BT⋅AT)T=b⋅AT
AT⋅b⟹AT⋅B=(BT⋅A)T=b⋅A
等比例缩放物体求逆矩阵
使用K1⋅ UNITY_MATRIX_T_MV作为ObjectToWorld的逆矩阵,K为缩放系数。
转置矩阵矫正法线方向
#ifdef UNITY_ASSUME_UNIFORM_SCALING output.normalWS = mul((float3x3)GetObjectToWorldMatrix(), input.normalOS); #else output.normalWS = mul(input.normalOS, (float3x3)GetWorldToObjectMatrix()); #endif
|
我们对法线的定义是与表面垂直的向量,那么法线在世界空间与模型空间都应该垂直于表面。
当模型被非等比例缩放时,使用ObjectToWorld矩阵乘以法线得到的新矢量不再与表面垂直。
设ObjectToWorld矩阵为M,T为模型空间的切线向量,N为法线向量,期望的矩阵为G。
(M⋅T)⋅(G⋅N)=0
转换到世界空间后的切线和法线也应该垂直,也就是2个向量的点积为0。
如果把切线当成横向矩阵,法线当竖向矩阵,两个矩阵相乘模仿点积,结果也应为0。
(M⋅T)T⋅(G⋅N)=0
TT⋅MT⋅G⋅N=0
由于TT⋅N=0,如果MT⋅G=E上式即可成立。
即G=(MT)−1=(M−1)T,活用转置后:G⋅N=NT⋅M−1。
这里的M−1也就是求ObjectToWorld矩阵的逆矩阵,WorldToObject矩阵。
如果物体是等比例缩放,就可以简化求逆过程;对于非统一缩放物体就必须计算逆矩阵了。
线性变换:可以保留矢量加和标量乘的变换,使用3X3矩阵。
f(x)+f(y)=f(x+y)
k⋅f(x)=f(k⋅x)
其中f(x)表示一个矩阵变换过程。 变换后相加=先相加后变换,先标量乘再变换=先变换再标量乘。
符合线性变换规律的有:缩放、旋转、错切、镜像、正交投影。
错切(shear):比如移动正方形的一边使其变成一个平行四边形。
正交投影:观察空间-正交裁剪空间,去除位移部分,相当于把一个长方形变成正方形。