Unity 渲染原理(六)Unity HLSL

在Unity中,我们使用HLSL的语法来写Shader Program,不过一开始Unity采用的是CG语法,因此会使用 Unity 某些关键字的名称 (CGPROGRAM) 和文件扩展名 (.cginc)。虽然Unity 不再使用 Cg,但这些名称仍在使用。

将 HLSL 代码放在 ShaderLab 代码中的代码块中。着色器程序通常如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Pass {
// ... 常规通道状态设置 ...

HLSLPROGRAM
// 此代码片段的编译指令,例如:
#pragma vertex vert
#pragma fragment frag

// 着色器程序本身

ENDHLSL

// ... 通道的剩余部分 ...
}

HLSL 语言有两种语法:旧版的 DirectX 9 样式语法以及更现代的 DirectX 10+ 样式语法。不同之处主要在于纹理采样函数的工作方式:

  • 旧版语法使用 sampler2D、tex2D() 和类似函数。此语法适用于所有平台。
  • DX10+ 语法使用 Texture2D、SamplerState 和 .Sample() 函数。由于纹理和采样器在 OpenGL 中不是不同对象,因此该语法的某些形式在 OpenGL 平台上无效。

HLSL中的预处理指令

在内部,着色器汇编具有多个阶段。第一阶段是预处理,其中一个名为“预处理程序”的程序准备编译代码。预处理器指令是预处理器的说明。

详情可以参考官方文档

着色器语义

在编写HLSL着色器程序时,输入和输出变量需要通过语义来表明其意图.

需要说明的一点是,变量的语义和变量的类型不同,比如float4 vertex : POSITION,这个vertex变量的类型是float4,但是它的语义是POSITION,表示这个变量代表的是点在裁剪空间下的坐标

顶点着色器输入语义

主顶点着色器函数(由 #pragma vertex 指令表示)需要在所有输入参数上都有语义。 这些对应于各个网格数据元素,如顶点位置、法线网格和纹理坐标。 有关更多详细信息,请参阅顶点程序输入。

以下是一个简单的顶点着色器的示例,它采用顶点位置 和纹理坐标作为输入。像素着色器 将纹理坐标可视化为颜色。

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
Shader "Unlit/Show UVs"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

struct v2f {
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};

v2f vert (
float4 vertex : POSITION, // 顶点位置输入
float2 uv : TEXCOORD0 // 第一个纹理坐标输入
)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.uv = uv;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return fixed4(i.uv, 0, 0);
}
ENDCG
}
}
}

片元着色器输出语义

通常,片元(像素)着色器会输出颜色,并具有 SV_Target 语义。上面示例中的片元着色器 完全就是这样的:

fixed4 frag (v2f i) : SV_Target
函数 frag 的返回类型为 fixed4(低精度 RGBA 颜色)。因为它只返回一个值,所以语义 由函数自身指示: SV_Target。

也可以返回包含输出的结构。 上面的片元着色器也可以按如下所示重写, 功能完全相同

1
2
3
4
5
6
7
8
9
struct fragOutput {
fixed4 color : SV_Target;
};
fragOutput frag (v2f i)
{
fragOutput o;
o.color = fixed4(i.uv, 0, 0);
return o;
}

从片元着色器返回结构对于不止返回单个颜色的 着色器非常有用。片元着色器 输出支持的其他语义如下。

SV_TargetN:多个渲染目标

SV_Target1、SV_Target2 等等:这些是着色器写入的附加颜色。这在一次渲染到多个渲染目标(称为“多渲染目标”渲染技术,简称 MRT)时使用。SV_Target0 等同于 SV_Target。

SV_Depth:像素着色器深度输出

通常情况下, 片元着色器不会覆盖 Z 缓冲区值,并使用 常规三角形栅格化中的默认值。但是, 对于某些效果,输出每个像素的自定义 Z 缓冲区深度值很有用。

请注意,在许多 GPU 上,这会关闭一些深度缓冲区优化,因此如果没有充分的理由,请不要覆盖 Z 缓冲区值。SV_Depth 产生的成本取决于 GPU 架构,但总体上与 Alpha 测试(使用 HLSL 中的内置 clip() 函数)的成本非常相似。通过渲染着色器在所有常规不透明着色器之后修改深度(例如,使用 AlphaTest 渲染队列)。

深度输出值必须为单个 float。

顶点着色器输出和片元着色器输入

顶点着色器需要输出顶点的最终裁剪空间位置,以便 GPU 知道屏幕上的栅格化位置以及深度。此输出需要具有 SV_POSITION 语义,并为 float4 类型。

顶点着色器生成的所有其他输出(“插值器”或“变化”)都是您的特定着色器需要的。从顶点着色器输出的值将在渲染三角形的面上进行插值,并且每个像素的值将作为输入传递给片元着色器。

