着色器数据类型和精度
Unity 中的标准着色器语言为 HLSL,支持一般 HLSL 数据类型。但是,Unity 对 HLSL 类型有一些补充,特别是为了在移动平台上提供更好的支持。
基本数据类型
着色器中的大多数计算是对浮点数(在 C# 等常规编程语言中为 float)进行的。浮点类型有几种变体:float、half 和 fixed(以及它们的矢量/矩阵变体,比如 half3 和 float4x4)。这些类型的精度不同(因此性能或功耗也不同):
高精度:float
最高精度浮点值;一般是 32 位(就像常规编程语言中的 float)。 完整的 float 精度通常用于世界空间位置、纹理坐标或涉及复杂函数(如三角函数或幂/取幂)的标量计算。
中等精度:half
中等精度浮点值;通常为 16 位(范围为 –60000 至 +60000,精度约为 3 位小数)。 半精度对于短矢量、方向、对象空间位置、高动态范围颜色非常有用。
低精度:fixed
最低精度的定点值。通常是 11 位,范围从 –2.0 到 +2.0,精度为 1/256。 固定精度对于常规颜色(通常存储在常规纹理中)以及对它们执行简单运算非常有用。
整数数据类型
整数(int 数据类型)通常用作循环计数器或数组索引。为此,它们通常可以在各种平台上正常工作。
根据平台的不同,GPU 可能不支持整数类型。例如,Direct3D 9 和 OpenGL ES 2.0 GPU 仅对浮点数据进行运算,并且可以使用相当复杂的浮点数学指令来模拟简单的整数表达式(涉及位运算或逻辑运算)。 Direct3D 11、OpenGL ES 3、Metal 和其他现代平台都对整数数据类型有适当的支持,因此使用位移位和位屏蔽可以按预期工作。
复合矢量/矩阵类型
HLSL 具有从基本类型创建的内置矢量和矩阵类型。例如,float3 是一个 3D 矢量,具有分量 .x、.y 和 .z,而 half4 是一个中等精度 4D 矢量,具有分量 .x、.y、.z 和 .w。或者,可使用 .r、.g、.b 和 .a 分量来对矢量编制索引,这在处理颜色时很有用。
矩阵类型以类似的方式构建;例如 float4x4 是一个 4x4 变换矩阵。请注意,某些平台仅支持方形矩阵,最主要的是 OpenGL ES 2.0。
纹理/采样器类型
通常按照如下方式在 HLSL 代码中声明纹理:
sampler2D _MainTex;
samplerCUBE _Cubemap;
对于移动平台,这些将转换为“低精度采样器”,即预期纹理应具有低精度数据。如果您知道纹理包含 HDR 颜色,则可能需要使用半精度采样器:
sampler2D_half _MainTex;
samplerCUBE_half _Cubemap;
或者,如果纹理包含完整浮点精度数据(例如深度纹理),请使用完整精度采样器:
sampler2D_float _MainTex;
samplerCUBE_float _Cubemap;
精度、硬件支持和性能
使用 float/half/fixed 数据类型的一个难题是:PC
GPU 始终为高精度。也就是说,对于所有 PC (Windows/Mac/Linux)
GPU,在着色器中编写 float、half 还是 fixed 数据类型都无关紧要。这些 GPU
将始终以 32 位浮点精度来计算所有数据。
仅当目标平台是移动端 GPU 时,half 和 fixed 类型才变得重要,在这种情况下,这些类型主要面临功耗(有时候是性能)约束。请记住,要确认是否遇到精度/数值问题,必须在移动设备上测试着色器。
即使在移动端 GPU 上,不同的精度支持也会因 GPU 产品系列而异。下面概述了个每个移动端 GPU 产品系列如何处理每个浮点类型(以用于该产品系列的位数来表示):
使用采样器状态
耦合的纹理和采样器
大多数情况下, 在着色器采样纹理时,纹理采样状态来自纹理设置;本质上。纹理和采样器耦合在一起。 使用 DX9 风格的着色器语法时,这是默认行为:
sampler2D _MainTex;
// ...
half4 color = tex2D(_MainTex, uv);
使用 HLSL 关键字 sampler2D、sampler3D 和 samplerCUBE 可声明纹理和采样器。 大部分情况下,这是您想要的结果,而且在较旧的图形 API (OpenGL ES) 中,这是唯一受支持的选项。
单独的纹理和采样器
很多图形 API 和 GPU 都允许使用的采样器数量少于纹理,而耦合的纹理+采样器语法可能不允许编写更复杂的着色器。例如,Direct3D 11 允许在单个着色器中最多使用 128 个纹理,但最多仅允许使用 16 个采样器。 Unity 允许使用 DX11 风格的 HLSL 语法来声明纹理和采样器,但需要通过一个特殊的命名约定来让它们匹配:名称为“sampler”+TextureName 格式的采样器将从该纹理中获取采样状态。 以上部分中的着色器代码片段可以用 DX11 风格的 HLSL 语法重写,并且也会执行相同的操作:
Texture2D _MainTex;
SamplerState sampler_MainTex; //"sampler"+"_MainTex"
// ...
half4 color = _MainTex.Sample(sampler_MainTex, uv);
但这样一来,就可以编写着色器来重复使用其他纹理中的采样器,同时采样多个纹理。在以下示例中,采样了三个纹理,但仅一个采样器用于所有这些纹理:
Texture2D _MainTex;
Texture2D _SecondTex;
Texture2D _ThirdTex;
SamplerState sampler_MainTex; //"sampler"+"_MainTex"
// ...
half4 color = _MainTex.Sample(sampler_MainTex, uv);
color += _SecondTex.Sample(sampler_MainTex, uv);
color += _ThirdTex.Sample(sampler_MainTex, uv);
但是请注意,DX11 风格的 HLSL 语法在某些较旧的平台(例如,OpenGL ES 2.0)上无效,请参阅着色语言以了解详细信息。您可能希望指定 #pragma target 3.5(请参阅着色器编译目标)以避免较旧的平台使用着色器。 Unity 提供了一些着色器宏帮助您使用这种“单独采样器”方法来声明和采样纹理,请参阅内置宏。以上示例可以采用所述的宏重写为下列形式:
UNITY_DECLARE_TEX2D(_MainTex);
UNITY_DECLARE_TEX2D_NOSAMPLER(_SecondTex);
UNITY_DECLARE_TEX2D_NOSAMPLER(_ThirdTex);
// ...
half4 color = UNITY_SAMPLE_TEX2D(_MainTex, uv);
color += UNITY_SAMPLE_TEX2D_SAMPLER(_SecondTex, _MainTex, uv);
color += UNITY_SAMPLE_TEX2D_SAMPLER(_ThirdTex, _MainTex, uv);
以上代码将在 Unity 支持的所有平台上进行编译,但会在 DX9 等旧平台上回退到使用三个采样器。
内联采样器状态
除了能识别名为“sampler”+TextureName 的 HLSL SamplerState 对象,Unity 还能识别采样器名称中的某些其他模式。这对于直接在着色器中声明简单硬编码采样状态很有用。例如:
Texture2D _MainTex;
SamplerState my_point_clamp_sampler;
// ...
half4 color = _MainTex.Sample(my_point_clamp_sampler, uv);
名称 “my_point_clamp_sampler”将被识别为应该使用点(距离最近)纹理过滤和钳制纹理包裹模式的采样器。
采样器名称被识别为“内联”采样器状态(全都不区分大小写):
“Point”、“Linear”或“Trilinear”(必需)设置纹理过滤模式。
“Clamp”、“Repeat”、“Mirror”或“MirrorOnce”(必需)设置纹理包裹模式。
可根据每个轴 (UVW) 来指定包裹模式,例如"ClampU_RepeatV"。
“Compare”(可选)设置用于深度比较的采样器;与 HLSL SamplerComparisonState 类型和 SampleCmp/SampleCmpLevelZero 函数配合使用。
以下是分别使用 sampler_linear_repeat 和 sampler_point_repeat 采样器状态进行纹理采样的示例,说明了如何通过名称控制过滤模式: