Unity渲染教程(四):第一个光源
译者:张乾光(星际迷航)审校:崔国军(飞扬971)
?将法线从物体空间转换到世界空间。
?使用方向光。
?计算漫反射和镜面高光反射。
?实现能量守恒。
?使用金属的工作流程。
?利用Unity的基于物理规则渲染的算法。
这是关于渲染基础的系列教程的第四部分。前面的教程介绍了混合使用多张纹理。这一次,我们将看看如何计算光照。
这个系列教程是使用Unity 5.4.0开发的,这个版本目前还是开放测试版本。我使用的是5.4.0b17版本。
这个教程使用的着色器与前面的教程不匹配?
为了提高兼容性,我已经改变了以前的教程中的着色器。我还在这个系列的第二部分中介绍了着色器的结构,而不是推迟到这个教程才介绍。
现在是时候让光源照射到事物上了。
法线
我们可以看到东西,这是因为我们的眼睛可以检测到电磁辐射。传递电磁相互作用的基本粒子称为光子我们只可以看到电磁光谱的一部分,这是我们所知的可见光。电磁光谱的其余部分我们是看不见的。
什么是整个电磁频谱?
光谱被分成光谱带。按照从低频到高频的顺序,这些被称为无线电波、微波、红外线、可见光、紫外线、X射线和伽马射线。
光源能够发射光。一些光会击中物体。击中物体的光中的一部分会被物体反射。如果会被物体反射的光最终进入到我们的眼睛 - 或是相机镜头 -然后我们就看到这个物体了。
要做到这一点,我们必须知道我们物体的表面信息。我们已经知道物体的表面的位置,但不知道物体的表面的方向。为了知道我们物体的表面信息,我们需要物体的表面法线向量。
使用网格的法线
复制我们的第一个着色器,并使用复制的第一个着色器的代码来作为我们的第一个光照着色器。使用光照着色器来创建材质,并将其分配给场景中的某些立方体和球体。给对象不同的旋转和尺度,其中一些并不均匀,这样就得到了一个变化的场景。
1 2 3 4 5
Shader "Custom/My First Lighting Shader" {
…
}
场景中的一些立方体和球体。
Unity的立方体和球面网格包含了顶点法线。我们可以得到这些法线信息并将它们直接传递给片段着色器。
1 s truct VertexData {
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
float4 position : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct Interpolators {
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
};
Interpolators MyVertexProgram (VertexData v) {
Interpolators i;
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
i.position = mul(UNITY_MATRIX_MVP, v.position);
i.normal = v.normal;
return i;
}
现在我们可以在我们的着色器中对法线进行可视化。
1 2 3 4 5 float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
return float4(i.normal * 0.5 + 0.5, 1);
}
把法线向量作为颜色表示出来。
这些数据是原始法线数据,直接从网格中得到的。立方体的表面看起来很平,这个因为立方体的每个面是具有四个顶点的独立四边形。这些顶点的法线都指向相同的方向。相反,球体的顶点的法线都指向不同的方向,这导致了平滑的插值。
动态批次合并
立方体法线发生了一些奇怪的事情。我们期望每个立方体会显示相同的颜色,但事实却不是这样。立方体的法线其实是可以改变颜色的,这取决于我们如何看这些立方体。
有颜色变化的立方体。
这个问题是由动态批次合并引起的。 Unity动态地将小的网格合并在一起,以减少绘制调用。球体的网格对动态批次合并而言太大了,因此球体的网格不受影响。但是动态批次合并会对立方体有影响。
要合并网格的话,这些网格必须从它们的本地空间转换到世界空间。是否以及如何对对象进行批次合并取决于其他因素,比如说如何对这些对象进行排序以便进行渲染。因为这种转换也会影响法线,所以这就是为什么我们会看到颜色的变化。如果需要的话,你可以通过播放器的设置来关闭动态批次合并。
批次合并的设置。
除了动态批次合并以外,Unity还可以进行静态批次合并。这涉及对静态几何体的处理,所以静态批次合并和动态批次合并的工作原理不同,但也涉及到世界空间的转换。行静态批次合并发生在构建的时候。
在没有动态批次合并时候的法线数据。
虽然你需要对动态批次合并有了解,但是这其实没有什么可担心的。事实上,我们必须对我们的法线做同样的事情。所以你可以启用动态批次合并。
世界坐标空间中的法线
除了被动态批次合并的对象以外,我们所有的法线都在物体空间之中。但是我们必须知道世界坐标空间中的表面的方向。因此,我们必须将法线从物体空间转换到世界坐标空间。为了做到这一点,我们需要物体的转换矩阵信息。
Unity 将一个物体的整个变换层次结构折叠成一个单一的变换矩阵,就像我们在第一部分中所做的那样。我们可以将它写为O = T1T2T3 ...其中T 是单独的变换矩阵,而O 是组合变换矩阵。这个矩阵被称为物体空间到世界空间的变换矩阵。 Unity 通过类型为float4x4的unity_ObjectToWorld 变量使这个矩阵在着色器中可用,该变量在UnityShaderVariables 中进行定义。将这个矩阵乘以顶点着色器中的法线数据,以便将数据转换到世界坐标空间。因为它是一个方向,重新定位应该被忽略。所以齐次坐标的第四个分量必须为零。 1 2 3 4 5 6 7 8 9 10 11 12 13 Interpolators MyVertexProgram (VertexData v) {
Interpolators i;
i.position = mul(UNITY_MATRIX_MVP, v.position);
i.normal = mul(unity_ObjectToWorld, float4(v.normal, 0));
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
或者,我们可以只对矩阵的3×3的部分做乘法运算。编译出来的代码最终是一样的,因为编译器会去掉所有与常数零相乘的东西。 1 i
.normal = mul((float3x3)unity_ObjectToWorld, v.normal);
从物体空间变换到世界坐标空间。
法线现在处于世界坐标空间,但有些发现看起来比别的发现更亮。这是因为他们也进行了缩放。因此,我们必须在转换后对法线进行归一化。
1 2 3 i.normal = mul(unity_ObjectToWorld, float4(v.normal, 0));
i.normal = normalize(i.normal);
归一化后的法线。
虽然我们再次对向量进行了归一化,但对于没有均匀大小的对象来说,它们看起来很奇怪。这是因为当表面在一个维度上进行拉伸的时候,这个表面的法线不会以相同的方式进行拉伸。
在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
的转置被写为。通过翻转矩阵的主对角线上的变量来对矩阵进行转置。因此它的行会成为转置矩阵的列,它的列会成为转置矩阵的行。需要注意的是,这意味着对角线上的变量本身保持不变。
像逆矩阵一样,对矩阵乘法进行转置会反转其顺序。。 当对不是方阵的矩阵使用的时候,这是有意义的,否则可能会导致无效的乘法。 但是一般来说,这个等式是成立的,你可以查找下它的证明。