Unity_Shader基础篇_4.2_Unity Shader入门精要

4.8 Unity Shader 的内置变量(数学篇)
使用unity写Shader的一个好处在于,它提供了很多内置的参数,这使得我们不再需要自己手动计算一些值。本节将给出Unity内置的用于空间变换和摄像机以及屏幕参数的内置变量。这些内置变量可以在UnityShadrVariables.cginc文件中找到定义和说明。
4.8.1 变换矩阵
首先是用于坐标空间变换的矩阵。表4.2给出了unity5.2版本提供的所有内置变换矩阵。下面所有的规矩都是float4×4类型的。
这里写图片描述
表4.2给出了这些矩阵的触感进行。例如我们可以提供坐标空间的坐标轴。
其中有一个矩阵比较特殊,即UNITY_MATRIX_T_MV矩阵。已知对于正交矩阵来说,它的逆矩阵就是转置矩阵。因此,如果UNITY_MATRIX_MV是一个正交矩阵的话,那么UNITY_MATRIX_T_MV就是它的逆矩阵,也就是说,我们可以使用UNITY_MATRIX_T_MV把顶点和方向矢量从观察空间变换到模型空间。那么问题是,UNITY_MATRIX_MV什么时候是一个正交矩阵(4.5)?
总结一下,如果我们只考虑旋转、平移和缩放这3种变换的话,如果一个模型的变换只包括旋转,那么UNITY_MATRIX_MV就是一个正交矩阵。这个条件似乎有些苛刻,我们可以把条件再放宽一些,如果只包括旋转和统一缩放(假设缩放系数是k),那么UNITY_MATRIX_MV就几乎是一个正交矩阵了。为什么是几乎呢?因为统一缩放可能会导致每一行(或每一列)的矢量长度不为1,而是k,这不符合正交矩阵的特性,但我们可以通过除以这个统一缩放系数,来把它变成正交矩阵。这种情况下,UNITY_MATRIX_MV的逆矩阵就是1/k(UNITY_MATRIX_T_MV)。而且,如果我们只是对方向矢量进行变换的话,条件可以放得更宽,即不用考虑有没有平移变换,因为平移对方向矢量对方向矢量没有影响。因此,我们可以截取UNITY_MATRIX_T_MV的前3行前3列来把方向矢量从观察空间变换到模型空间(前提是只存在旋转变换和统一缩放)。对于方向矢量,我们可以在使用前对它们进行归一化处理,来消除统一缩放的影响。
UNITY_MATRIX_IT_MV矩阵也比较特殊,在4.7中我们已知,法线的变换需要使用原变换矩阵的逆转置矩阵。因此UNITY_MATRIX_IT_MV可以把法线从模型空间变换到观察空间。但只要我们做一点手脚,他也可以用于直接得UNITY_MATRIX_MV的逆矩阵——我们只需要对它进行转置就可以了。因此,为了把顶点或方向矢量从观察空间变换到模型空间,我们可以使用类似下面的代码:

//方法一:使用transpose函数对UNITY_MATRIX_IT_MV进行转置,
//得到UNITY_MATRIX_MV的逆矩阵,然后进行列矩阵乘法,
//把观察空间中的点或方向矢量变换到模型空间中
float4 modelPos = mul(transpose(UNITY_MATRIX_IT_MV),viewPos);

//方法二:不直接使用转置函数transpose,而是交换mul参数的位置,使用行矩阵乘法
//本质和方法一是完全一样的
float4 modelPos = mul(viewPos,UNITY_MATRIX_IT_MV);

4.8.2 摄像机和屏幕参数
Unity提供了一些内置变量来让我们访问当前正在渲染的摄像机的参数信息。这些参数对应了摄像机上的Camera组件中的属性值。表4.3给出了Unity5.2版本提供的这些变量。
这里写图片描述
这里写图片描述

4.9 答疑解惑
4.9.1 使用3×3还是4×4的变化矩阵
对于线性变换(例如旋转和缩放)来说,仅使用3×3的矩阵就足够表示所有的变换了。但如果存在平移变换,我们就需要使用4×4的矩阵。因此,在对顶点的变换中,我们通常使用4×4的变换矩阵。当然,在变换前我们需要把点坐标抓换成齐次坐标的表示,即把顶点的w分量设为1。而在对方向矢量的变换中,我们通常使用3×3的矩阵就足够了,这是因为平移变换对方向矢量是没有影响的。
4.9.2 CG中的矢量和矩阵类型
我们通常在Unity Shader中使用CG作为着色器编程语言。在CG中变量类型有很多种,但在本节我们是想解释如何使用这些类型进行数学运算。因此,我们只以float家族的变量来做说明。
在进行矩阵乘法时,参数的位置将决定是按列矩阵还是行矩阵进行乘法。在CG中,矩阵乘法是通过mul函数实现的。例如:

float4 v = float4(1.0,2.0,3.04.0)
float4×4 M = float4×41.0,0.0,0.0,0.00.0,1.0,0.00.0
                       0.0,0.01.0,0.0
                       0.0,0.00.0,1.0)
//把v当成列矩阵和矩阵M进右乘(4-1)
float4 column_mul_result = mul(M,v);
//把v当成行矩阵和矩阵M进行左乘(1-4)
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)

Cg 标准函数库:https://wenku.baidu.com/view/3a9db318fad6195f312ba675.html
mul(M, N) 计算两个矩阵相乘,如果M为AxB阶矩阵,N为BxC阶矩阵,则返回AxC阶矩阵。下面两个函数为其重载函数。
 mul(M, v) 计算矩阵和向量相乘 
 mul(v, M) 计算向量和矩阵相乘 

因此,参数的位置会直接影响结果值。通常在变换顶点时,我们都是使用右乘的方式来按列矩阵进行乘法。这是因为,Unity提供的内置矩阵(如UNITY_MATRIX_MVP等)都是按列存储的。但有时,我们也会使用左乘的方式,这是因为可以省去对矩阵转置的操作。
需要注意的一点是,CG对矩阵类型中元素的初始化和访问顺序。在CG中,对float4×4等类型的变量是按行优先的方式进行填充的。
CG使用的是行优先的方法,计时一行一行地填充矩阵的。因此,如果读者需要自己定义一个矩阵时(例如,自己构建用于空间变换的矩阵),就要注意这里的初始化方式。
类似地,当我们在CG中访问一个矩阵中的元素时,也是按行来索引的。例如:

//按行优先的方式初始化矩阵M
float3×3 M = float3×3 (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的第二行第一列的元素,即4.0
float ele = M[1][0];

之所以Unity Shader中的矩阵类型满足上述规则,是因为使用的是CG语言。换句话说,上面的特性都是CG的规定。
在Unity中Unity脚本中提供一种矩阵类型——Matrix×4×4。脚本中的这个矩阵类型则是采用列优先的方式。这与Unity Shader中的规定不一样。
4.9.3 Unity中的屏幕坐标:ComputeScreenPos/VPOS/WPOS
在顶点/片元着色器中,有两种方式来获得片元的屏幕坐标。
一种是在片元着色器的输入中声明VPOSWPOS语义(5.4)。VPOSHLSL中对屏幕坐标的语义,而WPOSCG中对屏幕坐标的语义。两者在Unity Shader中是等价的。我们可以在HLSL/CG中通过语义的方式来定义顶点/片元着色器的默认输入,而不需要自己定义输入输出的数据结构。使用这种方法,可以在片元着色器中这样写:

fixed4 frag(float4 sp:VPOS):SV_Target{
    //用屏幕坐标除以屏幕分辨率_ScreenParams.xy,得到视口空间中的坐标
    return fixed4(sp.xy/_ScreenParams.xy,0.0,1.0;
}

我们把屏幕空间除以屏幕分辨率来得到视口空间(viewport space)中的坐标。视口坐标很简单,就是把屏幕坐标归一化,这样屏幕左下角就是(0,0),右上角就是(1,1)。如果已知屏幕坐标的话,我们只需要把xy值除以屏幕分辨率即可。
另一种方式是通过Unity提供的ComputeScreenPos函数。这个函数在UnityCG.cginc里被定义。通常的用法需要两个步骤,首先在顶点着色器中将ComputeScreenPos的结果保存在输出结构体中,然后在片元着色器中进行一个齐次除法运算后得到视口空间下的坐标。例如:

struct verOut{
    float4 pos : SV_POSITION;
    float4 scrPos : TEXCOORD0;
}
vertOut vert(appdata_base v){
     vertOut o;
     o.pos = mul(UNITY_MATRIX_MVP,V.Vertex);
     //第一步:把ComputeScenePos的结果保存到scrPos中
     o.scrPos = ComputeScenePos(o.pos);
     return 0;
}
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);
}

