UnityObjectToWorldDir用于把模型空间下的矢量转换到世界空间
UnityObjectToWorldNormal用于把模型空间下的法线向量转换到世界空间。因为必须保证法线垂直于模型的表面,所以缩放的时候与普通矢量不一样。
如果法线用UnityObjectToWorldDir,则会出现以下错误
而用UnityObjectToWorldNormal,则可得到正确的结果
世界坐标空间中的法线
除了被动态批次合并的对象以外,我们所有的法线都在物体空间之中。但是我们必须知道世界坐标空间中的表面的方向。因此,我们必须将法线从物体空间转换到世界坐标空间。为了做到这一点,我们需要物体的转换矩阵信息。
Unity将一个物体的整个变换层次结构折叠成一个单一的变换矩阵,就像我们在第一部分中所做的那样。我们可以将它写为O = T1T2T3 ...其中T是单独的变换矩阵,而O是组合变换矩阵。这个矩阵被称为物体空间到世界空间的变换矩阵。
Unity通过类型为float4x4的unity_ObjectToWorld变量使这个矩阵在着色器中可用,该变量在UnityShaderVariables中进行定义。将这个矩阵乘以顶点着色器中的法线数据,以便将数据转换到世界坐标空间。因为它是一个方向,重新定位应该被忽略。所以齐次坐标的第四个分量必须为零。
或者,我们可以只对矩阵的3×3的部分做乘法运算。编译出来的代码最终是一样的,因为编译器会去掉所有与常数零相乘的东西。
从物体空间变换到世界坐标空间。
法线现在处于世界坐标空间,但有些发现看起来比别的发现更亮。这是因为他们也进行了缩放。因此,我们必须在转换后对法线进行归一化。
归一化后的法线。
虽然我们再次对向量进行了归一化,但对于没有均匀大小的对象来说,它们看起来很奇怪。这是因为当表面在一个维度上进行拉伸的时候,这个表面的法线不会以相同的方式进行拉伸。
在X轴进行缩放,顶点和法线都变为½。
当大小不均匀的时候时,应该对法线进行取逆操作。这样,当它们被再次被归一化后,法线将匹配变形的曲面的形状。而这对于均匀尺度来说没有影响。
在X轴进行缩放,顶点变为½,而法线加倍。
所以我们必须对大小进行取逆操作,但旋转应该保持不变。那么我们应该怎么做?
我们将对象的变换矩阵描述为O = T1T2T3 ...但我们可以更加具体一些。我们知道层次结构中的每个步骤都结合了缩放、旋转和位移。因此每个T可以分解为SRP。
这意味着 O=S1R1P1S2R2P2S3R3P3…,但是为了方便起见,让我们假设说 O=S1R1P1S2R2P2 。
因为法线是方向向量,所以我们不关心重新定位的问题。所以我们可以进一步简化到O=S1R1S2R2,而且我们只需要考虑3×3的矩阵。
我们想要对缩放取逆,但同时保持旋转不变。所以我们想要一个新的矩阵N = S-11R1S-12R2。
如何对矩阵取逆?
矩阵M的逆写作。 它也是一个矩阵,当它们相乘的时候,将抵消另外一个矩阵带来的操作。互相是对方矩阵的逆。所以。
要抵消一系列步骤带来的影响,必须以相反的顺序执行相反的步骤。这方面的助记符涉及一些规则。这意味着。
对于单个数x的情况,它的逆更加简单的 ,这是因为 。这也表明零没有逆元。也不是每个矩阵都具有相应的逆矩阵。
我们正在使用缩放、旋转和重新定位矩阵。只要我们不把矩阵缩放为零,所有这些矩阵可以取逆。
位移矩阵的逆矩阵是通过简单地对其第四列中的XYZ分量取负来得到的。
缩放矩阵的逆矩阵是通过对它的对角线上的分量取倒数得到的,我们只需要考虑3×3的矩阵。
旋转矩阵可以每次针对一个轴进行考虑,例如考虑围绕Z轴的情况。 旋转z弧度的操作可以通过简单旋转-z弧度的操作来抵消。当你研究正弦和余弦波的时候,你会注意到sin(-z)= - sinz和cos(-z)= cosz。 这使得旋转矩阵的逆矩阵非常简单。
需要注意的是,旋转矩阵的的逆矩阵在其主对角线上的分量与原始矩阵相同。只有正弦分量的正负发生了变化。
除了物体空间到世界空间的变换矩阵意外,Unity还提供了一个世界空间到物体空间的变换矩阵。这些矩阵实际上是彼此的逆矩阵。所以我们得到这么一个公式。
这给出了我们需要的缩放矩阵的逆矩阵,但也给了我们旋转矩阵和位移矩阵的逆矩阵。幸运的是,我们可以通过转置矩阵来移除那些我们不需要的效果。 然后我们得到。
什么是矩阵的转置?
矩阵M的转置被写为。通过翻转矩阵的主对角线上的变量来对矩阵进行转置。因此它的行会成为转置矩阵的列,它的列会成为转置矩阵的行。需要注意的是,这意味着对角线上的变量本身保持不变。
像逆矩阵一样,对矩阵乘法进行转置会反转其顺序。。 当对不是方阵的矩阵使用的时候,这是有意义的,否则可能会导致无效的乘法。 但是一般来说,这个等式是成立的,你可以查找下它的证明。
所以,让我们转置世界空间到物体空间的矩阵,并乘以顶点的法线数据。
正确的世界坐标空间的法线。
实际上,UnityCG包含一个方便的UnityObjectToWorldNormal函数,正是做这个工作。所以我们可以使用那个函数。它也使用显式的矩阵乘法,而不是使用矩阵转置。这应该会生成更好的编译代码。
UnityObjectToWorldNormal看起来是什么样子?
这里就是UnityObjectToWorldNormal的代码了。 inline关键字不起任何作用。
重新归一化
在顶点程序中产生正确的法线之后,正确的法线值会通过内插值器。不幸的是,在不同单位长度的向量之间进行线性内插不会生成另外一个单位长度的向量。它会比单位长度的向量要小一些。
所以我们必须在片段着色器中再次对法线进行归一化。
对法线重新进行归一化。
虽然对法线重新进行归一化可以产生更好的结果,但这两者之间的误差通常非常小。如果你更重视性能的话,你可以决定不在片段着色器里面再次进行归一化。这是移动设备中常见的优化。
这是比较夸张的错误。