《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 中的状态设置同样适用于 PassPass 中的 Tags 设置会覆盖掉 SubShader 中同样的配置项),Pass 也可以设置 Tags,但是不同于 SubShaderTags

Tags

Fallback

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

着色器代码片段

这是整个 Unity Shader 中的核心部分。
由两个全大写的标签包围,根据使用的着色器语言不同,这里的标签也不一样。
比如这里由 CGPROGRAMENDCG 包裹,因为在 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);
}