深入解析 Cook-Torrance BRDF 各项含义,GetMainLight()、UniversalFragmentPBR() 调用方式,以及能量守恒原则
一、BRDF 基础概念
**BRDF(双向反射分布函数)**是描述光线如何从表面反射的数学模型。在真实感渲染中,BRDF 是连接物理光学与图形学的桥梁。
fr(ωi, ωr) = kDfd + kSfs
这个公式告诉我们:物体表面的反射 = 漫反射 + 镜面反射。两者需要满足能量守恒------反射的能量不能超过入射能量。

💡 核心问题: 如何精确计算光线从 L 方向入射,经过表面反射后,在 V 方向观察到的亮度?
二、Cook-Torrance BRDF 详解
Cook 和 Torrance 在 1982 年提出的 BRDF 模型至今仍是 PBR 的基石。其核心公式包含三个关键项:
fs = (D · G · F) / (4 · NdotV · NdotL)
D --- 法线分布函数
Normal Distribution Function
描述微表面法线与 H 的对齐程度
G --- 几何遮蔽函数
Geometry / Shadowing-Masking
描述微表面相互遮挡的程度
F --- 菲涅尔方程
Fresnel Equation (Schlick 近似)
描述不同视角下的反射率变化
2.1 D --- 法线分布函数 (GGX)
GGX 是目前最流行的法线分布函数,它模拟微表面法线的统计分布。 roughness 越高,分布越分散;roughness 越低,分布越集中。
D(H) = α² / [π · (NdotH² · (α² - 1) + 1)²]

直觉理解: 想象表面由无数微小的"镜子碎片"组成。D 函数描述这些碎片有多少正对着 H 方向。roughness 低时,碎片排列整齐,像一面大镜子;roughness 高时,碎片杂乱排列,光线散射开来。
2.2 G --- 几何遮蔽函数 (Smith)
由于微表面会相互遮挡,即使 D 函数计算某方向的分布很强,实际可见的比例也会降低。Smith 方法将 G 分解为两个独立函数:
G(N, V, L) = G1(N, V) · G1(N, L)
Smith 提出的 GGX 遮蔽函数:
G1(N, X) = 2 · NdotX / (NdotX + √(α² + (1-α²)·NdotX²))

2.3 F --- 菲涅尔方程 (Schlick 近似)
真实的金属表面在不同观察角度下,反射率会显著变化。Schlick 提出了简洁的近似公式:
F(NdotV) = F0 + (1 - F0) · (1 - NdotV)⁵
其中 F0 是法向入射时的基础反射率:
| 材质 | F₀ (基础反射率) |
|---|---|
| 水 | vec3(0.02, 0.02, 0.02) |
| 塑料/皮肤 | vec3(0.04, 0.04, 0.04) |
| 铝 | vec3(0.91, 0.92, 0.92) |
| 金 | vec3(1.00, 0.78, 0.34) |
| 铜 | vec3(0.95, 0.64, 0.54) |
| 铁 | vec3(0.56, 0.57, 0.58) |

三、能量守恒原则
能量守恒是 PBR 的核心约束之一:出射能量不能超过入射能量。这决定了漫反射和镜面反射如何分配能量。
kD + kS ≤ 1
其中:
kS= 镜面反射占比 = F(取决于视角)kD= 漫反射占比 = (1 - F) · (1 - metallic)
**⚠️ 重要约束:**金属表面没有漫反射!metallic = 1 时,kD = 0,所有能量都参与镜面反射。非金属才存在真正的漫反射。