许多现代 GPU 并不真正关心这些变量的语义;然而,一些旧系统(最主要的是 Direct3D 9 上的着色器模型 2 GPU)存在关于语义的特殊规则:

TEXCOORD0、TEXCOORD1 等语义用于指示任意高精度数据,如纹理坐标和位置。
顶点输出和片元输入的 COLOR0 和 COLOR1 语义用于低精度 0 到 1 范围的数据(如简单的颜色值)。
为了获得最佳的跨平台支持,应将顶点输出和 片元输入标记为 TEXCOORDn 语义。

插值器数量限制
对于总共可以使用多少个插值器变量将信息 从顶点传递到片元着色器,存在一些限制。该限制 取决于平台和 GPU,一般准则如下:

最多 8 个插值器:OpenGL ES 2.0 (Android)、Direct3D 11 9.x 级别 (Windows Phone) 和 Direct3D 9 着色器模型 2.0(老旧 PC)。由于插值器 数量受到限制,但每个插值器可以是一个 4 分量矢量, 所以一些着色器将内容打包在一起以便不会超过限制。例如,两个纹理 坐标可以在一个 float4 变量中传递(.xy 表示一个坐标,.zw 表示第二个坐标)。
最多 10 个插值器:Direct3D 9 着色器模型 3.0 (#pragma target 3.0)。
最多 16 个插值器:OpenGL ES 3.0 (Android) 和 Metal (iOS)。
最多 32 个插值器:Direct3D 10 着色器模型 4.0 (#pragma target 4.0)。
无论特定目标硬件如何,出于性能原因,通常最好使用尽可能少的插值器

其他特殊语义

  1. 屏幕空间像素位置:VPOS
    片元着色器可以接收渲染为特殊 VPOS 语义的像素的位置。 此功能仅从着色器模型 3.0 开始存在,因此着色器需要具有 #pragma target 3.0 编译指令。

在不同的平台上,屏幕空间位置输入的基础类型会有所不同,因此为了获得最大的可移植性,请对其使用 UNITY_VPOS_TYPE 类型(在大多数平台上将是 float4,在 Direct3D 9 上将是 float2)。

另外,使用像素位置语义将导致难以让裁剪空间位置 (SV_POSITION) 和 VPOS 处于相同的顶点到片元结构中。因此顶点着色器应将裁剪空间位置输出为单独的“out”变量。

  1. 面对方向:VFACE
    片元着色器可以接收一种指示渲染表面是面向摄像机还是背对摄像机的变量。这在渲染应从两侧可见的几何体时非常有用 - 通常用于树叶和类似的薄型物体。VFACE 语义输入变量将包含表示正面三角形的正值,以及表示背面三角形的负值。

此功能从着色器模型 3.0 开始才存在,因此着色器需要具有 #pragma target 3.0 编译指令。

  1. 顶点 ID:SV_VertexID
    顶点着色器可以接收具有“顶点编号”(为无符号整数)的变量。当您想要从纹理或 ComputeBuffers 中 获取额外的每顶点数据时,这非常有用。

此功能从 DX10(着色器模型 4.0)和 GLCore/OpenGL ES 3 开始才存在,因此着色器需要具有 #pragma target 3.5 编译指令。

如何使用着色器属性

着色器在 Properties 代码块中声明材质属性。如果要在着色器程序中访问其中一些属性,则需要声明具有相同名称和匹配类型的 Cg/HLSL 变量。

例如,以下着色器属性:

1
2
3
4
5
_MyColor ("Some Color", Color) = (1,1,1,1) 
_MyVector ("Some Vector", Vector) = (0,0,0,0)
_MyFloat ("My float", Float) = 0.5
_MyTexture ("Texture", 2D) = "white" {}
_MyCubemap ("Cubemap", CUBE) = "" {}

可通过如下 Cg/HLSL 代码进行声明以供访问:

1
2
3
4
5
fixed4 _MyColor; // 低精度类型对于颜色而言通常已经足够
float4 _MyVector;
float _MyFloat;
sampler2D _MyTexture;
samplerCUBE _MyCubemap;

Cg/HLSL 还可以接受 uniform 关键字,但该关键字并不是必需的:

1
uniform float4 _MyColor;

ShaderLab 中的属性类型以如下方式映射到 Cg/HLSL 变量类型:

  • Color 和 Vector 属性映射到 float4、half4 或 fixed4 变量。
  • Range 和 Float 属性映射到 float、half 或 fixed 变量。
  • 对于普通 (2D) 纹理,Texture 属性映射到 sampler2D 变量;立方体贴图 (Cubemap) 映射到 samplerCUBE__;3D 纹理映射到 sampler3D__。

如何向着色器提供属性值

在下列位置中查找着色器属性值并提供给着色器:

MaterialPropertyBlock 中设置的每渲染器值。这通常是“每实例”数据(例如,全部共享相同材质的许多对象的自定义着色颜色)。
在渲染的对象上使用的材质中设置的值。
全局着色器属性,通过 Unity 渲染代码自身设置(请参阅内置着色器变量),或通过您自己的脚本来设置(例如 Shader.SetGlobalTexture)。
优先顺序如上所述:每实例数据覆盖所有内容;然后使用材质数据;最后,如果这两个地方不存在着色器属性,则使用全局属性值。最终,如果在任何地方都没有定义着色器属性值,则将提供“默认值”(浮点数的默认值为零,颜色的默认值为黑色,纹理的默认值为空的白色纹理)。

序列化和运行时材质属性

材质可以同时包含序列化的属性值和运行时设置的属性值。

序列化的数据是在着色器的 Properties 代码块中定义的所有属性。通常,这些是需要存储在材质中的值,并且可由用户在材质检视面板中进行调整。

材质也可以具有着色器使用的一些属性,但不在着色器的 Properties 代码块中声明。通常,这适用于在运行时从脚本代码(例如,通过 Material.SetColor)设置的属性。请注意,矩阵和数组只能作为非序列化的运行时属性存在(因为无法在 Properties 代码块中定义它们)。

特殊纹理属性

对于设置为着色器/材质属性的每个纹理,Unity 还会在其他矢量属性中设置一些额外信息。

纹理平铺和偏移

材质通常具有其纹理属性的 Tiling 和 Offset 字段。此信息将传递到着色器中的 float4 {TextureName}_ST 属性:

x 包含 X 平铺值
y 包含 Y 平铺值
z 包含 X 偏移值
w 包含 Y 偏移值
例如,如果着色器包含名为 _MainTex 的纹理,则平铺信息将位于 _MainTex_ST 矢量中。

纹理大小

{TextureName}_TexelSize - float4 属性包含纹理大小信息:

x 包含 1.0/宽度
y 包含 1.0/高度
z 包含宽度
w 包含高度

纹理 HDR 参数

{TextureName}_HDR - 一个 float4 属性,其中包含有关如何根据所使用的颜色空间解码潜在 HDR(例如 RGBM 编码)纹理的信息。请参阅 UnityCG.cginc 着色器 include 文件中的 DecodeHDR 函数。

颜色空间和颜色/矢量着色器数据

使用线性颜色空间时,所有材质颜色属性均以 sRGB 颜色提供,但在传递到着色器时会转换为线性值

例如,如果 Properties 着色器代码块包含名为“MyColor“的 Color 属性,则相应的”MyColor”HLSL 变量将获得线性颜色值。

对于标记为 Float 或 Vector 类型的属性,默认情况下不进行颜色空间转换;而是假设它们包含非颜色数据。可为浮点/矢量属性添加 [Gamma] 特性,以表示它们是以 sRGB 空间指定,就像颜色一样

向顶点程序提供顶点数据

对于 Cg/HLSL 顶点程序, 网格顶点数据作为输入传递给顶点 着色器函数。每个输入都需要有指定的语义:例如,POSITION 输入表示顶点位置,NORMAL 表示顶点法线。

通常,顶点数据输入在结构中声明,而不是 逐个列出。在 UnityCG.cginc include 文件中 定义了几个常用的顶点结构,在大多数情况下, 仅使用它们就足够了。这些结构为:

  • appdata_base:位置、法线和一个纹理坐标。
  • appdata_tan:位置、切线、法线和一个纹理坐标。
  • appdata_full:位置、切线、法线、四个纹理坐标和颜色。

要访问不同的顶点数据,您需要自己声明 顶点结构,或者将输入参数添加到 顶点着色器。顶点数据由 Cg/HLSL 语义标识,并且必须来自 以下列表:

  • POSITION 是顶点位置,通常为 float3 或 float4。
  • NORMAL 是顶点法线,通常为 float3。
  • TEXCOORD0 是第一个 UV 坐标,通常为 float2、float3 或 float4。
  • TEXCOORD1、TEXCOORD2 和 TEXCOORD3 分别是第 2、第 3 和第 4 个 UV 坐标。
  • TANGENT 是切线矢量(用于法线贴图),通常为 float4。
  • COLOR 是每顶点颜色,通常为 float4。
    当网格数据包含的分量少于顶点着色器输入所需 的分量时,其余部分用零填充,但默认值为 1 的 .w 分量除外。例如,网格纹理坐标 通常是仅包含 x 和 y 分量的 2D 矢量。如果 顶点着色器使用 TEXCOORD0 语义声明一个 float4 输入,则 顶点着色器接收的值将包含 (x,y,0,1)。

内置的宏,变量,helper函数

Unity为我们内置了很多常用的宏,比如判断当前的平台,很多变量,如模型空间的转换矩阵,还有一些helper函数,如将模型坐标转换到世界坐标。

具体信息可以查看官方文档: