从UE4/Unity Shader代码反推:手把手拆解BRDF中的D、F、G函数(含GGX/Trowbridge-Reitz NDF详解)
在游戏开发与实时渲染领域,基于物理的渲染(PBR)已成为行业标准。而理解BRDF(双向反射分布函数)中的核心组件——法线分布函数(D)、几何函数(G)和菲涅尔项(F),是掌握PBR着色器编写的关键。本文将带您深入UE4和Unity的Shader源码,通过代码反推这些函数的实现细节,让理论公式真正落地到可操作的Shader编写中。
1. BRDF基础与代码视角
BRDF描述了光线与表面交互的方式,而D、F、G三个函数分别对应了微观表面法线分布、光线反射比例和几何遮挡效应。在UE4和Unity中,这些函数通常被封装在标准的PBR着色器中,我们可以通过反推引擎源码来理解其实现逻辑。
以Unity的Standard Shader为例,核心的BRDF计算通常位于UnityStandardBRDF.cginc文件中。以下是一个简化的BRDF结构:
half4 BRDF_Unity_PBS( half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, half3 normal, half3 viewDir, UnityLight light, UnityIndirect indirect ) { // D、F、G函数的调用通常在这里 half roughness = 1 - smoothness; half3 halfDir = normalize(light.dir + viewDir); // 法线分布函数(D) half D = DistributionGGX(normal, halfDir, roughness); // 几何函数(G) half G = GeometrySmith(normal, viewDir, light.dir, roughness); // 菲涅尔项(F) half3 F = FresnelSchlick(saturate(dot(halfDir, viewDir)), specColor); // 组合计算结果 half3 specular = (D * G * F) / (4 * max(dot(normal, viewDir), 0.1) * max(dot(normal, light.dir), 0.1)); // 最终颜色计算 half3 color = diffColor * (1 - F) * light.color * max(dot(normal, light.dir), 0) + specular; color += indirect.diffuse * diffColor; return half4(color, 1); }这个简化版本展示了BRDF计算的基本流程,其中D、F、G三个函数是核心。接下来我们将逐一拆解这些函数的实现细节。
2. 法线分布函数(D)的代码实现与GGX详解
法线分布函数(D)描述了表面微观几何的法线分布情况,直接影响高光反射的形状和锐利程度。在UE4和Unity中,GGX/Trowbridge-Reitz分布是最常用的模型。
2.1 GGX/Trowbridge-Reitz分布公式
GGX分布的数学公式为:
D(h) = α² / (π * ( (n·h)² * (α² - 1) + 1 )² )其中:
h是半角向量n是表面法线α是粗糙度参数(通常为roughness²)
在Unity的源码中,这个公式的实现通常如下:
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; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return a2 / max(denom, 0.0000001); // 避免除以零 }2.2 粗糙度参数的影响
粗糙度参数对高光效果有显著影响。在Shader中,我们通常会对粗糙度进行一些调整以获得更好的视觉效果:
// Unity中的典型粗糙度处理 roughness = max(roughness, 0.002); // 避免完全光滑的表面 float perceptualRoughness = roughness; float alpha = perceptualRoughness * perceptualRoughness;注意:在实际项目中,美术资源提供的粗糙度贴图值可能需要经过特定转换才能用于GGX计算。UE4中常用线性到平方的转换,而Unity则可能使用不同的映射方式。
2.3 不同引擎的实现差异
UE4和Unity在GGX实现上有些微差异:
| 特性 | UE4实现 | Unity实现 |
|---|---|---|
| 粗糙度映射 | LinearRoughness = Roughness² | perceptualRoughness = 1 - Smoothness |
| 最小粗糙度 | 通常0.045 | 通常0.002 |
| 能量守恒 | 更严格的能量补偿 | 较简单的实现 |
这些差异导致同样的材质参数在两个引擎中可能呈现不同的视觉效果,跨引擎项目需要特别注意。
3. 几何函数(G)的代码实现与优化
几何函数(G)描述了由于表面微观几何导致的阴影和遮蔽效应。在实时渲染中,Schlick-GGX近似是最常用的模型。
3.1 Schlick-GGX近似
几何函数的完整计算通常分为两部分:遮蔽(Shadowing)和掩蔽(Masking)。在代码中,我们首先定义一个辅助函数:
float GeometrySchlickGGX(float NdotV, float roughness) { float r = (roughness + 1.0); float k = (r * r) / 8.0; float denom = NdotV * (1.0 - k) + k; return NdotV / denom; }然后组合得到完整的几何函数:
float GeometrySmith(float3 N, float3 V, float3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float ggx1 = GeometrySchlickGGX(NdotV, roughness); float ggx2 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; }3.2 性能优化技巧
几何函数的计算相对昂贵,在实际项目中可以考虑以下优化:
近似计算:对于移动平台,可以使用更简单的近似:
float GeometryMobile(float NdotV, float NdotL, float roughness) { float k = roughness * roughness * 0.5; float GV = NdotV / (NdotV * (1.0 - k) + k); float GL = NdotL / (NdotL * (1.0 - k) + k); return GV * GL; }预计算:对于静态光源,部分计算结果可以预计算并存储在贴图中。
质量权衡:在远处或运动快速的物体上,可以使用更低精度的计算。
3.3 视觉影响分析
几何函数主要影响以下视觉效果:
- 粗糙表面的边缘暗化
- 掠射角度的反射强度
- 高光的能量分布
通过调整几何函数的实现,可以在性能和质量之间找到平衡点。例如,UE4的材质编辑器中就提供了多种几何函数的选项,以适应不同质量需求。
4. 菲涅尔项(F)的实现与材质表现
菲涅尔效应描述了光线在不同入射角度下的反射比例。在实时渲染中,Schlick近似是最常用的模型。
4.1 Schlick近似公式
Schlick近似的数学表达式为:
F(v,h) = F0 + (1 - F0) * (1 - (v·h))^5在Shader中的典型实现:
float3 FresnelSchlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }其中F0是基础反射率,对于非金属通常在0.02-0.05之间,金属则使用其albedo颜色。
4.2 金属工作流中的F0处理
在现代PBR管线中,金属工作流(Metallic Workflow)已成为标准。处理F0的典型代码如下:
float3 albedo = tex2D(_MainTex, uv).rgb; float metallic = tex2D(_MetallicGlossMap, uv).r; float smoothness = tex2D(_MetallicGlossMap, uv).a; float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);这里unity_ColorSpaceDielectricSpec是Unity预定义的电介质基础反射率(通常约0.04)。
4.3 菲涅尔效应的视觉表现
菲涅尔效应导致以下重要视觉效果:
- 掠射角度反射增强
- 金属与非金属的反射差异
- 边缘高光的形成
在UE4中,菲涅尔计算可能更加复杂,考虑了更多因素:
// UE4风格的菲涅尔计算 float3 FresnelUE(float3 SpecularColor, float VoH) { float3 Fc = pow(1 - VoH, 5); return saturate(50.0 * SpecularColor.g) * Fc + (1 - Fc) * SpecularColor; }这种实现增强了高光反射,使材质在特定条件下更加"闪亮",是UE4材质看起来与Unity有所不同的原因之一。
5. 完整BRDF组合与性能考量
将D、F、G三个函数组合起来,就形成了完整的BRDF计算。在实际Shader编写中,还需要考虑多种优化策略。
5.1 BRDF组合实现
一个完整的BRDF组合实现可能如下:
float3 BRDF(float3 N, float3 V, float3 L, float3 albedo, float metallic, float roughness) { float3 H = normalize(V + L); // 计算各分量 float D = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); float3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); // 计算镜面反射 float3 numerator = D * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); float3 specular = numerator / max(denominator, 0.001); // 计算漫反射 float3 kS = F; float3 kD = (1.0 - kS) * (1.0 - metallic); float3 diffuse = kD * albedo / PI; // 组合结果 return (diffuse + specular) * max(dot(N, L), 0.0); }5.2 性能优化实践
在真实项目中,BRDF计算需要考虑多种优化:
- 近似计算:对于远处物体或低端设备,可以使用简化版BRDF
- 预计算:部分计算结果可以烘焙到贴图中
- 分支优化:根据材质类型(金属/非金属)使用不同的计算路径
- 指令优化:合理安排计算顺序以减少Shader指令数
例如,移动平台友好的简化实现:
// 移动平台简化BRDF half3 BRDF_Mobile(half3 N, half3 V, half3 L, half3 albedo, half metallic, half roughness) { half3 H = normalize(V + L); half NdotV = max(dot(N, V), 0.0001); half NdotL = max(dot(N, L), 0.0001); half NdotH = max(dot(N, H), 0.0); half VdotH = max(dot(V, H), 0.0); // 简化D项 half a = roughness * roughness; half a2 = a * a; half denom = NdotH * NdotH * (a2 - 1.0) + 1.0; half D = a2 / (PI * denom * denom); // 简化G项 half k = a * 0.5; half GV = NdotV / (NdotV * (1.0 - k) + k); half GL = NdotL / (NdotL * (1.0 - k) + k); half G = GV * GL; // 简化F项 half3 F0 = lerp(0.04, albedo, metallic); half3 F = F0 + (1.0 - F0) * exp2((-5.55473 * VdotH - 6.98316) * VdotH); // 组合结果 half3 specular = (D * G) * F / (4.0 * NdotV * NdotL); half3 kD = (1.0 - F) * (1.0 - metallic); half3 diffuse = kD * albedo / PI; return (diffuse + specular) * NdotL; }5.3 跨引擎兼容性处理
在多引擎项目中,保持材质表现一致是一个挑战。以下是一些实用技巧:
粗糙度重映射:在不同引擎间转换粗糙度值
// Unity到UE4的粗糙度转换 float UE4Roughness = sqrt(1.0 - UnitySmoothness);反射率调整:统一基础反射率F0的定义
环境光处理:确保IBL计算方式一致
后处理协调:匹配Tonemapping和颜色空间
在实际项目中,通常会开发跨引擎的材质转换工具,自动处理这些差异。