PBR Cook-Torrance BRDF 渲染器

今天实现了基于物理的渲染(Physically Based Rendering, PBR)中最核心的 Cook-Torrance BRDF 模型,渲染出 5×4 材质球阵列,直观展示金属度(Metallic)和粗糙度(Roughness)对材质外观的影响。

项目目标

  • 实现完整的 Cook-Torrance BRDF 光照模型
  • 展示 PBR 的两个核心参数:金属度 × 粗糙度
  • ACES 色调映射 + sRGB Gamma 校正
  • 4 点光源 + 简单阴影

渲染结果

PBR 材质球阵列

  • 横轴(左→右):金属度 0.0 → 1.0
  • 纵轴(上→下):粗糙度 0.05 → 1.0
  • 材质颜色:金黄色(R:1.0, G:0.71, B:0.29)

理论基础

Cook-Torrance BRDF

PBR 核心公式:

1
f(l,v) = kd · (albedo/π) + ks · (D·G·F) / (4·(n·v)·(n·l))

其中三个镜面项分别是:

D - 法线分布函数(GGX/Trowbridge-Reitz)

描述微表面法线的统计分布,GGX 相比 Blinn-Phong 有更真实的高光拖尾:

1
2
3
4
5
6
7
8
double distributionGGX(double NdotH, double roughness) {
double a = roughness * roughness;
double a2 = a * a;
double NdotH2 = NdotH * NdotH;
double denom = NdotH2 * (a2 - 1.0) + 1.0;
denom = PI * denom * denom;
return a2 / std::max(denom, EPSILON);
}

G - 几何遮蔽函数(Smith’s + Schlick-GGX)

模拟微表面的自遮挡效果,粗糙表面自遮挡更严重:

1
2
3
4
5
double geometrySmith(double NdotV, double NdotL, double roughness) {
double ggx1 = geometrySchlickGGX(NdotV, roughness);
double ggx2 = geometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}

F - Fresnel 方程(Schlick 近似)

掠射角度反射更强,金属和非金属基础反射率不同:

1
2
3
4
Vec3 fresnelSchlick(double cosTheta, const Vec3& F0) {
double f = std::pow(std::max(1.0 - cosTheta, 0.0), 5.0);
return F0 + (Vec3(1,1,1) - F0) * f;
}

金属度的物理意义

  • 非金属(m=0):F0 = 0.04(4% 基础反射率),有漫反射(kD = 1 - kS)
  • 金属(m=1):F0 = albedo(用材质颜色作为反射率),无漫反射(kD = 0)

核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vec3 cookTorranceBRDF(
const Vec3& N, const Vec3& V, const Vec3& L,
const PBRMaterial& mat
) {
Vec3 H = (V + L).normalize(); // 半程向量

// F0: 基础反射率(非金属0.04,金属用albedo)
Vec3 F0(0.04, 0.04, 0.04);
F0 = F0 * (1.0 - mat.metallic) + mat.albedo * mat.metallic;

double D = distributionGGX(NdotH, mat.roughness);
double G = geometrySmith(NdotV, NdotL, mat.roughness);
Vec3 F = fresnelSchlick(HdotV, F0);

Vec3 specular = (F * D * G) / (4.0 * NdotV * NdotL + EPSILON);
Vec3 kD = (Vec3(1,1,1) - F) * (1.0 - mat.metallic);

return (kD * mat.albedo / PI + specular) * NdotL;
}

量化验证

球体位置 金属度 粗糙度 中心色 RGB 亮度
第1行-第1列 0.00 0.05 RGB(202,181,119) 167
第1行-第3列 0.50 0.05 RGB(113,104,68) 95
第1行-第5列 1.00 0.05 RGB(45,35,23) 34
第4行-第1列 0.00 1.00 RGB(195,172,111) 159
第4行-第5列 1.00 1.00 RGB(123,102,56) 93

物理规律验证:低粗糙度(0.05)金属球的中心亮度随金属度增大而显著降低(167→34),这是因为镜面高光极度集中,采样中心点不在高光处。高粗糙度(1.0)球的亮度变化更平缓(159→93),漫射光均匀分布。✅

技术总结

今天学到的关键点:

  1. PBR 的本质:统一的能量守恒框架,不是”看起来更好”,而是基于物理正确
  2. GGX vs Blinn-Phong:GGX 的高光拖尾更自然,不会突然截断
  3. kD 和 kS 的平衡kD = (1 - F) * (1 - metallic) 确保能量守恒
  4. F0 的重要性:非金属统一用 0.04,金属用 albedo 颜色,这个简化非常优雅
  5. ACES 色调映射:必须的步骤,不做的话高光会过爆

遇到的设计决策:

  • 阴影采样偏移 N * 0.001 避免自相交
  • roughness = max(roughness, 0.05) 避免除零(纯镜面在实践中也不存在)
  • 4 个光源使场景更有立体感

代码仓库

GitHub: https://github.com/chiuhoukazusa/daily-coding-practice/tree/main/2026/02/02-27-pbr-cook-torrance


完成时间: 2026-02-27 05:32
迭代次数: 1 次(一次编译成功)
渲染时间: ~0.2 秒(800×600,20 球体,4 光源)
编译器: g++ 12.3.1 (C++17)