本篇只讨论ShaderLab相关的数学问题。
如需了解渲染流水线的配置部分,可参见ShaderLab笔记
如需了解ShaderLab函数部分,可参见ShaderLab函数

参考:建模软件中,如何确定mesh自带属性。

参与计算的元素


坐标系:在Unity的Scene视图,可以看到这种Z轴朝里的坐标系。
右手参考手势:右手大拇指朝向Y轴,向内旋转,模拟从X轴移动到Z轴的过程。

矩阵(matrix):表示一个有i排j列的矩阵。
在ShaderLab函数中,矩阵表示变换,将一个顶点或向量进行平移/旋转/缩放。

shader中取矩阵值时,M[0]表示第一排全部元素,M[0][1]表示第一排第2个元素。

三角形参数
π=180°;sin:对边/斜边;cos:临边/斜边;tan:对边/临边;cot:临边/对边;

矢量函数的几何意义

矢量 + 矢量 = 矢量

矢量 · 矢量 = 标量

点积,dot(a,b),可用于求夹角β、验证方向性,结果为|a||b|cosβ。

矢量 ✖ 矢量 = 矢量

叉积,cross(a,b),可用于求夹角β、验证三角面的朝向,结果长度为|a||b|sinβ。
(x,y,z)✖(a,b,c)=(yc − zb,za − xc,xb − ya)
(1,0,0)✖(0,1,0)=(0x0-0x1,0x0-1x0,1x1-0x0)=(0,0,1) : X轴✖Y轴=Z轴
(0,1,0)✖(0,0,1)=(1x1-0x0,0x0-0x1,0x0-1x0)=(1,0,0) : Y轴✖Z轴=X轴
(0,1,0)✖(0,1,0)=(1x0-0x1,0x0-0x0,0x1-1x0)=(0,0,0) : 平行的轴避免叉乘
(4,0,0)✖(4.3.0)=(0x0-0x3,0x4-4x0,4x3-0x4)=(0,0,12)
(4,3,0)✖(4.0.0)=(3x0-0x0,0x4-4x0,4x0-3x4)=(0,0,-12) :交换叉积顺序导致结果反向

矩阵 * 标量 = 矩阵

矩阵A * 矩阵B = 矩阵C

矩阵相乘,可用于计算形变、位移,mul(A, B);结果的行数来自于A,列数来自于B。
矩阵C中第i行第j列的元素,等于矩阵A的所有i行的元素分别于矩阵B的所有j列的对应元素相乘后求和。

矩阵 * 矢量 = 矢量
矢量(一维数组)被转化为矩阵后参与矩阵相乘,矢量在右边时作为列矩阵,矢量在左边时作为行矩阵。
矩阵与矢量的相乘可以表示顶点位移、向量旋转、向量缩放。

顶点位移:

绕x轴旋转向量:

原空间的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旋转矩阵中的每一列,对应着新空间中对旧空间的轴的描述,也就是竖向填充矩阵
剩下的几个向量旋转、缩放的例子都类似,在后面的规律总结中会再次讲到。

绕y轴旋转向量:

绕z轴旋转向量:

缩放向量:

复合变换
Unity中约定变换的顺序为:缩放、旋转Z,旋转X,旋转Y、平移;
复合变换就相当于Ms * Mz * Mx * My * Mm * 向量 = 向量,这些mul计算的左右顺序不能乱。

Transform组件
transform.Positon=new Vect3(3,3,3)
transform.EulerAngles=new Vect3(90,90,90)
transform.Scale=new Vect3(2,3,4)
这些属性都是从世界空间来描述transform本地空间的,可以活用竖向填充矩阵来构建矩阵。

顶点空间转换
设子坐标空间的XYZ轴在父坐标空间下用3个向量表示x,y,z;
子坐标空间的原点在父坐标空间的坐标H(a,b,c)。
现给定子空间坐标中的顶点K(d,e,f),求其在父坐标空间下的位置。

相当于将H分别沿着x、y、z方向移动d、e、f长度:(a,b,c)+xd+ye+zf,这是4个向量相加。
(a,b,c)+()d+()e+()f
将向量相加表示为顶点:
(a,b,c,1)+

