Shader 的中文翻译是着色器,它的作用是,将场景中的物体经过一些列计算,最终呈现在屏幕上的一个个像素点。要学会编写 Unity Shader,需要一些图形学的前置基础知识,包括渲染管线、线性代数、一点编程经验。在 Unity 中,如果想要呈现绚丽的画面,我们就要学会写 Unity Shader,本篇主要讲述了 Unity Shader 中的一些关键基础概念。

一个单独的 Unity Shader 是无法发挥任何作用的,它必须和材质结合起来。Unity Shader 支持多种 shader 语言,但我们一般使用 CG 语言来写,即 C for Graphic。

ShaderLab

Unity 提供了一种专门为 Unity Shader 服务的语言——ShaderLab。在传统的编写着色器方式中,程序员往往要处理很多文件和设置,才能让画面呈现出想要的效果,比如载入模型、根据平台选择不同接口、设置着色器的输入、把资源加载到GPU、设置渲染状态、对渲染状态进行排序等等,而使用 ShaderLab 语言编写的 Unity Shader 可以完成这些所有的工作。

ShaderLab 是一种说明性语言,它使用了一些嵌套在花括号内部的语义(syntax)来描述一个 Unity Shader 文件的结构

Unity Shader 的结构

Unity Shader 里有多种语义,比如 Properties、SubShader、Fallback等。这些语义定义了 Unity Shader 的结构,从而帮助 Unity 分析该 Unity Shader 文件,以便进行正确的编译。

名字

每个 Unity Shader 文件的第一行都需要通过 Shader 语义来指定该 Unity Shader 的名字。这个名字由一个字符串来定义,例如“QwQ”。当为材质选择使用的 Unity Shader 时,这些名称就会出现在材质面板的下拉列表里。通过在字符串中添加斜杠“/”,可以控制 Unity Shader 在材质面板出现的位置。例如:

Shader "wuren/QwQ" { }

材质和 Unity Shader 的桥梁: Properties

在编写 Shader 时,我们往往需要一些“外来”的属性,比如场景中出现了两个球,我们想要它们使用同一种光照算法,那自然而然地,就会只写一个 Unity Shader,然后绑定到那两个球上。而如果我们想要两个球分别是红色和蓝色的,那么显然,“红”和“蓝”就要作为材质传给 Shader 的一个变量,然后 Shader 再根据这个变量进行一些列的运算。

从 C++、Python 那些高级编程语言的角度来看,我们可以把 Unity Shader 看成一个大函数,Properties 就是这个函数的传入形参,只不过在 Unity Shader 中我们将其称为属性Properties 语义块的定义通常如下:

Properties{
    Name ("display name", PropertyType) = DefaultValue
    Name ("display name", PropertyType) = DefaultValue
}

在上面的 Properties 语义块中,Name 指的是属性名,也是我们在 Shader 代码中使用的变量名;display name 指的是出现在材质面板上的名称,也就是说,一个属性其实有两个名,一个对内使用,一个对外作描述作用;PropertyType 指的是属性的类型,DefaultValue 指的是属性的默认值。

如果对应 C++ 的话,C++ 函数的1个传入形参有变量类型、变量名和默认值,就像 void print (int a = 1) 这样,那么 Properties 语义块的 PropertyType 就对应了变量类型,Name 对应了变量名,DefaultValue 对应了默认值。

下面是 Properties 语义块支持的属性类型:

属性类型默认值的定义语法
Intnumber
Floatnumber
Range(min,max)number
Color(number,number,number,number)
Vector(number,number,number,number)
2D"defaulttexture"{}
Cube"defaulttexture"{}
3D"defaulttexture"{}

IntFloatRange 是数字类型的属性,它们的值就是一个单独的数字;ColorVector的值是四维向量,也就是 4 个数字;2DCube3D 这 3 种是纹理类型,默认值是一个字符串后跟一个花括号,字符串要么是空的,要么是内置的纹理名称,如whitebump等,花括号本来用于指定纹理属性,但 Untiy 5.0 以后把这个功能移除了,可以视为残留的无用语法。

重量级成员: SubShader

从中文的角度来看的话,Shader 是着色器,SubShader 就是子着色器。一个 Unity Shader 可以有多个 Subshader,为什么呢?这是因为考虑不同平台、不同显卡的差异,只写一段 shader 代码可能无法在所有平台上运行。假如场景中有一个球,我们为它绑定了一个 Unity Shader 文件,这个文件包含多个 SubShader 语义块,则加载运行时,Unity 会扫描所有的 SubShader 语义块,然后选择第一个能够在目标平台上运行的 Subshader

所以,一个 SubShader 应当包含了一个材质的所有的渲染计算。一个 Unity Shader 文件即使包含多个 SubShader,渲染时也只有一个 SubShader 在执行。

SubShader 语义块中包含的定义通常如下:

SubShader {
    //可选的
    [Tags]

    //可选的
    [RenderSetup]

    Pass{
    }
    //其他的Pass
}

SubShader 中定义了一系列 Pass 以及可选的状态([RenderSetup])和标签([Tags])设置。状态指的是渲染状态,比如是否开启深度测试、是否开启混合模式等等,标签用来告诉 Unity 的渲染引擎——自己希望怎样以及何时渲染这个对象。每个 Pass 定义了一次完整的渲染流程

这个 Pass 又是什么意思呢,让我们设想一个场景,里面有多种光源,包括平行光、环境光、点光源、聚光灯等等,每种光源也可能有多个,而每个光源又有不同的属性(比如亮度),且都会对场景中的物体产生影响。显然,在渲染某个物体时,我们在 Shader 中一般没办法知道场景中究竟设置了多少光源,那么一种通俗的想法就是:依次计算每个光源对物体产生的影响,然后把所有光源的影响叠加起来。例如,在场景中有 A 光源和 B 光源,以及一个球,当我们要把这个球渲染到屏幕上时,对 A 光源运行一次球的 SubShader,得到球的颜色亮度是 0.1,对 B 光源也运行一次球的 SubShader,得到球的颜色亮度是 0.2,那么最后在屏幕上的球的颜色亮度就是 0.3(只是举例,不一定是这样算),也就是说,光源越多,球越亮,也符合我们的一般常识。

而且,一般来说,对于不同类型的光源,我们使用不同的计算方法。例如点光源需要考虑光源的位置,而平行光却不需要,那么 Unity 在渲染时,虽然它们都会运行同一个 SubShader,但会运行不同的 Pass 中的代码,这也就从一个角度理解了为什么 SubShader 中有多个 Pass 且每个 Pass 定义了一次完整的渲染流程。另外,在渲染时,对于面的正面和反面,我们也会采取不一样的计算方式,这也会分成不同的 Pass

总的来说,一个物体,或者说一个材质,在呈现到屏幕之前可能会经过多次渲染。每次渲染可能对应了不同的光源,可能对应了正反面,也可能是其他的不同情况。SubShader 中的一个 Pass 就是一次渲染,往往会根据情况的不同而选择特定的 Pass 来执行,而一种情况也可能对应多个 Pass (比如闲得无聊把漫反射和镜面反射分开到两个 Pass 中),每次渲染都能得到一个结果,最后把多个结果按照某种方式结合起来就是屏幕上呈现的画面。

Pass 语义块包含的语义如下:

Pass {
    [Name]
    [Tags]
    [RenderSetup]
}

可以定义 Pass 的名称,这样就能在别的 Shader 中直接引用,就像调用函数一样。Pass 中的标签 Tags 不同于 SubShader 中的标签,但也是用于告诉渲染引擎要怎样来渲染该物体。

留一条后路: Fallback

紧跟在各个 SubShader 语义块后面的,可以是一个 Fallback 指令。它是兜底用的,假如我们自己写的所有 SubShader 都在某块显卡上都不能运行,那就使用一个 Unity 内置的最低级的 Shader。语义如下:

Fallback "name"
//或者
Fallback Off

我们可以通过一个字符串来告诉 Unity 这个“最低级的 Unity Shader ”是谁,也可以任性地关闭 Falback 功能,但一旦这么做,意思大概就是:“如果一块显卡跑不了上面所有的 SubShader,那就不要管它了!”

结语

一开始没有打算写这篇基础知识,想直接从代码开始介绍,但是想到,Unity Shader 即使是 Helloworld 也非常复杂,不作解释的话基本看不懂,不像 C 语言那种两三行就写一个最简单的程序。书和文档的介绍过于详细了,于是我打算把 Helloworld 中会出现的东西先在本篇介绍一遍,然后再慢慢扩展。毕竟,万丈高楼平地起,磨刀不误砍柴工。
希望你喜欢这篇博客~