四、URP 中 GetMainLight() 与 UniversalFragmentPBR() 的调用
Unity URP 提供了完整的光照计算管线,理解其调用方式对于手写自定义光照至关重要。
4.1 GetMainLight() --- 获取主光源
获取主光源
1void GetMainLight(out Light light, float3 positionWS, half shadowMask)
2{
3// 从 URP 光照管线获取主光源数据
4light = GetAdditionalLight(0, positionWS, shadowMask);
5}
Light 结构体包含:
direction--- 光照方向(从顶点指向光源)color--- 光照颜色与强度distanceAttenuation--- 距离衰减shadowAttenuation--- 阴影衰减
4.2 UniversalFragmentPBR() --- 完整 PBR 计算
cs
half4 UniversalFragmentPBR(VertexOutput input)
{
// Step 1: 准备数据
float2 ddxDdy = ddx_fdy(input.positionWS.xyz);
// Step 2: 构建 SurfaceData
SurfaceData surfaceData;
surfaceData.albedo = SAMPLE_TEXTURE2D(_BaseMap,
_BaseMap_ST, input.uv);
surfaceData.metallic = _Metallic;
surfaceData.smoothness = _Smoothness;
surfaceData.normalTS = UnpackNormal(input.normal);
// Step 3: 获取主光源
Light mainLight = GetMainLight(input.positionWS,
-1.0);
// Step 4: 调用 BRDF 计算
return UniversalBRDF(surfaceData, mainLight,
viewDirection, ...);
}
五、手写 PBR 光照计算完整实现
以下是一个精简但完整的 PBR 光照计算 Shader,涵盖了 Cook-Torrance BRDF 的所有核心要素:
cs
// ===== 属性声明 =====
Properties
{
_BaseColor("基础颜色", Color) = (1, 1, 1, 1)
_Metallic("金属度", Range(0, 1)) = 0.0
_Smoothness("光滑度", Range(0, 1)) = 0.5
}
// ===== HLSL 头文件 =====
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/"
"ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/"
"ShaderLibrary/Lighting.hlsl"
// ===== CBUFFER =====
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float _Metallic;
float _Smoothness;
CBUFFER_END
// ===== 顶点着色器 =====
float4 VertexShader(Attributes input) : SV_POSITION
{
Varyings output;
float3 positionWS = TransformObjectToWorld(input.position);
output.positionWS = positionWS;
output.positionCS = TransformWorldToHClip(positionWS);
output.normalWS = TransformObjectToWorldNormal(input.normal);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return output.positionCS;
}
cs
// ============================================
// D 项:GGX 法线分布函数
// ============================================
float DistributionGGX(float3 N, float3 H, float roughness)
{
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
// GGX 分布公式
float nom = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = 3.141592 * denom * denom; // π 归一化因子
return nom / denom;
}
// ============================================
// G 项:Smith 几何遮蔽函数
// ============================================
float GeometrySmith(float3 N, float3 V,
float3 L, float roughness)
{
float r = roughness + 1.0; // 平方修正
float k = (r * r) / 8.0;
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx1 = NdotV / (NdotV * (1.0 - k) + k);
float ggx2 = NdotL / (NdotL * (1.0 - k) + k);
return ggx1 * ggx2; // 视角遮蔽 × 光照遮蔽
}
// ============================================
// F 项:Schlick 菲涅尔近似
// ============================================
float3 FresnelSchlick(float cosTheta, float3 F0)
{
return F0 + (float3)(1.0 - F0) *
pow(1.0 - cosTheta, 5.0);
}
// ============================================
// 主光照计算函数
// ============================================
float3 CalculatePBRLighting(
float3 albedo,
float metallic,
float smoothness,
float3 N, // 法线
float3 V, // 视角方向
float3 L, // 光照方向
float3 lightColor) // 光照颜色
{
// ===== 1. 准备参数 =====
float roughness = 1.0 - smoothness;
float3 H = normalize(V + L); // 半程向量
// ===== 2. 计算 F0 =====
float3 F0 = 0.04; // 非金属基础反射率
F0 = lerp(F0, albedo, metallic);
// ===== 3. 计算三项 BRDF =====
float NDF = DistributionGGX(N, H, roughness); // D 项
float G = GeometrySmith(N, V, L, roughness); // G 项
float NdotV = max(dot(N, V), 0.0001);
float NdotL = max(dot(N, L), 0.0001);
float3 F = FresnelSchlick(dot(N, H), F0); // F 项
// ===== 4. Cook-Torrance BRDF 公式 =====
float3 numerator = NDF * G * F;
float denominator = 4.0 * NdotV * NdotL + 0.0001;
float3 specular = numerator / denominator;
// ===== 5. 能量守恒:漫反射 = (1-F) * (1-金属度) =====
float3 kD = (float3)(1.0 - specular); // 剩余能量
kD *= (1.0 - metallic); // 金属无漫反射
// ===== 6. 最终光照 =====
float3 diffuse = kD * albedo / 3.141592;
return (diffuse + specular) * lightColor * NdotL;
}
六、片段着色器中调用主光源
cs
float4 FragmentShader(Varyings input) : SV_Target
{
// ===== 采样贴图 =====
float4 albedoTex = SAMPLE_TEXTURE2D(_BaseMap,
_BaseMap_ST, input.uv);
float3 albedo = albedoTex.rgb * _BaseColor.rgb;
// ===== 准备向量 =====
float3 N = normalize(input.normalWS);
float3 V = normalize(_WorldSpaceCameraPos - input.positionWS);
// ===== 获取主光源 =====
Light mainLight = GetMainLight(input.positionWS);
// 光照方向从光源指向表面(与 L 相反)
float3 L = normalize(mainLight.direction);
// ===== 计算 PBR 光照 =====
float3 color = CalculatePBRLighting(
albedo,
_Metallic,
_Smoothness,
N, V, L,
mainLight.color // 包含强度衰减
);
// ===== 添加环境光 =====
float3 ambient = SampleSH(N) * albedo;
color += ambient;
// ===== 色调映射 & 伽马校正 =====
color = NeutralToneMapping(color);
color = LinearToSRGB(color);
return float4(color, 1.0);
}
核心要点总结
- BRDF = 漫反射 + 镜面反射,必须满足
kD + kS ≤ 1 - D (GGX) = 描述微表面法线与半程向量 H 的对齐程度 → 高光形状
- G (Smith) = 描述微表面之间的相互遮蔽 → 避免过度高光
- F (Schlick) = 描述掠射角反射率增加的现象 → 金属感
- 能量守恒 = 漫反射分配剩余能量,金属无漫反射
- GetMainLight() = 从 URP 获取主光源(方向、颜色、衰减)
- Cook-Torrance =
(D·G·F) / (4·NdotV·NdotL)