[toc]本篇只讨论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),可用于求夹角β、验证方向性,结果为abcosβ。 矢量 ✖ 矢量 = 矢量 叉积,cross(a,b),可用于求夹角β、验证三角面的朝向,结果长度为absinβ。 (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)。 附:正交相机的P矩阵

透视裁剪空间-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):比如移动正方形的一边使其变成一个平行四边形。 正交投影:观察空间-正交裁剪空间,去除位移部分。相当于把一个长方形变成正方形。