PA3 The First Light
Shader 与渲染算法 - 表面与光照 - 光照模型与 PBR
1 Normals:法线可视化输出

1.1 缜密的想法 Meticulous Idea
structure 中添加法线相关内容;
vertex shader 中处理顶点法线,需要重新考虑法线坐标(原本在物体局部坐标系)和方向(跟着物体被缩放后需要重新计算),并且进行归一化处理;
fragment shader 将法线映射为颜色输出,法线插值后再次重新归一化处理。
1.2 CODING
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
struct VertexData {
float4 position: POSITION;
float3 normal: NORMAL;
};
struct Interpolators {
float4 position: SV_POSITION;
float3 normal: TEXCOORD0;
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.position = mul(unity_MatrixMVP, v.position);
i.normal = UnityObjectToWorldNormal(v.normal);
return i;
}
float4 Frag(Interpolators i): SV_Target {
i.normal = normalize(i.normal);
return float4(i.normal * 0.5 + 0.5, 1);
}
ENDCG
1.3 番外:Dynamic Batching
Dynamic Batching 是一种合并网格的批处理技术,可以降低 GPU 渲染压力。副作用是,会导致重新计算、不受控制的坐标结果。
按理来说,法线颜色由片元着色器制定,不会随着观察角度的变化而变化。
然而如果 Dynamic Batching 开启,法线会从原本物体的局部坐标变成网格合成(Dynamic Merge)后的世界坐标,从而法线颜色着色颜色也会随着观察角度不同而改变。

2 Diffuse:漫反射部分