现在我们得到了子坐标系中的顶点K在父坐标系中的位置。

竖向填充矩阵
将上面的公式转化为一个矩阵相乘:

这个矩阵中的每列分别由x,y,z,H的分量填充,本篇中称之为竖向填充矩阵
竖向填充矩阵中每列存储的是目标空间对原空间的XYZ轴和原点的描述;
竖向填充矩阵可以将顶点从原空间转换到目标空间。

特殊矩阵
单位矩阵(E):矩阵的默认值,斜对角均为1,任何矩阵和单位矩阵相乘的结果都还是原来的矩阵。
转置矩阵:将原矩阵的行列对调后得到转置矩阵。
正交矩阵:正交矩阵和他的转置矩阵的乘积是单位矩阵,通常等比例缩放矩阵都是正交的。
逆矩阵:X * A = B,设Y为X的逆矩阵,则:A = B * Y,X * Y = E。
正交矩阵的转置矩阵和逆矩阵是一样的,在shader计算中广泛用转置矩阵代替逆矩阵。

可逆性
当我们要把一个世界空间下的向量转化到模型空间,已知模型空间在世界空间下的x轴,y轴,z轴和原点位置(a,b,c);
也就是求竖向填充矩阵的逆矩阵,我们可以考虑将缩放、旋转、平移操作逆着执行一遍,这样步骤会很多。
如果物体是等比例缩放的,就可以直接使用转置矩阵作为逆矩阵了,新矩阵中每一行4个元素包含轴的缩放和位移:
新矩阵的3X3版本,可用于计算矢量的空间转换的逆操作。

计算一次从模型空间到屏幕

推导一次像素在屏幕的位置,熟悉shader计算流程。

模型空间-世界空间

单位长度
在Unity里面不能直接看到模型空间,也无法直接得知指定顶点在模型空间下的具体坐标。
模型空间可以在3D模型编辑软件内查看,在编辑模式下选择顶点,就可以查看顶点位置。
MMD:切换到顶点列表页面,编辑框中选中顶点后,顶点列表中自动高亮被选中的顶点。
Blender:没有发现直接面板查看顶点,但是有一个叫做Python Console的界面可以自己做脚本输出日志信息。
通用方法:模型空间都有网状的参考线,通过数格子的方法可以估计出顶点坐标。
默认模型空间中地上一格的宽度,和Unity中1个单位的距离是一样的。
在模型导入面板和模型的Transform面板都可以设置缩放比例。
通常在3D建模时,建模师可能会用一个默认的Cube为基础形体创建头部或者身体,人物躯干的宽度一般在2-4个单位。

从建模软件导入模型
这里我使用Blender中的默认Cube,并给其中一个面绘制贴图来表示正面,这个面的右上角P就是我想要计算的顶点。

我现在将这个Cube导入到Unity中。模型导入设置中反选掉Convert Units,拖入场景,Reset模型GameObject的Transform属性。

这个模型我没有命名,所以就叫做untitled,可以看到模型的宽度就相当于Unity中的两格。

通过观察,P在世界空间中的位置为(1,-1,1),P在模型空间位置是(-1,-1,1),与建模软件轴向有关。
为了让贴图了的那一面正着显示在屏幕上,我将Rotation设置为(180,0,0);
计算P在世界空间中的新位置:cos180=-1,sin180=0,得到点(1,1,-1)。

世界空间-观察空间

观察空间是从摄像机的观察角度去描述空间关系,Unity中观察空间使用右手坐标系
观察空间是一个未经缩放过的三维空间,对摄像机进行旋转、平移可以调整观察空间位置。
我们可以从相机组件的Transform获取相机的旋转和平移,其缩放属性不会生效。
因为相机的Transform是在世界空间角度去描述的,要转换到观察空间需要使用
这里,我的场景是新建的默认场景,相机的rotation为(0,0,0),position为(0,1,-10)。
那么用相机的模型空间去描述世界空间的Transform,其rotation为(0,0,0),positon(0,-1,10)
使用(0,-1,10)去构建平移矩阵即可。

在上一步中,在世界空间中,点P的位置为(1,1,-1),计算其在相机的模型空间中的位置,得到(1,0,9):

相机的模型空间与观察空间的差别是Z轴相反,可与Z分量取反矩阵相乘。
Z分量取反矩阵如下:

P点其实没有动过,它在不同空间里面有不同的版本只是换了一个描述角度而已。
到这里,我们获得了在观察空间下P点的位置(1,0,-9)。

观察空间-裁剪空间

观察空间换了个角度去描述世界空间,裁剪空间则进一步模拟我们的视角。
视椎体:描述当前相机观察角度下,可被观察到的空间范围。
本篇的意义不在推导公式,而是去描述裁剪过程的几何意义,应注意在不同空间下对视椎体的描述。
Aspect为屏幕的宽度/高度,由Game视图的横纵比和像机的W和H属性共同决定。
Aspect = nearClipPlaneWidth/nearClipPlaneHeight = farClipPlaneWidth/farClipPlaneHeight

透视相机的裁剪空间:
透视相机可以模拟出近大远小的效果,物体与相机的距离不同占视口的比例不同。
从观察空间的角度去看视椎体的范围,就像俯瞰金字塔的顶端,但是在裁剪空间内去看视椎体是立方体。
从裁剪空间的角度去描述观察空间是非常困难的,因为其不是等比例缩放的,也就是透视裁剪矩阵

这个观察空间→裁剪空间的矩阵中对x、y、z分量进行的不同程度的缩放,z分量还做了平移。
这样的缩放的意义在于便于计算一个顶点是否在视椎体内。

左边是从观察空间去看视椎体,并标注了其中4个点的位置。
右边可以说是从观察空间或者裁剪空间去看视椎体,标注了其在裁剪空间中的位置。
结论:
在观察空间中,能被看到的顶点其Z分量永远小于0,对应裁剪空间中的w分量永远大于0。
视椎体在被从观察空间转化到裁剪空间后依然是金字塔状,原点朝视椎体方向移动了一段距离。
观察空间中的点(0,0,-2·Near·Far/(Far+Near))转换到裁剪空间后坐标为(0,0,0),这个偏移量在裁剪空中相当于2·Near的距离,两个空间的距离不等价。
在裁剪空间中,原点离近视口距离为Near,离远视口距离为Far。
在裁剪空间中,在视椎体内的顶点x、y、z、w分量均有范围限制,随着z值的变化,要满足顶点保持在视椎体内,其x、y值的范围也在发生变化,这种变化的曲线是线性的。

设一个点距离远视口a,求这个点的切面中在视椎体内的顶点的x分量的最大值b。
b=Far-a·(Far-Near)/(Far+Near)
其中a的最小值为0,最大值为(Far+Near)。
因为这个范围限制变化是线性的,我们有机会通过在空间转换时保存一个数值作为范围的临界点,观察空间中顶点的Z轴值的取反刚好能满足需求,以上解释了为什么要使用裁剪矩阵,以及裁剪空间中w分量为何不等于1
如果一个顶点在视椎体内,那么它变换到裁剪空间后的坐标必须满足:
−w ≤ x ≤ w; −w ≤ y ≤ w; −w ≤ z ≤ w;

正交相机的裁剪空间:
正交相机看物体不会因为物体的远近变化而改变大小,其视椎体是长方形,Near=Far。
相机的Size属性X2 = 视椎体的高度
从观察空间到正交相机的裁剪空间变换矩阵,也就是正交裁剪矩阵

这个观察空间→裁剪空间的矩阵中对x、y、z分量进行的不同程度的缩放,z分量还做了平移。
这样的缩放的意义在于使视椎体从长方体变成了正方体,便于计算一个顶点是否在视椎体内。

在上一步中,我得到了点P在观察空间中的位置(1,0,-9)。
Game窗口中,我设置屏幕比例为4:3。
相机是透视模式,Near=5,Far=10,角度为60度。cot30=√3,√3=1.732。
使用透视裁剪矩阵乘以(1,0,-9,1)后得到(3·√3/4,0,7,9),在视椎体内。
现在我得到了点P在透视裁剪空间下的坐标(1.299,0,7,9)。

透视裁剪空间-NDC

现在我们已经将顶点P从观察空间变换到了透视裁剪空间。
接下来需要对透视裁剪空间中的顶点P进行齐次去除,x、y、z分量都除以w分量。
相当于透视裁剪空间下金字塔形状的视椎体变成了宽度为2的正方体,和正交裁剪空间中的视椎体一样了。
点P的x、y、z分量除以W分量后得到新顶点(0.1443,0,0.7778),四舍五入到小数点后第四位。
正交裁剪空间中顶点的w分量固定为1,没有必要做齐次去除。
这样得到的坐标称为归一化的设备坐标(NDC,Normalized Device Coordinates)。

NDC-屏幕空间

齐次去除后的视椎体中的顶点,x、y、z分量的范围都是[-1,1],z分量会被用于记录点的深度。
所有在视椎体范围内的顶点会铺满屏幕,左下角为(-1,-1),右上角为(1,1),点P在屏幕中央稍微靠右的位置。
视口空间
一个过渡概念,用于描述点在屏幕上相对位置的一种描述方法,左下角为(0,0),右上角为(1,1)。
X轴比例的为(x+1)/2,Y轴的比例为(y+1)/2,得到点P在视口空间中的位置为(0.57215,0.5)。
如果屏幕有400X300像素,那么点P在的屏幕空间中的位置为(229,150)。

使用QQ截图可以观察图片的大概尺寸,会有2像素左右的误差。
我这里截图后在PS中去除了多余的部分,得到了一张229X150像素的图片,说明计算结果与显示结果一致。

几何计算规律

转置矩阵定义:将矩阵的横排与列排交换,总能转置。

当B是由矢量b转化为的矩阵时,因为结果矩阵转置后再转换为矢量是等价的:


逆矩阵定义:能取消一个矩阵的相乘效果的矩阵,几何上总能逆。


逆转置矩阵:一个矩阵先求逆再转置,或者转置换再求逆,几何上总能逆转置。



正交矩阵:正交矩阵与其置换矩阵相乘=单位矩阵,一个矩阵不一定是正交矩阵。


正交矩阵条件:构成矩阵的三个轴是互相垂直的单位向量,也就是标准正交基

阅读顺序的问题
矢量与向量相乘时,默认从右往左阅读,矢量进行列排序且保持在公式的最右侧。

当矢量使用横排序时,阅读顺序变成从左到右,且参与计算的矩阵全部转置。

这两个公式里面,v和A其实都发生了转置,其结果也发生了转置,相当于转置矩阵的第一个特性。
两个向量AB的点积=A的横排序矩阵乘以B的竖排列矩阵

转置矩阵的使用
通常判断矩阵是否是正交矩阵,然后用转置矩阵代替逆矩阵。
正交矩阵的三个轴是互相垂直的单位向量,有缩放+旋转+位移的矩阵需要去除位移→归一化三个轴。
在矩阵是正交矩阵时,可以使用其置换矩阵代替逆矩阵,如:1/k·UNITY_MATRIX_T_MV,k为缩放系数。

逆转置矩阵的使用
法线方向
当模型应用矩阵M被非等比例缩放时,用矩阵M乘以法线得到的新矢量和转换后的切线之间不再垂直。
使用M的逆转置矩阵来变换法线可以得到正确的法线方向,求证方法:

其中第一个箭头指的是将向量点积转化为两个矩阵相乘,G为M的逆转置矩阵时可正确转换法线。
但是逆转置矩阵只有当矩阵是正交矩阵时才可以轻松得到结果,非正交矩阵需要进行归一化。
使用Unity提供的逆转置参数如:UNITY_MATRIX_IT_MV。
注:相比转置矩阵,让Unity去计算逆矩阵可能非常的消耗性能。

线性变换:可以保留矢量加和标量乘的变换,使用3X3矩阵。

其中f()表示一个变换处理。
矢量x和矢量y先变换后相加=先相加后再变换,标量x先变换再缩放=先缩放再变换。
符合线性变换规律的有:缩放、旋转、错切、镜像、正交投影。
错切(shear):比如移动正方形的一边使其变成一个平行四边形。
正交投影:观察空间-正交裁剪空间,去除位移部分。相当于把一个长方形变成正方形。


关注成长,注重因果。