Unity Shader入门精要 第4章 答疑解惑 读书笔记

第4章 学习Shader所需的数学基础-学习Shader所需的数学基础-答疑解惑

注意:图片的来源基本来自作者冯乐乐的GitHub,感谢作者分享

https://github.com/candycat1992/Unity_Shaders_Book

 

3×3矩阵 还是 4×4矩阵:

对于线性变换(例如旋转和缩放),仅使用 3×3矩阵 就足够表示所有的变换了。

如果存在平移变换,就需要使用 4×4变换矩阵。

点坐标 在变换前需要把转换成 齐次坐标 的表示(把顶点的 w分量 设置为1)

方向矢量 直接使用 3×3矩阵 足够(平移变换对方向矢量没有影响)

 

Cg 中的矢量和矩阵类型:

在 Unity Shader 中使用Cg作为着色器编程语言,使用 Cg 变量类型进行数学运算

在 Cg 中矩阵类型是由 float3×3 float4×4 等关键词进行声明和定义

float3、float4 等类型的变量(取决于运算的种类和它们在运算中的位置):

1、可以当成一个矢量,

2、也可以当成一个 1×n 的行矩阵

3、或者当成一个 n×1 的列矩阵

 

点积操作,两个操作数就被当成了 矢量类型:

float4 a = float4( 1.0, 2.0, 3.0, 4.0);

float4 b = float4( 1.0, 2.0, 3.0, 4.0);

//对两个矢量进行点积操作

float result = dot(a,b)

 

矩阵乘法,参数的位置决定是 列矩阵 还是 行矩阵 进行乘法。

在 Cg 中,矩阵乘法是通过mul函数实现的:

float4 v = float4(1.0, 2.0, 3.0, 4.0);

float4x4 M = float4x4(

                                   1.0, 0.0, 0.0, 0.0,

                                   0.0, 2.0, 0.0, 0.0,

                                   0.0, 0.0, 3.0, 0.0,

                                   0.0, 0.0, 0.0, 4.0

                                 );

//把v当成 列矩阵 和 矩阵M 进行右乘:

float4 column_mul_result = mul(M,v);

 

//把v当成 行矩阵 和 矩阵M 进行左乘:

float4 row_mul_result = mul(v,M);

 

//注意:column_mul_result 不等于 row_mul_result,而是:

// mul(M,v) == mul(v, tranpose(M))

// mul(v,M) == mul(tranpose(M), v)

 

参数的位置会直接影响结果值。通常在变换顶点时,都是使用 右乘的方式 按列矩阵 进行乘法(因为 Unity 提供的内置矩阵,如 UNITY_MATRIX_MVP 等 都是按列存储的),但是有时候会使用到左乘的方式,因为左乘可以省去对矩阵转置的操作

 

注意:Cg 对 矩阵类型中元素的 初始化 和 访问顺序。

在 Cg 中,对 float4x4 等类型的变量是按 行优先 的方式进行填充的。

填充一个矩阵需要给定一串数字,例如,声明 3×4 的矩阵,需要提供12个数字。

这串数字是 一行一行地填充矩阵 还是 一列一列地填充矩阵(两种方式得到的矩阵是不同的)

例如,使用 ( 1, 2, 3, 4, 5, 6, 7, 8, 9 ) 填充 3×3 矩阵。

行优先的方式得到的矩阵是:

列优先的方式得到的矩阵是:

 

Cg 使用的是 行优先 的方法,即是 一行一行地填充矩阵

因此,如果需要定义一个矩阵的时候(例如自己构建用于空间变换的矩阵),就要注意这里的初始化方式(用 行 优先的方式)

类似的,当在 Cg 中访问一个矩阵中的元素时,也是按照行索引的:

//按 行优先 的方式初始化矩阵:

float3x3 M = float4x4(

                                   1.0, 2.0, 3.0,

                                   4.0, 5.0, 6.0,

                                   7.0, 8.0, 9.0

                                 );

//得到 M 的第一行,即 (1.0, 2.0, 3.0)

float3 row = M[0];

//得到 M 的第2行 第1列 的元素 ,即 4.0

float ele = M[1][0];

 

注意:Unity 在 脚本中提供的 Matrix4x4,这种矩阵类型 采用的是 列优先 的方式。

与 Cg 采用的 行优先 不同

 

Unity 中的 屏幕坐标:ComputeScreenPos / VPOS / WPOS

在 顶点/片元 着色器中,有两种方式获得 片元的屏幕坐标:

 

方式一:在 片元着色器 的输入中声明 VPOS 或者 WPOS 语义(VPOS 是 HLSL 中对屏幕坐标的语义,WPOS 是 Cg 中对屏幕坐标的语义,两者在 Unity Shader 中是等价的)

 

在 HLSL/Cg 中通过语义的方式来 定义 顶点/片元着色器 的默认输入,而不需要自己定义输入输出的数据结构。在 片元着色器 中这么写:

 

fixed4 frag( float4 sp : VPOS ) : SV_Target {

        //用屏幕坐标除以屏幕分辨率 _ScreenParams.xy ,得到视口空间中的坐标

        return fixed4( sp.xy / _ScreenParams.xy , 0.0 , 1.0 );

}

 

VPOS/WPOS 定义的输入是一个 float4 类型的变量:

