Unity Shader笔记(一)

最近Unity ShaderLab学习中的一些知识点记录。包括数据精度选择,切线空间到世界空间的转换,卷积和卷积核等内容。

数据精度的选择

Shader中的绝大多数计算都是通过浮点数来完成的。其中包括几种不同的类型:floathalffixed(矢量和矩阵也有对应的类型如half3float4x4)。这些类型的精度、性能和能耗都有差异:

  • 高精度:float

    最高精度的浮点数,通常是32位。常用在世界空间的位置坐标、纹理坐标或涉及到复杂函数的标量计算中(如三角函数、幂运算、指数运算)。

  • 中等精度:half

    中等精度的浮点数,通常是16位(范围是-60000到+60000,精度大约在小数点后三位)。通常用于短的矢量、方向或物体空间的坐标及HDR的颜色。

  • 低精度:fixed

    最低精度的浮点数,通常11位。范围是-2.0到+2.0,精度为1/256。常用在普通的颜色(保存在普通纹理)及相关计算中。

  • 整数类型

    整数类型int常用于循环计数或数组索引。这种使用方式通常在跨平台时可以正常工作。但取决于平台,有的GPU不支持int类型。如Direct3D 9和OpenGL ES 2.0只能运算浮点数,因此整数表达式通常是使用复杂的浮点数学指令模拟出来。而Direct3D 11、OpenGL ES 3、Metal和其它的现代平台会提供对整数的支持,可以正常使用位移(bit shifts)和位掩码(bit masking)计算。

当使用Cg/HLSL写shader时,关于数据选择(floathalffixed)的一些策略:

为了达到好的性能,应当尽可能地选择最低精度。这对于移动平台(iOS和Android)尤为重要。以下是一些指导思路:

  • 对于世界位置坐标和纹理坐标,使用float精度
  • 其它的一切(矢量、HDR颜色等)可先使用half,如果需要的话再增加精度
  • 对于纹理数据的一些简单操作,使用fixed精度

在实践中,使用何种精度的数值,还依赖于平台和GPU。通常来讲:

  • 所有的现代桌面系统GPU总是会使用float精度来计算,所以使用float/half/fixed实际最终都会是一样的。这样就会使测试变得很困难,因为无法确认half/fixed是否可以真的满足要求,因此必须要在目标设备上进行测试以获得准确的结果。
  • 移动平台GPU实际上会使用half精度。这样通常更快,计算时能耗更少。
  • fixed精度通常用在旧的移动GPU上。大多数现代GPU(可运行OpenGL ES 3或Metal)内部会同等对待fixedhalf

切线空间到世界空间的转换

将点(矢量)从切线空间转换到世界空间时,需要用到TBN矩阵,即由切线、副切、法线组成的矩阵。变换公式如下:

$$ \begin{bmatrix} Tangent_{x} & Bitangent_{x} & Normal_{x} \\ Tangent_{y} & Bitangent_{y} & Normal_{y} \\ Tangent_{z} & Bitangent_{z} & Normal_{z} \end{bmatrix} * \begin{bmatrix} X_T \\ Y_T \\ Z_T \end{bmatrix} = \begin{bmatrix} X_W \\ Y_W \\ Z_W \end{bmatrix} $$

在ShaderLab中,需要定义三个3维矢量来传值,以下是官方的一个示例的片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
struct v2f {
float3 worldPos : TEXCOORD0;
// these three vectors will hold a 3x3 rotation matrix
// that transforms from tangent to world space
half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
// texture coordinate for the normal map
float2 uv : TEXCOORD4;
float4 pos : SV_POSITION;
};

// vertex shader now also needs a per-vertex tangent vector.
// in Unity tangents are 4D vectors, with the .w component used to
// indicate direction of the bitangent vector.
// we also need the texture coordinate.
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(_Object2World, vertex).xyz;
half3 wNormal = UnityObjectToWorldNormal(normal);
half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
// compute bitangent from cross product of normal and tangent
half tangentSign = tangent.w * unity_WorldTransformParams.w;
half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
// output the tangent space matrix
o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
o.uv = uv;
return o;
}

// normal map texture from shader properties
sampler2D _BumpMap;

fixed4 frag (v2f i) : SV_Target
{
// sample the normal map, and decode from the Unity encoding
half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
// transform normal from tangent to world space
half3 worldNormal;
worldNormal.x = dot(i.tspace0, tnormal);
worldNormal.y = dot(i.tspace1, tnormal);
worldNormal.z = dot(i.tspace2, tnormal);

// rest the same as in previous shader
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, worldNormal);
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
return c;
}

v2f中定义了三个half3用于存储TBN矩阵的三行,在frag中与切线空间的法线tnormal相乘得出世界空间的法线worldNormal

卷积和卷积核

卷积(convolution)是将图片以卷积核(kernel)为权重,把每个像素与其相邻的像素相加的过程。

卷积核通常是一个四方形网格结构(例如 2x2、3x3的方形区域),该区域内每个方格都有一个权重值。当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,翻转核之后再依次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到的结果就是该位置的新像素值。

常会遇到的两种应用是高斯模糊和边缘检测。

高斯模糊

高斯模糊使用的卷积核基于正态分布,距离核心越近其权重值越高,越远的权重值越低,其分布为标准正态分布:

$$ f(x,y)=\frac{1}{{2\pi}\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}} $$

例如,常用的一个5x5的高斯卷积核可近似表示为:

fig

处理一张尺寸为HxW的图片,使用5x5的高斯卷积核需要进行H*W*5*5次计算。而考虑到卷积核可分解为横向和纵向的两个单向的5阶矢量的乘积,为提高效率,高斯模糊通常可以使用纵向和横向的两次卷积(1x5的卷积核和5

x1的卷积核)。需要使用两个pass来绘制,并且需要额外存储第经过一个pass处理之后的纹理。但是复杂度从H*W*5*5变为HW\(5+5)。

边缘检测

边缘检测使用的算子(operator)常见的有三个:Roberts、Prewitt和Sobel:

fig

其中GxGy,分别用于计算横向和纵向的梯度,然后使用下边的公式得出总体的梯度。

$$ G = \sqrt{G_x^2 + G_y^2} $$

为减少开方计算提高效率也可以使用:

$$ G = \vert{G_x}\vert + \vert{G_y}\vert $$

获取相邻uv的方法

在ShaderLab中,获取相邻的像素,首先要使用_MainTex_TexelSize来取纹素的尺寸(如果是别的纹理也是_xxx_TexelSize这样的格式),使用纹理坐标(uv)进行偏移得到对应相邻位置的纹理坐标,进而得到对应位置的纹理。

1
2
3
// ...
// 当前uv的右上角相邻格子的uv存入uv[0]
o.uv[0] = i.texcoord + _MainTex_TexelSize.xy * half2(1,1);

REFERENCE

https://docs.unity3d.com/Manual/SL-DataTypesAndPrecision.html

https://docs.unity3d.com/Manual/SL-ShaderPerformance.html

https://docs.unity3d.com/Manual/SL-VertexFragmentShaderExamples.html

http://www.songho.ca/dsp/convolution/convolution.html

https://en.wikipedia.org/wiki/Kernel_(image_processing))

《Unity Shader入门精要》