着色器数据类型和精度

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 采样器状态进行纹理采样的示例,说明了如何通过名称控制过滤模式:


文章作者: Yonggang Long
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yonggang Long !
 上一篇
2022-08-10 Yonggang Long
下一篇 
2022-08-10 Yonggang Long
  目录