《Unity Shader入门精要》三、Unity Shader 基础

275 阅读8分钟

3.1 Unity Shader 概述

3.1.1 一对好兄弟:材质和 Unity Shader

在 Unity 中我们需要配合使用材质(Material)和 Unity Shader 才能达到需要的效果

流程:

  1. 创建一个材质
  2. 创建一个 Unity Shader,并把它赋给上面的材质
  3. 将材质赋给要渲染的对象
  4. 在材质面板中调整 Unity Shader 属性,以得到满意的效果
  • Unity Shader 定义了渲染所需的各种代码(各种着色器)、属性(纹理等)、和指令(渲染和标签设置等)

3.1.2 Unity 中的材质

  • Unity 中的材质需要结合一个 GameObject 的 Mesh 或者 Particla Systel 组件来工作
  • 与许多建模软件中提供的材质类似,它们都提供了一个面板来调整各个参数

3.1.3 Unity 中的 Shader

  • Unity Shader 与之前提到的渲染管线的 Shader 有很大不同

  • 创建方法(略)。Unity 提供了 5 种模板供选择

    image.png

    • Standard Surface:包含标准光照模型
    • Unlit:不包含光照(但包含雾效)
    • Image Effect:可实现各种屏幕后处理效果
    • Compute:可利用 GPU 的并行性来进行一些与常规渲染流水线无关的计算(用来挖矿?)
    • Ray Tracing:提供光线追踪相关的功能。(2019.3 版本首次实验性加入,在Unity 2022.2 LTS 后功能全面且稳定)
  • 必须与材质结合起来,才能发生神奇的“化学反应”

  • 对于表面着色器,它的属性面板中

    • 点击 Show generated code 可打开着色器文件,方便修改和研究
    • 若 Shader 是固定函数着色器,在 Fixed function 后面会有一个同样的按钮
    • Compile and show code 下拉列表可让开发者检查针对不同图像编程接口最终编译成的 Shader 代码

3.2 Unity Shader 的基础:ShaderLab

  • 学习和编写着色器的过程的曲线一直很陡峭,Unity 为了解决这个问题,提供了一层抽象——Unity Shader
  • 而我们和这层抽象打交道的方法就是使用专门为 Unity Shader 服务的语言——ShaderLab

什么是 ShaderLab?

image.png

ShaderLab 是 Unity 提供的编写 Unity Shader 的一种说明性语言

  • Unity 在背后会根据平台将 Unity Shader 代码编译成真正的代码和 Shader 文件,开发者只需要和 Unity Shader 打交道

3.3 Unity Shader 的结构

3.3.1 给 Shader 起名字(略)

3.3.2 材质和 Unity Shader 的桥梁:Properties

Properties 语义块定义通常如下:

Properties {
    Name {"display name", PropertyType} = DefaultValue
    Name {"display name", PropertyType} = DefaultValue
    // more...
}
  • 声明这些属性以便在材质属性面板中调整各种材质属性
  • Name:属性名字。若想在 Shader 中访问它们,就需要使用它
    • 通常由一个下划线开始
  • display name:显示的名称。材质面板上显示的名字
  • PropertyType:属性类型

image.png

  • DefaultValue:默认值

例子:

image.png

  • 若想显示更多类型的变量(如使用布尔值),Unity 允许我们重载默认的材质编辑面板
  • 即使不在 Properties 语义块中声明属性,也只可以直接在 CG 代码中定义变量。此时,我们可通过脚本向 Shader 中传递属性

3.3.3 重量级成员:SubShader

  • 每个 Unity Shader 文件中至少包含一个 SubShader 语义块
  • 当 Unity 需要加载这个 Unity Shader 时,它会扫描所有 SubShader 语义块,然后选择首个能在目标平台运行
  • 若都不支持,就会使用 Fallback 语义指定的 Unity Shader
  • 提供这种语义的原因在于,不同的显卡具有不同的能力。我们希望在旧的显卡上使用复杂度较低的着色器,而在高级的显卡上使用复杂度较高的

SubShader 中定义了一系列 Pass 以及可选的状态([RenderSetup])标签([Tags])设置

  • 每个 Pass 定义了一次完整的渲染流程(过多会影响性能)
  • 状态标签同样可在 Pass 中声明
    • 状态:跟 SubShader 中的语法是相同的
    • 标签:部分标签跟在 SubShader 中声明的不一样
  • 在 SubShader 中的设置(状态、标签)将会影响所有 Pass

状态设置

ShaderLab 提供了一系列渲染状态的设置指令,以设置显卡的各种状态(是否开启混合/深度测试等)

image.png

标签

一个键值对(都是字符串)。它们是 SubShader 和渲染引擎间的桥梁,用来告诉渲染引擎:SubShader 希望怎样以及何时渲染这个对象

标签结构:

Tags { "TagName1"="Value1" "TagName2"="Value2" }

SubShader 的标签块支持的标签类型

image.png

  • 注意:上述标签仅可以在 SubShader 中声明,而不可在 Pass 块中声明

Pass 语义块

语义块结构:

Pass {
    [Name]
    [Tags]
    [RenderSetup]
    // Other code
}
  • Name:该 Pass 的名称。通过这个名称,我们可以使用 ShaderLab 的 UsePass 命令来直接使用其他 Unity Shader 中的 Pass。例如:
UsePass "MyShader/MYPASSNAME"  // Pass 名称必须全部大写
  • 除了可在 Pass 中设置状态外,还可以使用 固定管线的着色器命令
  • 同样可在 Pass 中设置标签,但跟 SubShader 的标签不同,但目的一样——告诉渲染引擎我们(Pass)希望怎样来渲染物体。标签类型

image.png

  • 除了上面普通的 Pass 定义外,Unity Shader 还支持一些特殊 Pass,以便进行代码复用或实现更复杂的效果
    • UsePass:复用其他 Unity Shader 中的 Pass
    • GrabPass:抓取屏幕并将结果存储在一张纹理中,以用于后续的 Pass 处理

3.3.4 留一条后路:Fallback

“如果上面所有的 SubShader 在这块显卡上都不能运行,就使用这个最低级的 Shader 吧!”

语义:

Fallback "name"
// 或
Fallback Off
  • 事实上,Fallback 还会影响阴影投射。在渲染阴影纹理时,Unity 会在每个 Unity Shader 中寻找一个阴影投射的 Pass。通常情况下,我们不需要自己专门实现,这是因为 Fallback 使用的内置 Shader 中包含了一个通用的 Pass。因此为每个 Unity Shader 正确设置 Fallback 非常重要

3.3.5 ShaderLab 还有其他语义吗

  • 可使用 CustomEditor 语义来扩展编辑界面
  • 还可以使用 Category 语义来对 Unity Shader 中的命令进行分组

3.4 Unity Shader 的形式

我们可以使用下面 3 种形式来编写 Unity Shader,真正意义上的 Shader 代码都需要包含在 ShaderLab 语义块中

Shader "MyShader" {
    Properties {
        // 所需的各种属性
    }
    SubShader {
        // 真正意义上的 Shader 代码会出现在这里
        // 表面着色器(Surface Shader)或者
        // 顶点/片元着色器(Vertex/Fragment Shader)或者
        // 固定函数着色器(Fixed Funciton Shader)
    }
    SubShader {
        // 和上一个类似
    }
}

3.4.1 Unity 的宠儿:表面着色器

Unity 自己创造的一种着色器代码类型。需要的代码量少,但渲染代价较大

  • 本质上和顶点/片元着色器是一样的。Unity 在背后仍旧把它转换成对应的顶点/片元着色器
  • 表面着色器是 Unity 对顶点/片元着色器的更高一层抽象(帮我们处理很多光照细节)

一个简单的表面着色器代码:

Shader "Custom/Simple Surface Shader" {
    SubShader {
        Tags {"RenderType"="Opaque"}
        CGPROGRAM
        #pragma surface surf lambert
        struct Input {
            float4 color : COLOR;
        };
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = 1;
        }
        ENDCG
    }
    Fallback "Diffuse"
}
  • 表面着色器被定义在 SubShader 语义块中的 CGPROGRAM 和 ENDCG 之间。原因是,它不需要开发者关心使用多少个 Pass

3.4.2 最聪明的孩子:顶点/片元着色器

  • 更加复杂,但灵活性更高。示例代码:

image.png

  • 顶点/片元着色器的代码也需要定义在 CGPROGRAM 和 ENDCG 之间。但跟表面着色器不同,顶点/片元着色器是写在 Pass 语义块内,而非 SubShader 内

3.4.3 被抛弃的角落:固定函数着色器

  • 上面两种 Unity Shader 形式都使用了可编程管线,但有的旧设备并不支持,因此我们就需要使用固定函数着色器来完成渲染。示例:

image.png

  • 需要完全使用 ShaderLab 的语法来编写

3.4.4 选择哪种 Unity Shader 形式

  • 除非有非常明确的需要必须使用固定函数着色器,否则使用其余两种
  • 若想和各种光源打交道,则推荐使用表面着色器,但要小心它在移动平台的性能表现
  • 若需要使用的光照数非常少,例如只有一个平行光,那么使用顶点/片元着色器会更好
  • 若有很多自定义的渲染效果,请选择顶点/片元着色器

3.5 本书使用的 Unity Shader 形式

将着重使用顶点/片元着色器,知其然亦知其所以然

3.6 答疑解惑

3.6.1 Unity Shader != 真正的 Shader

Unity Shader 可以做的事情远多于一个传统意义上的 Shader

传统 ShaderUnity Shader
编码灵活性仅可编写特定类型的 Shader在同一个文件里同时包含多种着色器代码
可配置性无法设置一些渲染配置(混合、深度测试等)通过一行特定的指令来完成
可读性需要编写冗长的代码来设置着色器的输入和输出,并小心处理位置对应关系只需要在特定语句块中声明一些属性,便可通过材质来方便修改它们
对于模型数据需自行编码传给着色器提供了直接访问的方法

Unity Shader 的缺点:

  • 可编写的 Shader 类型(曲面细分/几何着色器等)和语法都被限制了

3.6.2 Unity Shader 和 CG/HLSL 之间的关系

3.6.3 我可以使用 GLSL 来写吗

可以,但意味着将放弃仅支持 DirectX 的平台

  • GLSL 代码需要嵌套在 GLSLPROGRAM 和 ENDGLSL 之间