学习第六章Unity内置函数时,由于之前使用mul矩阵乘法时的顺序与书中不一致,导致使用内置函数时出现光照效果不一样,因此引出以下两个问题:
1 什么时候使用3x3矩阵,什么时候使用4x4矩阵?
2 法线变换矩阵与坐标变换矩阵不相同?
解答1:
4.9.1节书中讲述了何时使用3x3和4x4矩阵。因为4x4矩阵是比3x3矩阵多了平移变换,因此对空间坐标进行变换时,通常使用4x4矩阵。而对于切线和法线这两种空间矢量,不存在平移的情况,因此仅使用3x3矩阵即可(是否可以偷懒使用4x4矩阵?是可以的)。
解答2:
而对于法线变换,因为对坐标缩放时,有可能不是等比缩放,这会导致法向量使用坐标变换矩阵后不再垂直于表面。有下述推导过程求得法线变换矩阵:
假设某平面点的模型空间切线为T,法线为N。通过空间转换M,法线转换G后,切线为T‘,法线为N‘。那么有:
TT·N = 0 ----- (1),
(T‘)T·N‘ = 0 -----(2),
T‘ = M·T -----(3),
N‘ = G·N -----(4)。
由式(2)(3)(4)有:
(M·T)T·(G·N) = 0 ==> TT·MT·G·N = 0 ==> (矩阵结合律) (TT·MT·G)·N = 0 -----(5)
结合式(1)和(5),得到:TT = TT·MT·G,由此得到 MT·G = I,则:
G = (MT)-1 = (M-1)T
即法线的变换矩阵,是空间变换矩阵的转置的逆。
======================Unity Shader中计算注意事项=======================
1 在Unity Shader中,将坐标从模型空间转换到世界空间,使用如下方式:
v2f o; o.worldPos = mul(_Object2World, v.vertex).xyz;
UnityShader中的转换矩阵为行矩阵,模型空间坐标v.vertex作为列矩阵,与转换矩阵右乘,得到世界坐标。
注意,这里不能使用 o.worldPos = UnityObjectToWorldDir(v.vertex); 在UnityCG.cginc中查看UnityObjectToWorldDir的定义:
// Transforms direction from object to world space inline float3 UnityObjectToWorldDir( in float3 dir ) { return normalize(mul((float3x3)_Object2World, dir)); }
这里使用的是3x3矩阵,使得坐标变换丢失了平移参数。
2 将法线从模型空间转换到世界空间,使用如下方式:
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
在这里,矩阵_World2Object是矩阵_Object2World的逆矩阵。由于是法向量,因此取3x3矩阵进行计算。
需要注意的是,这里的mul实现的矩阵乘法,当向量在左侧时,此向量相当于行向量,其中数值是与矩阵的列元素进行乘法与加法。相当于如下写法:
o.worldNormal = mul(transpose((float3x3)_World2Object), v.normal);
即法线的变换是空间变换_Object2World的逆矩阵的转置transpose(_World2Object)。
这里Unity Shader封装了一个函数来转换法线到世界坐标UnityObjectToWorldNormal(),替代上面的矩阵乘法写法,不容易出错。
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// Transforms normal from object to world space inline float3 UnityObjectToWorldNormal( in float3 norm ) { // Multiply by transposed inverse matrix, actually using transpose() generates badly optimized code return normalize(_World2Object[0].xyz * norm.x + _World2Object[1].xyz * norm.y + _World2Object[2].xyz * norm.z); }
================书中完整示例===============
Shader "Unity Shaders Book/Chapter 6/Blinn-Phong Build-In Function" { Properties { _Diffuse("Diffuse", Color) = (1.0, 1.0, 1.0, 1.0) _Specular("Specular", Color) = (1.0, 1.0, 1.0, 1.0) _Gloss("Gloss", Range(8.0, 256.0)) = 20.0 } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Diffuse; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; fixed3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert (a2v v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); // 效果与下一行相同,因为是矢量变换,用3x3变换矩阵即可。 // o.worldNormal = mul(v.normal, (float3x3)_World2Object); // o.worldPos = UnityObjectToWorldDir(v.vertex); // normalize(mul((float3x3)_Object2World, dir));效果与下一行不同 // o.worldPos = mul(v.vertex, transpose(_Object2World)).xyz; // 将坐标左乘,将模型空间坐标转换到世界空间,与下式相等 o.worldPos = mul(_Object2World, v.vertex).xyz; // 将坐标右乘,将模型空间坐标转换到世界空间 return o; } fixed4 frag(v2f i) : SV_Target { fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 halfDir = normalize(viewDir + worldLightDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(halfDir, worldNormal)) , _Gloss); fixed3 color = ambient + diffuse + specular; return fixed4(color, 1.0); } ENDCG } } Fallback "Specular" }
使用Unity5 build-in的函数实现Blinn-Phong光照模型。