当然转置两次会让你得到最初的结果。所以 。
所以,让我们转置世界空间到物体空间的矩阵,并乘以顶点的法线数据。 1 2 3 4 5 6 7 8 9 i.normal = mul(
transpose((float3x3)unity_WorldToObject),
v.normal
);
i.normal = normalize(i.normal);
正确的世界坐标空间的法线。
实际上,UnityCG 包含一个方便的UnityObjectToWorldNormal 函数,正是做这个工作。所以我们可以使用那个函数。它也使用显式的矩阵乘法,而不是使用矩阵转置。这应该会生成更好的编译代码。
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 = UnityObjectToWorldNormal(v.normal);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
UnityObjectToWorldNormal 看起来是什么样子?
这里就是UnityObjectToWorldNormal 的代码了。 inline 关键字不起任何作用。
1 \\\ 将法线从物体空间变换到世界坐标空间。
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 inline float3 UnityObjectToWorldNormal( in float3 norm ) {
\\\乘以转置逆矩阵,
\\\ 实际上使用transpose ()来生成高度优化的代码。
return normalize(
unity_WorldToObject[0].xyz * norm.x +
unity_WorldToObject[1].xyz * norm.y +
unity_WorldToObject[2].xyz * norm.z
);
}
重新归一化
在顶点程序中产生正确的法线之后,正确的法线值会通过内插值器。不幸的是,在不同单位长度的向量之间进行线性内插不会生成另外一个单位长度的向量。它会比单位长度的向量要小一些。
所以我们必须在片段着色器中再次对法线进行归一化。
1 2 3 4 5 6 7 float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
return float4(i.normal * 0.5 + 0.5, 1);
}
对法线重新进行归一化。
虽然对法线重新进行归一化可以产生更好的结果,但这两者之间的误差通常非常小。如果你更重视性能的话,你可以决定不在片段着色器里面再次进行归一化。这是移动设备中常见的优化。
这是比较夸张的错误。
漫反射的渲染
我们如果看到的物体它本身不是光源的话,那么就是因为它们反射光我们才能看到这个物体。可能有不同的方式来发生反射。让我们先考虑漫反射
发生漫反射是因为光线不仅仅从物体表面发生反射。相反,光会穿透表面,反弹一会儿,然后分裂几次,直到它再次离开物体的表面。在现实中,光子和原子之间的相互作用比这更复杂,但我们不需要知道真实世界的物理那么多的细节。 多少光会在物体表面上进行漫反射取决于光线射到物体表面的角度。当光线射到物体表面的角度是0°角,也就是正面碰撞的时候,大多数的光会被反射。 随着光线射到物体表面的角度的增加,
光的漫反射将减小。光线射到物体表面的角度到达90°的时候,就没有光照射到物体的表面,所以物体的表面会保持黑暗。漫反射光的量与光的入射方向和表面法线之间的角度的余弦成正比。这被称为兰伯特余弦定律。
漫反射。
我们可以通过计算表面法线向量和光的入射方向的点积来确定这个兰伯特反射系数。我们已经知道了表面法线向量,但还不知道是光的方向。让我们从一个固定的光线方向开始,从垂直上方入射开始。
1 2 3 float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
4
5 6 7
return dot(float3(0, 1, 0), i.normal);
}
从上面照亮的效果,在伽马空间和线性空间的对比结果。
什么是点积?
两个向量之间的点积在几何上定义为A·B = || A || || B || cosθ。这意味着两个向量之间的点积是矢量之间的角度的余弦乘以它们的长度。因此,在两个单位向量的情况下,A·B =cosθ。
代数上,它被定义为
。
这意味着你可以通过乘以所有分量对并对它们进行求和来计算它。
float dotProduct = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
在视觉上,这个操作将一个向量直接向下投射到另一个向量之上。好像在上面做了一个投影。通过这样做,你会得到一个直角三角形,其底边的长度是点积的结果。如果两个向量都是单位长度向量的话,那么结果就是它们的角度的余弦。
点积。
受钳制的光照
计算点积的工作原理是当表面的法线向量指向光的入射方向的时候,而不是表面的法线向量指向远离光的入射方向的时候。在这种情况下,表面将在逻辑上处于其自身的阴影中,并且它应该根本不接收光。由于光的入射方向和表面法线之间的角度在这一点上必须大于90°,所以其余弦和点积变为负。由于我们不想要负光,我们必须钳制结果。我们可以使用标准的最大函数来做这个事情。
1 r eturn max(0, dot(float3(0, 1, 0), i.normal));
除了max 函数以外,你会经常看到着色器使用saturate 函数进行代替。这个标准函数在把结果限制在0和1之间。 1 r eturn saturate(dot(float3(0, 1, 0), i.normal));
这似乎是不必要的,因为我们知道我们的点积将永远不会产生大于1的结果。但是,在某些情况下,它实际上可以更高效,这取决于硬件的实现。但是我们不应该担心这种比较小的优化。事实上,我们可以将这个事情委托给Unity 的开发人员。
UnityStandardBRDF 导入文件定义了方便的DotClaped 函数。这个函数会执行一个点积,并确保点积的结果永远不为负。这正是我们需要的。它还包含许多其他光照功能,并会导入其他有用的文件,我们以后会需要这些文件。所以,让我们使用这个导入文件! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include "UnityCG.cginc"
#include "UnityStandardBRDF.cginc"
…
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
return DotClamped(float3(0, 1, 0), i.normal);
}
DotClaped 看起来是什么样子?
下面就是它的具体代码。显然,他们决定在面向低性能着色器硬件的时候以及在面向PS3的时候使用saturate 函数。 1 2 3 4 5 6 7 8 9 10 11 12 13 inline half DotClamped (half3 a, half3 b) {
#if (SHADER_TARGET < 30 || defined(SHADER_API_PS3))
return saturate(dot(a, b));
#else
return max(0.0h, dot(a, b));
#endif
}
这个着色器使用半精度的数字,但是你不需要担心数字的精度。它只对移动设备有所帮助。
因为UnityStandardBRDF 已经导入了UnityCG 和一些其他文件,我们不必显式导入它。这样做是没有错的,但我们也可以保持简短。
// #include "UnityCG.cginc"
#include "UnityStandardBRDF.cginc"
导入文件的层次结构,从UnityStandardBRDF文件开始。
光源
为了不对光的入射方向进行硬编码,我们应该使用在我们的场景中的光的入射方向。在默认情况下,每个Unity场景都有一个表示太阳的光源。它是一个方向光,这意味着它被认为是无限远的。结果就是,场景中所有的光线来自完全相同的方向。当然,在现实生活中这不是真的,但太阳是如此的远,以至于这是一个很棒的近似。