Unity Shader 手写基于 PBR 的 URP Lit Shader 核心光照计算

深入解析 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)
相关推荐
小贺儿开发1 小时前
Unity3D 智能云端数字标牌系统
unity·阿里云·人机交互·视频·oss·广告·互动
魔士于安2 小时前
Unity windows 同步 异步 打开文件文件夹工具
游戏·unity·游戏引擎·贴图·模型
笑虾2 小时前
cocos2d-x lua 加载 Cocos Studio 导出的 csb
游戏引擎·lua·cocos2d
魔士于安2 小时前
unity lowpoly 风格 城市 建筑 道路 交通标志
游戏·unity·游戏引擎·贴图·模型
mxwin2 小时前
Unity GPU Shader 性能优化指南
unity·游戏引擎·shader
董董女友15 小时前
unity mcp 配置指南
unity·游戏引擎
垂葛酒肝汤20 小时前
Unity的可视化网格和文字标签
unity·游戏引擎
魔士于安20 小时前
Unity UI图片 复活节UI,卡通风格
游戏·ui·unity·游戏引擎·材质·贴图
weixin_4239950020 小时前
unity 团结开发小游戏,加载AssetBundles(第二种方法)
unity·游戏引擎