2.1 前置知识
2.2 缜密的想法 Meticulous Idea
首先计算物体与光照交互模型(根据兰伯特余弦定律),即公式中 部分;
然后计算光线信息。原公式做为数学计算,主要考虑了光的强度即光线衰减 。这里对光的问题整体考虑,即光的强度与光源颜色;
最后计算物体表面信息,即 系数。
2.3 CODING
Shader "Code/PA3_FirstLight/Diffuse"
{
// 为了展示 Diffuse 的效果,增加了材质 _MainTex 和光源颜色 _Tint 及相关代码
Properties {
_Tint("Tint", Color) = (1, 1, 1, 1)
_MainTex("Albedo", 2D) = "white" {}
}
SubShader {
Pass {
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityStandardBRDF.cginc"
sampler2D _MainTex;
float4 _Tint, _MainTex_ST;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
float3 normal: NORMAL;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
float3 normal: TEXCOORD1;
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.position = mul(unity_MatrixMVP, v.position);
i.normal = UnityObjectToWorldNormal(v.normal);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
float4 Frag(Interpolators i): SV_Target {
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 lightColor = _LightColor0.rgb;
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
// 返回光照模型各项乘积
float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
return float4(diffuse, 1);
}
ENDCG
}
}
}
3 Specular:高光部分
3.1 前置知识
3.2 粗糙的想法 Rough Idea
首先解决高光与物体表面交互信息,即 部分。这里主要需要进行半程向量的计算;
定义光泽度 _Smoothness,即公式中指数 部分,控制高光锐度;
然后解决光线信息,即 部分;
最后定义物体表面信息,即公式中 系数;
解决能量守恒问题。
3.3 CODING
Shader "Code/PA3_FirstLight/Specular"
{
// Properties 新增 _SpecularTint 和 _Smoothness
Properties {
_Tint("Tint", Color) = (1, 1, 1, 1)
_MainTex("Albedo", 2D) = "white" {}
_SpecularTint("Specular", Color) = (0.5, 0.5, 0.5)
_Smoothness("Smoothness", Range(0, 1)) = 0.5
}
SubShader {
Pass {
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityStandardBRDF.cginc"
#include "UnityStandardUtils.cginc"
sampler2D _MainTex;
float4 _Tint, _MainTex_ST, _SpecularTint;
float _Smoothness;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
float3 normal: NORMAL;
};
// Interpolators 中定义 worldPos,将 mesh 顶点转换到世界坐标系
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
float3 normal: TEXCOORD1;
float3 worldPos: TEXCOORD2;
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.position = mul(unity_MatrixMVP, v.position);
i.worldPos = mul(unity_ObjectToWorld, v.position); // 物体顶点转换到世界坐标系
i.normal = UnityObjectToWorldNormal(v.normal);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
float4 Frag(Interpolators i): SV_Target {
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 lightColor = _LightColor0.rgb;
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
float oneMinusReflectivity;
albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo, _SpecularTint.rgb, oneMinusReflectivity);
// 半程向量公式:normalize(light direction + view direction)
// viewDir 是观察方向,来自观察点 - 物体顶点(两点相减得向量)
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos); // view director, 观察方向
float3 halfVector = normalize(lightDir + viewDir); // half vector, 半程向量
float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
float3 specular = _SpecularTint.rgb * lightColor * pow(DotClamped(halfVector, i.normal), _Smoothness * 100);
return float4(diffuse + specular, 1);
}
ENDCG
}
}
}
4 Metallic Workflow
根据两类材质(金属 & 非金属)的光照特点,可以有两种工作流(高光工作流 & 金属工作流)来进行处理。其中每种工作流都能满足两种材质的需求,只是各有侧重。
4.1 前置知识
4.2 缜密的想法 Meticulous Idea
所以工作流的切换关键是是 albedo 和 SpecularTint 的逻辑处理区别。
高光工作流的 _SpecularTint 会在 Properties 编辑器里就开始定义,也就是说,高光工作流的高光是单独设置的。而金属工作流的 SpecularTint 是在 fragment shader 里临时定义的,并且马上就 “剥夺” 了 albedo 的成果 —— 赋予其 albedo 与金属度 _Metallic 的乘积结果。
这就实现了金属材质的 idea:其材质主要表现为高光,而非金属材质则主要表现为漫反射。
Properties:移除高光(Specular Tint)并删除与高光相关的代码;添加金属度滑块(Metallic):新增一个
_Metallic
滑块,这是金属工作流的核心,用于在金属和非金属之间切换;重新进行 specular 与 albedo 的交互计算:
SpecularTint = albedo * _Metallic
albedo *= oneMinusReflectivity (即 1 - _Metallic)
// 关键逻辑(不使用工具函数 DiffuseAndSpecularFromMetallic 的情况)
float3 SpecularTint = albedo * _Metallic;
float oneMinusReflectivity = 1 - _Metallic;
// albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo, _SpecularTint.rgb, oneMinusReflectivity);
albedo *= oneMinusReflectivity;
4.3 CODING
Shader"Code/PA3_FirstLight/MetallicWorkflow"
{
Properties {
_Tint("Tint", Color) = (1, 1, 1, 1)
_MainTex("Albedo", 2D) = "white" {}
[Gamma] _Metallic("Metallic", Range(0, 1)) = 0 // 结构改动 (_SpecularTint -> _Metallic)
_Smoothness("Smoothness", Range(0, 1)) = 0.5
}
SubShader {
Pass {
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityStandardBRDF.cginc"
#include "UnityStandardUtils.cginc"
sampler2D _MainTex;
float4 _Tint, _MainTex_ST;
float _Smoothness, _Metallic;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
float3 normal: NORMAL;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
float3 normal: TEXCOORD1;
float3 worldPos: TEXCOORD2;
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.position = mul(unity_MatrixMVP, v.position);
i.worldPos = mul(unity_ObjectToWorld, v.position);
i.normal = UnityObjectToWorldNormal(v.normal);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
float4 Frag(Interpolators i): SV_Target {
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
float3 halfVector = normalize(lightDir + viewDir);
float3 lightColor = _LightColor0.rgb;
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
float3 SpecularTint;
float oneMinusReflectivity;
albedo = DiffuseAndSpecularFromMetallic(albedo, _Metallic, SpecularTint, oneMinusReflectivity);
float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
float3 specular = * lightColor * pow(DotClamped(halfVector, i.normal), _Smoothness * 100);
return float4(diffuse + specular, 1);
}
ENDCG
}
}
}
5 PBS:基于物理的光照模型

Blinn-Phong 模型长期以来一直是游戏工业中常用的光照模型。而现在则更多使用一种更为真实、可预测的模型 —— PBS(Physically-Based Shading,基于物理的光照模型。Unity 的标准 shader 就使用了 PBS 方法。
PBS (光照模型)通过 BRDF 函数(数学模型)来实现。
5.1 粗糙的想法 Rough Idea
PBS 算法通过一个 BRDF 函数宏(叫做
UNITY_BRDF_PBS
)实现,其定义在UnityPBSLighting
中;基于 Metallic Workflow 的代码,为该 BRDF 宏传入所需的八个参数:
前两个是 diffuse(漫反射)和 specular(高光)。前述代码已经实现了;
紧接着是 reflectivity(反射率)和 roughness(粗糙度)。为优化性能,这些系数需要以(1 减去它们)的形式给出。我们已经通过
DiffuseAndSpecularFromMetallic
得到了oneMinusReflectivity
,而 smoothness(光滑度)正好与 roughness 相反,因此可以直接用它们;第五和第六个参数是法线向量和观察方向向量 of course;
最后两个参数是 direct & indirect light(直射光和间接光)。这两个参数将传入一个 UnityLight / UnityIndirect 结构体;
因为 BRDF 函数返回一个 RGBA 颜色,其中 alpha 通道往往设置为 1。所以可以直接返回其结果。
5.2 CODING
// 以下 PBS 实现是基于 Metallic Workflow 的代码
Shader"Code/PA3_FirstLight/PBS"
{
Properties {
_Tint("Tint", Color) = (1, 1, 1, 1)
_MainTex("Albedo", 2D) = "white" {}
[Gamma] _Metallic("Metallic", Range(0, 1)) = 0
_Smoothness("Smoothness", Range(0, 1)) = 0.5
}
SubShader {
Pass {
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma target 3.0
#pragma vertex Vert
#pragma fragment Frag
// #include "UnityStandardBRDF.cginc"
// #include "UnityStandardUtils.cginc"
#define UNITY_PBS_USE_BRDF3
#include "UnityPBSLighting.cginc"
sampler2D _MainTex;
float4 _Tint, _MainTex_ST;
float _Smoothness, _Metallic;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
float3 normal: NORMAL;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
float3 normal: TEXCOORD1;
float3 worldPos: TEXCOORD2;
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.position = mul(unity_MatrixMVP, v.position);
i.worldPos = mul(unity_ObjectToWorld, v.position);
i.normal = UnityObjectToWorldNormal(v.normal);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
float4 Frag(Interpolators i): SV_Target {
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
// float3 halfVector = normalize(lightDir + viewDir); 不需要自行计算半程向量
float3 lightColor = _LightColor0.rgb;
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
float3 SpecularTint;
float oneMinusReflectivity;
albedo = DiffuseAndSpecularFromMetallic(albedo, _Metallic, SpecularTint, oneMinusReflectivity);
UnityLight light;
light.color = lightColor;
light.dir = lightDir;
light.ndotl = DotClamped(i.normal, lightDir);
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
return UNITY_BRDF_PBS(
albedo, SpecularTint,
oneMinusReflectivity, _Smoothness,
i.normal, viewDir,
light, indirectLight
);
// float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
// float3 specular = SpecularTint * lightColor * pow(DotClamped(halfVector, i.normal), _Smoothness * 100);
// return float4(diffuse + specular, 1);
}
ENDCG
}
}
}
APPENDIX
Anki
Reference
GAMES101
Blinn-Phong 模型部分 Lecture 7 - Shading1 (Illumination, Shading and Graphics Pipeline) Lecture 8 - Shading2 (Shading, Pipeline and Texture Mapping)
PBS 部分 Lecture 13 - Ray Tracing 1 (Whitted-Style Ray Tracing) Lecture 14 - Ray Tracing 2 (Acceleration & Radiometry) Lecture 15 - Ray Tracing 3 (Light Transport & Global Illumination)
Last updated