关于UnityShader法线和坐标变换的处理不一致的问题

前言

问题描述:在学习《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} MAB 把其从坐标空间A变换到坐标空间B中,但在变换法线的时候,由于我们只对坐标进行了变换,法线和切线都可以理解为通过向量相减得到的结果,如果使用同一个变换矩阵,就无法确保维持法线的垂直性

2 解决方案

假设模型空间切线为 T T T,法线为 N N N,则对于模型空间来说,法线和切线满足:
T ⋅ N = T T N = 0 T \cdot N = T^TN = 0 TN=TTN=0
对于新的变换后的世界空间,我们将unity_ObjectToWorld矩阵简写为 M O → W M_{O \rightarrow W} MOW,则变换后的切线向量为 M O → W T M_{O \rightarrow W}T MOWT

对于世界空间的法线,我们将其乘以一个矩阵 G G G,使其满足:
T ′ ⋅ N ′ = ( M O → W T ) ⋅ ( G N ) = 0 T^{'} \cdot N^{'} = (M_{O \rightarrow W}T) \cdot (GN) = 0 TN=(MOWT)(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 (MOWT)(GN)=(MOWT)T(GN)=TTMOWTGN=0
又由于
T T N = 0 T^TN = 0 TTN=0
只需满足
M O → W T G = I M_{O \rightarrow W}^TG = I MOWTG=I
即可,则
G = ( M O → W T ) − 1 G = (M_{O \rightarrow W}^T)^{-1} G=(MOWT)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_ObjectToWorld1)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_ObjectToWorld1)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_ObjectToWorld1)TN)T=NTunity_ObjectToWorld1
又由于我们知道,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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值