12 UnityShader语法入门
来源:《UNITY SHADER入门精要》
1、Unity的空间变换名词
最后的一个步骤:将视椎体(view frustum)变换成 立方体。我们进行齐次除法(homogeneous division),实际上就是用齐次坐标系中的 w 分量去除以 x,y,z ,在OpenGL中,我们把这一步得到的坐标叫做归一化的设备坐标(Normalized Device Coordinates,NDC)。
OpenGL中,立方体的 x,y,z 分量的范围是 [-1, 1] ,而 DirectX 中 z 的分量是 [0, 1] 也就是说,有半个立方体。
PS:一定要吐槽的是,各个教程和书籍的术语使用都不一样呢,这里的裁剪空间,在标准的术语中叫做投影空间。
2、Unity的变换矩阵
Unity内置的空间变换和内置变量已经在UnityShaderVariables.cginc文件定义和说明了。这些都是 float4x4 类型的。
其中,UNITY_MATRIX_T_MV矩阵是从模型空间到观察空间的矩阵,当只有旋转、平移、缩放 这种线性变换,MV矩阵就是个正交矩阵,也就是说它的逆矩阵是它的转置矩阵。在Unity中可以直接调用。
3、Unity中的向量矩阵乘法
Unity提供的内置矩阵(如UNITY_MATRIX_MVP等)都是按列存储的,所以,我们都是使用右乘的方法来按列存储的。**但有时,我们也会使用左乘的方式,这是因为可以省去对矩阵转置的操作。**所以,看到这种左乘的方式,就不要慌,很有可能就是它不想求逆,所以左乘的。
4、Unity中的屏幕坐标
在写Shader的过程中,我们有时希望能获得屏幕上像素的位置。
由于 Unity 有 HLSL/CG 的语义支持。,我们就嫩在片元着色器中声明VPOS(HLSL中对屏幕坐标的语义)或者WPOS(CG中对屏幕坐标的语义),就能获得。例子:
fiexd4 frag(float4 sp : VPOS) : SC_Target{
return fixed4(sp.xy/_ScreenParams.xy, 0.0, 1.0);
}
如果屏幕分辨率为 400 × 300 400 \times 300 400×300,那么 x 的范围是 [ 0.5 , 400.5 ] [0.5, 400.5] [0.5,400.5],y 的范围 [ 0.5 , 300.5 ] [0.5, 300.5] [0.5,300.5] 。这里的像素坐标不是整数值,是因为 OpenGL 和 DirectX 10以后的版本认为像素的中心应该是像素中心的浮点值中的0.5。
5、屏幕坐标问题
在OpenGL中,渲染纹理(Render Texture)的(0, 0)点是在左下角,而在DirectX中,(0, 0)点是在左上角。
6、法线变换问题
这里讨论的是从 模型空间 到 世界空间 的法线变换问题。一般来说,绝大部分矢量和空间中的点都能通过一个 4x4 或者 3x3 的矩阵 m A → B \boldsymbol{m}_{A\rightarrow B} mA→B 能够解决。但是,在变换法线的时候,如果使用同一个换换矩阵,就可能出现无法确保法线的垂直性。
为此,我们引入切线(tangent)。它也是模型顶点所携带能确认的信息,而且与法线垂直。
同时,我们通过数学的约束条件来推导出所需要的矩阵,即,切线
T
A
\boldsymbol{T}_A
TA 和法线
N
A
\boldsymbol{N}_A
NA ,必须满足:
T
A
⋅
N
A
=
0
\boldsymbol{T}_A \cdot \boldsymbol{N}_A = 0
TA⋅NA=0。我们已知 世界变换矩阵
m
A
→
B
\boldsymbol{m}_{A\rightarrow B}
mA→B ,有
T
B
=
m
A
→
B
T
A
\boldsymbol{T}_B = \boldsymbol{m}_{A\rightarrow B} \, \boldsymbol{T}_A
TB=mA→BTA 。我们现在需要一个矩阵
G
\boldsymbol{G}
G,来保持垂直的特性。
为使得变换后,法线任然与切线垂直,有:
T
B
⋅
N
B
=
0
(
M
A
→
B
T
A
)
⋅
(
G
N
A
)
=
0
(
M
A
→
B
T
A
)
T
(
G
N
A
)
=
0
T
A
T
M
A
→
B
T
G
N
A
=
0
T
A
T
(
M
A
→
B
T
G
)
N
A
=
0
\boldsymbol{T}_B\cdot \boldsymbol{N}_B=0 \\ \left( \boldsymbol{M}_{A\rightarrow B}\boldsymbol{T}_A \right) \cdot \left( \boldsymbol{GN}_A \right) =0 \\ \left( \boldsymbol{M}_{A\rightarrow B}\boldsymbol{T}_A \right) ^T\left( \boldsymbol{GN}_A \right) =0 \\ {\boldsymbol{T}_A}^T{\boldsymbol{M}_{A\rightarrow B}}^T\,\,\boldsymbol{GN}_A=0 \\ {\boldsymbol{T}_A}^T\left( {\boldsymbol{M}_{A\rightarrow B}}^T\,\,\boldsymbol{G} \right) \boldsymbol{N}_A=0
TB⋅NB=0(MA→BTA)⋅(GNA)=0(MA→BTA)T(GNA)=0TATMA→BTGNA=0TAT(MA→BTG)NA=0
由于
T
A
⋅
N
A
=
0
\boldsymbol{T}_A \cdot \boldsymbol{N}_A = 0
TA⋅NA=0,因此,如果
M
A
→
B
T
G
=
I
{\boldsymbol{M}_{A\rightarrow B}}^T\,\,\boldsymbol{G}=\boldsymbol{I}
MA→BTG=I,那么上式成立。
也就是说:
G
=
(
M
A
→
B
T
)
−
1
=
(
M
A
→
B
−
1
)
T
\,\,\boldsymbol{G}=\left( {\boldsymbol{M}_{A\rightarrow B}}^T \right) ^{-1} \\ =\left( {\boldsymbol{M}_{A\rightarrow B}}^{-1} \right) ^T
G=(MA→BT)−1=(MA→B−1)T
翻译一下:用原来的变换矩阵的 逆矩阵 转置 一下,就能得到我们想要的矩阵
G
\boldsymbol{G}
G。
当然,值得注意的是,如果
m
A
→
B
\boldsymbol{m}_{A\rightarrow B}
mA→B 本身就是正交矩阵,按照性质,
M
A
→
B
T
=
M
A
→
B
−
1
{\boldsymbol{M}_{A\rightarrow B}}^T={\boldsymbol{M}_{A\rightarrow B}}^{-1}
MA→BT=MA→B−1,那么
(
M
A
→
B
T
)
−
1
=
M
A
→
B
\left( {\boldsymbol{M}_{A\rightarrow B}}^T \right) ^{-1}=\boldsymbol{M}_{A\rightarrow B}
(MA→BT)−1=MA→B。
也就是,这个时候的 法线的世界转换矩阵 就是 普通其它顶点的世界矩阵。
那什么时候它又是正交矩阵呢,答案:当变换只包含旋转,不包含平移和缩放的时候,它就是正交矩阵。
而如果当变换只有旋转和统一缩放的时候,我们可以利用统一缩放系数 k 来得到 世界矩阵 的逆转置矩阵
(
M
A
→
B
T
)
−
1
=
1
k
M
A
→
B
\left( {\boldsymbol{M}_{A\rightarrow B}}^T \right) ^{-1}=\frac{1}{k}\boldsymbol{M}_{A\rightarrow B}
(MA→BT)−1=k1MA→B。这样可以避免计算逆矩阵的过程。当然,避无可避的时候,就只能计算它的逆矩阵了。