VPOS/WPOS 的 xy 值代表了在屏幕空间中的像素坐标。

如果屏幕分辨率为 400x300。那么 x 的范围就是 [ 0.5 , 400.5 ] ,y 的范围就是 [ 0.5 , 300.5 ]

注意:这里的像素坐标不是整数值,因为 OpenGL 和 DirectX 10 以后的版本都认为 像素中心对应的是浮点值中的0.5。

 

在 Unity 中, VPOS/WPOS 的 z分量 范围 是 [ 0 , 1 ]

在摄像机的近裁剪平面处,z值为0,在远裁剪平面处,z值为1。

对于 w分量 ,需要考虑摄像机的投影的类型:

1、透视投影 的 w分量 的范围是:

Near 对应了在 Camera组件 中设置的近裁剪平面 距离摄像机的 近

Far 对应了在 Camera组件 中设置的远裁剪平面 距离摄像机的 远

2、正交投影 的 w分量 的值恒为1

 

w分量的值 是经过对投影矩阵变换后的 w分量 取倒数后 得到的

 

把屏幕空间 除以 屏幕分辨率 得到 视口空间 中的坐标

视口坐标:把屏幕坐标归一化,这样屏幕左下角就是 ( 0, 0 ),右上角就是 ( 1, 1 )

如果已知屏幕空间,就只需要把 xy值 除以屏幕分辨率 即可

 

方式二:使用 Unity 提供的 ComputeScreenPos 函数(在 UnityCG.cginc 被定义)

步骤1:在 顶点着色器 中将 ComputeScreenPos 的结果保存在输出结构体中

步骤2:在 片元着色器 中进行一个 齐次除法运算 后得到 视口空间下的坐标

这种方式手动实现了 屏幕映射 的过程,而且得到的坐标直接就是视口空间中的坐标

 

struct vertOut {

         float4 pos : SV_POSITION;

         float4 scrPos : TEXCOORD0;

};

 

vertOut vert( appdate_base v ){

         vertOut o;

         o.pos = mul( UNITY_MATRIX_MVP, v.vertex );

         //第一步:把 ComputeScreenPos 的结果保存到 scrPos 中

         o.scrPos = ComputeScreenPos( o.pos );

         return o;

};

 

fixed4 frag(vertOut i) : SV_Target {

         //第二步:用 scrPos.xy 除以 scrPos.w 得到视口空间中的坐标

         float2 wcoord = ( i.scrPos.xy / i.scrPos.w );

         return fixed4( wcoord, 0.0, 1.0 );

}

 

得到视口空间中的坐标。对裁剪空间下的坐标进行齐次除法,得到范围在 [ -1 , 1 ] 的 NDC,然后再将其映射到范围在 [ 0 , 1 ] 的视口空间下的坐标。

公式:

 

在 UnityCG.cginc 中找到 ComputeScreenPos 函数的定义:输入参数 pos 是经过 MVP 矩阵变换后在裁剪空间中的顶点坐标。UNITY_HALF_TEXEL_OFFSET 是 Unity 在某些 DirectX 平台上使用的宏。_ProjectionParams.x 在默认情况下是1(如果使用了一个翻转的投影矩阵的话就是 -1)

 

inline float4 ComputeScreenPos ( float4 pos ){

         float4 o = pos * 0.5f;

         # if defined(UNITY_HALF_TEXEL_OFFSET)

         o.xy = float2( o.x, o.y*_ProjectionParams.x ) + o.w * _ScreenParams.zw;

         #else

         o.xy = float2( o.x, o.y*_ProjectionParams.x) + o.w;

         #endif

 

         o.zw = pos.zw;

         return o;

}

 

上述代码实际输出:

Unity Shader入门精要 第4章 答疑解惑 读书笔记_第1张图片

这里的 xy 并不是真正的视口空间下的坐标。因此需要在片元着色器中进一步处理,除以裁剪坐标的 w分量。至此完成整个映射的过程。

因此,ComputeScreenPos 函数计算后,还需要在片元着色器除以它的 w分量,才能得到最后真正的视口空间中的位置。之所以让开发者自己除以 w分量,是因为 Unity 如果在顶点着色器中自己除以w分量的话会破坏插值的结果

 

除法操作后就可以得到该片元在视口空间中的坐标,也就是一个 xy 范围在 [ 0 , 1 ] 之间的值

从 顶点着色器 到 片元着色器 的过程 实际会有一个插值的过程。

如果不在 顶点着色器 中进行这个除法,保留 x、y 和 w分量,那么它们在插值后再进行这个除法,那么得到的就是正确的。

但是如果直接在 顶点着色器 中进行这个除法,那么就需要对 除后的结果进行插值。这样得到的插值会不准确,原因是:不可以在投影空间中进行插值,因为投影空间不是一个线性空间,而插值往往是线性的。

 

zw值的计算:

在顶点着色器中,直接把 裁剪空间的 zw值存进了输出结构体中,因此 片元着色器 输入的就是这些插值后的裁剪空间中的 zw值。这意味着:

如果使用的是 透视投影,那么 z值 的范围是 [ -Near, Far ]

如果使用的是 正交投影,那么 z值 的范围是 [ -1, 1 ],而 w值 恒为 1

 

你可能感兴趣的:(读书笔记,-,Unity,Shader,入门精要)