前言
问题描述:在学习《Shader入门精要》时,在顶点着色器中对法线和坐标的空间变换采用了不同的操作,进行坐标变换的矩阵为unity_ObjectToWorld,而进行法线变换的矩阵竟然是unity_WorldToObject,如下所示:
f.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); // 将顶点法线从模型空间变换到世界空间
f.worldPos = mul(unity_ObjectToWorld, v.vertex); // 将顶点坐标从模型空间变换到世界空间
但从功能的角度来考虑,我们确实需要将法线和坐标从模型空间变换到世界空间,而这里却用了unity_WorldToObject矩阵,在网上找了各种资料发现讲的都不太清楚,自己又琢磨了一番后,发现原因如下:
1 缩放导致的法线不再垂直
为了解决上述问题,我们首先需要了解:
一般来说,点和绝大部分方向矢量都可以使用同一个 4 × 4 4\times4 4×4 或 3 × 3 3\times3 3×3 的变换矩阵 M A → B M_{A\rightarrow B} MA→B 把其从坐标空间A变换到坐标空间B中,但在变换法线的时候,由于我们只对坐标进行了变换,法线和切线都可以理解为通过向量相减得到的结果,如果使用同一个变换矩阵,就无法确保维持法线的垂直性

2 解决方案
假设模型空间切线为
T
T
T,法线为
N
N
N,则对于模型空间来说,法线和切线满足:
T
⋅
N
=
T
T
N
=
0
T \cdot N = T^TN = 0
T⋅N=TTN=0
对于新的变换后的世界空间,我们将unity_ObjectToWorld矩阵简写为
M
O
→
W
M_{O \rightarrow W}
MO→W,则变换后的切线向量为
M
O
→
W
T
M_{O \rightarrow W}T
MO→WT
对于世界空间的法线,我们将其乘以一个矩阵
G
G
G,使其满足:
T
′
⋅
N
′
=
(
M
O
→
W
T
)
⋅
(
G
N
)
=
0
T^{'} \cdot N^{'} = (M_{O \rightarrow W}T) \cdot (GN) = 0
T′⋅N′=(MO→WT)⋅(GN)=0
对于向量的点乘,由于
T
T
T,
N
N
N 均为列向量,则写为矩阵的形式:
(
M
O
→
W
T
)
⋅
(
G
N
)
=
(
M
O
→
W
T
)
T
(
G
N
)
=
T
T
M
O
→
W
T
G
N
=
0
(M_{O \rightarrow W}T) \cdot (GN) = (M_{O \rightarrow W}T)^T(GN) = T^TM_{O \rightarrow W}^TGN = 0
(MO→WT)⋅(GN)=(MO→WT)T(GN)=TTMO→WTGN=0
又由于
T
T
N
=
0
T^TN = 0
TTN=0
只需满足
M
O
→
W
T
G
=
I
M_{O \rightarrow W}^TG = I
MO→WTG=I
即可,则
G
=
(
M
O
→
W
T
)
−
1
G = (M_{O \rightarrow W}^T)^{-1}
G=(MO→WT)−1
至此,我们就确定了法线在进行空间变换时需要乘以的矩阵就变成了原变换矩阵的逆转置矩阵,即
(
u
n
i
t
y
_
O
b
j
e
c
t
T
o
W
o
r
l
d
−
1
)
T
(unity\_ObjectToWorld^{-1})^T
(unity_ObjectToWorld−1)T
3 回到最开始问题
现在,我们要着手开始解释为什么会出现我们最开始讨论的情况(法线变换矩阵和坐标变换矩阵不一致的问题):
f.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); // 将顶点法线从模型空间变换到世界空间
f.worldPos = mul(unity_ObjectToWorld, v.vertex); // 将顶点坐标从模型空间变换到世界空间
我们现在已经知道,要对法线进行空间变换,需要用变换矩阵的逆转置矩阵乘以法线向量,即
N
′
=
(
u
n
i
t
y
_
O
b
j
e
c
t
T
o
W
o
r
l
d
T
)
−
1
N
=
(
u
n
i
t
y
_
O
b
j
e
c
t
T
o
W
o
r
l
d
−
1
)
T
N
N^{'} = (unity\_ObjectToWorld^T)^{-1}N = (unity\_ObjectToWorld^{-1})^TN
N′=(unity_ObjectToWorldT)−1N=(unity_ObjectToWorld−1)TN
对于等式的左右两边同取转置可得
(
N
′
)
T
=
(
(
u
n
i
t
y
_
O
b
j
e
c
t
T
o
W
o
r
l
d
−
1
)
T
N
)
T
=
N
T
u
n
i
t
y
_
O
b
j
e
c
t
T
o
W
o
r
l
d
−
1
(N^{'})^T = ((unity\_ObjectToWorld^{-1})^TN)^T = N^Tunity\_ObjectToWorld^{-1}
(N′)T=((unity_ObjectToWorld−1)TN)T=NTunity_ObjectToWorld−1
又由于我们知道,unity_ObjectToWorld矩阵的逆矩阵就是unity_WorldToObject,我们有:
(
N
′
)
T
=
N
T
u
n
i
t
y
_
W
o
r
l
d
T
o
O
b
j
e
c
t
(N^{'})^T = N^Tunity\_WorldToObject
(N′)T=NTunity_WorldToObject
与原代码中
f.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
比较可以发现,除了向量从列矩阵变为了行矩阵以外,其余的表达形式一致,个人推测是编译器对向量是一个行向量还是列向量做了自动的修正(不太确定,求大佬在评论区解答)
同时,对坐标进行变换的表达式也比较符合直觉:
f.worldPos = mul(unity_ObjectToWorld, v.vertex);
P W o r l d = u n i t y _ O b j e c t T o W o r l d P O b j e c t P_{World} = unity\_ObjectToWorldP_{Object} PWorld=unity_ObjectToWorldPObject