《Unity Shader 入门精要》总结01
《Unity Shader 入门精要》学习总结01
渲染流水线
渲染流水线可以将各类数据进行计算来得到画面。
渲染流水线分为三个阶段:
应用阶段(Application Stage) → 几何阶段(Geometry Stage) → 光栅化阶段(Rasterizer Stage)
应用阶段的数据计算得到渲染图元,渲染图元输入几何阶段,输出屏幕空间的顶点信息并输入到光栅化阶段。
应用阶段输出的渲染图元进入几何阶段后,首先由顶点着色器(Vertex Shader)进行处理。
几何阶段末尾的图元装配、裁剪,以及光栅化阶段的三角形设置和遍历,属于固定功能流水线,只能进行少量配置。
光栅化阶段后便会开始使用片元着色器(Fragment Shader)为图元进行着色并合成,最后得到我们看到的画面。
应用阶段大部分由 CPU 负责,但是现在也有使用 GPU 生成几何数据传递到流水线中的技术。后续阶段由 GPU 负责。
我们写 shader 主要探讨的阶段便是 GPU 阶段,我们所做的只是从 CPU 端获取数据。

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

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


Pass

首先我们可以使用 Name 为 Pass 命名,这样不仅可以让我们在调试中获得好处,还可以使用 UsePass 来从其他 Unity Shader 中使用它。
但是注意,Unity 中会将所有 Pass 名转换为大写字母表示。
UsePass "MyShader/MYPASSNAME"
接下来可以进行 Pass 渲染状态设置,SubShader 中的状态设置同样适用于 Pass(Pass 中的 Tags 设置会覆盖掉 SubShader 中同样的配置项),Pass 也可以设置 Tags,但是不同于 SubShader 的 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);
}



