《Unity Shader 入门精要》学习总结01


渲染流水线

渲染流水线可以将各类数据进行计算来得到画面。

渲染流水线分为三个阶段:

应用阶段(Application Stage) → 几何阶段(Geometry Stage) → 光栅化阶段(Rasterizer Stage)

应用阶段的数据计算得到渲染图元,渲染图元输入几何阶段,输出屏幕空间的顶点信息并输入到光栅化阶段。

应用阶段输出的渲染图元进入几何阶段后,首先由顶点着色器(Vertex Shader)进行处理。

几何阶段末尾的图元装配、裁剪,以及光栅化阶段的三角形设置和遍历,属于固定功能流水线,只能进行少量配置。

光栅化阶段后便会开始使用片元着色器(Fragment Shader)为图元进行着色并合成,最后得到我们看到的画面。

应用阶段大部分由 CPU 负责,但是现在也有使用 GPU 生成几何数据传递到流水线中的技术。后续阶段由 GPU 负责。

我们写 shader 主要探讨的阶段便是 GPU 阶段,我们所做的只是从 CPU 端获取数据。

3个概念阶段

CPU与GPU通讯概述

一般情况下,渲染数据会从硬盘(HDD/SSD)加载到系统内存(RAM)中,然后再被加载到显存(VRAM)中。这也是为什么我们在玩一些贴图非常清晰的游戏时需要更大的显存。当然现在也有优化这一流程效率的技术,这里不过多概述。

加载完成后,CPU 会开始为数据设置渲染状态,指导 GPU 渲染。这些状态定义场景中的网格使用的着色器,以及各种其他所需数据。

接下来就是调用 Draw Call,显卡,启动!
接下来显卡便会根据设置好的内容开始渲染输出图像。

GPU流水线

渲染流水线详细概览图

流水线详细概览

当然实际情况中还会有诸如曲面细分着色器(Tessellation Shader)这类非必要的着色器存在,但并不是所有的图形 API 都会支持这个,所以这里我们只讨论普遍情况。

几何阶段(对应上图中的"顶点处理")

完全可编程,由顶点着色器(Vertex Shader)进行控制。一般的渲染流程中,我们会在这里对顶点进行空间变换,以及进行一些基于顶点的数据运算。

一般我们会在这里进行 MVP 变换,通过三个变换矩阵,将顶点由最初的物体空间,转换到裁剪空间中。

当然除了顶点数据我们还可以传递一些自定义数据到缓冲区中。

光栅化阶段(对应上图中的"光栅化操作")

这个阶段相对固定,只能进行一些功能上的配置。
例如配置剔除面(Cull Face)。

最后会进行三角形遍历,将网格数据转换为图元序列。

逐片元操作(对应概览图中的"片元处理")

拿到片元(图元)数据和其它来自顶点着色器的自定义数据之后,便会开始逐片元操作。第一步便是使用片元着色器(Fragment Shader)对片元进行着色操作。在这一步发生的光照着色我们也称为"逐像素光照"。

完成着色后,各个片元便需要进行一个类似"合成"的阶段,这个阶段中的操作同样固定,只能够进行简单的配置。


Draw Call

Draw Call 是一个重要的概念,尤其是在性能优化中。
前面说到过,Draw Call 会调用 GPU 以设置好的状态开始渲染。

CPU和GPU的并行工作

由于 GPU 的并行特性,CPU 和 GPU 可以并行工作。在内存中,我们会有一个命令缓冲区(Command Buffer)。其中包含一个命令队列,CPU 会向其中添加指令,而 GPU 会从中读取命令。

命令缓冲区

但是一旦提供太多的数据(会产生大量 Draw Call 和其他指令),CPU 便需要花费大量时间进行命令提交(DrawCall 的提交过程难以并行化)。

因此在 Unity 中会存在有 Batching(批处理)优化的技术,可以将多个待渲染但渲染状态相同的对象进行合并,作为一次 Draw Call 进行发送,从而大幅减少 Draw Call 的数量。


Unity Shader 基础

Unity Shader 概述

在 Unity 中,我们通过编写 Unity Shader、创建材质,来达到我们想要的渲染效果。

达成渲染效果

Unity Shader 中包含渲染所需的各种代码与指令,然后我们会在材质中调节这些属性,将外观赋予给模型。

Unity Shader 和我们通俗所说的 Shader 有所区别,其中并不只有着色器语言,还有一种名为 Shader Lab 的声明语言。Shader Lab 是 Unity 为我们提供的高级渲染抽象层,通过 Shader Lab,可以组织所有渲染所需的代码与配置项,以及材质面板的显示等。

Shader Lab 语言使用嵌套在花括号内的语义描述 Unity Shader 文件结构,其中包含所需的数据,其定义要实现一个材质所需的所有东西,而不仅仅是着色器代码。

Unity Shader结构

Shader "UnityShader入门精要/Chapter6/Blinn_Phong"
{
    Properties
    {
        _MainColor ("基础色" , Color) = (1.0, 1.0, 1.0, 1.0)
        _Gloss ("光泽度" , Range(8.0,256)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Pass
        {
            Name "FwdBase"
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            // ...着色器代码片段
            ENDCG
        }
    }

    Fallback "Specular"
}

名字

`Shader "UnityShader入门精要/Chapter6/Blinn_Phong"` 定义 Unity Shader 中的名字,这里设置好的名字会在材质面板中显示。

Properties

Properties 语义块,定义在材质面板中显示的属性。

属性类型

SubShader

SubShader

定义了一系列 Pass 和可选状态以及标签(Tags,键值对)设置。每个 Pass 定义一个完整的渲染流程,Pass 过多将会造成性能下降。

常见渲染状态设置项
标签类型

Pass

Pass

首先我们可以使用 Name 为 Pass 命名,这样不仅可以让我们在调试中获得好处,还可以使用 UsePass 来从其他 Unity Shader 中使用它。
但是注意,Unity 中会将所有 Pass 名转换为大写字母表示。

UsePass "MyShader/MYPASSNAME"

接下来可以进行 Pass 渲染状态设置,SubShader 中的状态设置同样适用于 Pass(Pass 中的 Tags 设置会覆盖掉 SubShader 中同样的配置项),Pass 也可以设置 Tags,但是不同于 SubShader 的 Tags。

Tags

Fallback

如果上面的着色器 Pass 都不能满足渲染需求,那么去指定的另一个着色器中寻找。

着色器代码片段

这是整个 Unity Shader 中的核心部分。
由两个全大写的标签包围,根据使用的着色器语言不同,这里的标签也不一样。
比如这里由 CGPROGRAM 和 ENDCG 包裹,因为在 BRP(内置渲染管线) 中,我们通常使用 CG 来进行 shader 编写。

下面是一段着色器代码片段的基本结构:

// ---- 编译指令 ----
#pragma vertex vert
#pragma fragment frag
// #pragma multi_compile_fog

// ---- 依赖库 ----
#include "UnityCG.cginc"
#include "Lighting.cginc"

// ---- 数据结构 ----
struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
};

struct v2f
{
    float2 uv : TEXCOORD0;
    // UNITY_FOG_COORDS(1)
    float4 vertex : SV_POSITION;
    float3 normal_WS : TEXCOORD1;
};

// ---- 属性声明 ----
// sampler2D _MainTex;
// float4 _MainTex_ST;
fixed4 _MainColor;

// ---- 顶点着色器 ----
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv ;

    //Normal Transform
    o.normal_WS = UnityObjectToWorldNormal(v.normal);

    //Compute diffuse term
    // UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

// ---- 片元着色器 ----
fixed4 frag (v2f i) : SV_Target
{
    // sample the texture
    // fixed4 col = tex2D(_MainTex, i.uv);
    // apply fog
    // UNITY_APPLY_FOG(i.fogCoord, col);

    //Get Light direction (World Space)
    float3 light_WS = normalize(_WorldSpaceLightPos0);

    //Get Ambient term
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

    i.normal_WS = normalize(i.normal_WS);
    float3 DiffuseColor = (_MainColor.xyz * _LightColor0.xyz) * (dot(i.normal_WS, light_WS) * 0.5 + 0.5);
    return fixed4(DiffuseColor , 0.0);
}