上面的代码实际上是手动实现了屏幕映射的过程,而且他得到的坐标直接就是视口空间中的坐标。我们在3.6.8节中已经看到了如何将裁剪坐标空间中的点映射到屏幕坐标中。据此,我们可以得到视口空间中的坐标,公式如下:

viewport(x) = clip(x)/2*clip(w) + 1/2
viewport(y) = clip(y)/2*clip(w) + 1/2

上面公式的思想就是,首先对裁剪空间下的坐标进行齐次除法,得到范围在[-1,1]的NDC,然后再将其映射到范围在[0,1]的视口空间下的坐标。那么ComputeScreenPos究竟是如何做到的,我们在unityCG.cginc文件中找到了ComputeScreenPos函数的定义。如下:

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; 
}

ComputeScreenPos的输入参数pos是经过MVP矩阵变换后在裁剪空间中的顶点坐标。UNITY_HALF_TEXEL_OFFSET是Unity在某些DirextX平台上使用的宏,在这里我们可以忽略它。这样,我们可以只关注#else的部分。_ProjectionParams.x在默认情况下是1(如果我们使用了一个翻转的投影矩阵的话就是-1,但这种情况很少见)。那么上述代码的过程实际是输出了:
Output(x)= clip(x)/2 + clip(w)/2
Output(y)= clip(y)/2 + clip(w)/2
Output(z)= clip(z)
Output(w)= clip(w)
这里的xy并不是真正的视口空间下的坐标。因此,我们在片元着色器中再进行一步处理,即除以裁剪坐标的w分量。至此,完成整个映射的过程。因此,虽让ComputeScreenPos的函数名字似乎意味着会直接得到屏幕空间中的位置,但并不是这样的,我们仍需在片元着色器中除以它的w分量来得到真正的视口空间中的位置。那么,为什么Unity不直接在ComputeScreenPos中为我们进行除以w分量这个步骤呢?这是因为,如果Unity在顶点着色器中这么做的话,就会破坏插值的结果。我们知道,从顶点着色器到片元着色器的过程实际会有一个插值的过程(2.3.6)。如果不在顶点着色器中进行这个除法,保留x、y和
w分量,那么他们在插值后再进行这个除法,得到的x/w和y/w就是正确的(我们可以认为是除法抵消了插值的影响)。但如果我们直接在顶点着色器进行这个除法,那么就需要对x/w和y/w直接进行插值,这样得到的插值结果就会不准确。原因是,我们不可以在投影空间中进行插值,因为这并不是一个线性空间,而插值往往是线性的。
经过除法操作后,我们就可以得到该片元在视口空间中的坐标,也就是一个xy范围都在[0,1]之间的值。那么它的zw值是什么呢?可以看出,我们在顶点着色器中直接把裁剪空间的zw值存进了输出结构体,因此片元着色器输入的就是这些插值后的裁剪空间中的zw值。这意味着,如果使用的是透视投影,那么z值的范围是[-Near,Far],w值的范围是[Near,Far];如果使用的是正交投影,那么z值的范围是[-1,1],而w值恒为1。

4.10 扩展阅读
如果想要深入学习的好,书1、2是非常好的图形学数学学习资料。
1Fletcher Dunn,Ian Parberry.3D Math Promer for Fraphics and Game Development (2nd Edition).November 2, 2011 by A K Peters/CRC Press.
2Eric Lengyel.Mathematics for 3D game programming and computer grapics(3rd Enition).2011 by Charles River Media.
关于如何从左手坐标系转换到右手坐标系同时又保持视觉效果一样,可以参考资料3
3David Eberly.Conversion of Left-Handed Coordinates to Right-Handed Coordinates.
关于如何得到线性的深度值可以参考资料4
4http://www.humus.name/temp/Linearize%20depth.txt.

猜你喜欢

转载自blog.csdn.net/qq_39710961/article/details/80000103