第二十章:物理基础渲染(PBR)入门:微表面模型,Cook-Torrance BRDF,IBL环境光照
好,咱们终于聊到PBR了。说实话,这是OpenGL进阶路上的一道分水岭。我记得自己刚接触PBR那会儿,看着满屏的公式和术语,心里直打鼓——这玩意儿也太数学了吧?但后来真正上手做项目才发现,PBR其实没那么玄乎。说白了,它就是用物理规律来模拟光怎么跟物体表面打交道。
这一章,我会带你拆解PBR的三个核心模块:微表面模型、Cook-Torrance BRDF、以及IBL环境光照。别怕公式,我会用我踩过的坑帮你理解。
本章核心知识体系
20.1 微表面模型:从宏观到微观的视角转换
先问个问题:为什么一张磨砂纸看起来是粗糙的,而一面镜子看起来是光滑的?
你可能会说「因为磨砂纸表面凹凸不平啊」。没错,但更准确的说法是——所有表面在微观尺度上都是凹凸不平的。区别只在于凹凸的尺度大小。
微表面模型的核心思想很简单:把物体表面想象成由无数个极其微小的完美镜面组成。每个微表面都有自己的法线方向。当光线打过来时,只有那些法线恰好对准了入射光和观察方向中间位置的微表面,才会产生高光反射。
我的经验:刚开始学微表面模型时,我总纠结「微表面到底有多小」。其实不用钻牛角尖,你只要知道它比像素还小就行了。在项目中,我们通常用粗糙度参数来控制微表面法线的分布范围——粗糙度越高,法线越散乱,高光就越模糊。
微表面模型涉及两个关键函数:
- 法线分布函数(NDF):描述微表面法线朝向半向量的概率。常用的有GGX(Trowbridge-Reitz)模型。
- 几何遮蔽函数(G):描述微表面之间互相遮挡光线的程度。粗糙表面更容易出现自遮挡。
这两个函数配合起来,就能模拟从镜面到磨砂的各种质感。
20.2 Cook-Torrance BRDF:PBR的黄金公式
好了,微表面模型给了我们理论基础,但怎么把它写成代码呢?这就轮到Cook-Torrance BRDF登场了。
Cook-Torrance BRDF把反射拆成两部分:
- 漫反射项:用Lambert模型,简单粗暴——
kd / π。kd是漫反射颜色,除以π是为了能量守恒。 - 高光反射项:这才是重头戏,公式长这样:
// Cook-Torrance 高光项
vec3 CookTorranceSpecular(vec3 N, vec3 L, vec3 V, vec3 H, float roughness, vec3 F0) {
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = FresnelSchlick(cosTheta, F0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001;
return numerator / denominator;
}
这个公式里三个主角:
- D(法线分布):我用的是GGX,效果比Blinn-Phong好太多。粗糙度0.01时几乎就是镜面,0.5时像磨砂金属。
- G(几何遮蔽):Smith方法配合Schlick-GGX近似。我曾经在调金属材质时忽略了G项,结果高光边缘亮得离谱——后来才发现是自遮挡没算对。
- F(菲涅尔效应):Schlick近似。记住一个规律——所有材质在掠射角(视线几乎平行于表面)时反射率都会接近1.0。
注意:分母加0.0001是为了防止除零。我见过有人直接写4.0 * NdotV * NdotL,结果在边缘角度出现黑点。这种坑,踩一次就记住了。
金属材质和绝缘体材质的区别,主要体现在F0上:
| 材质类型 | F0(法线方向反射率) | 示例 |
|---|---|---|
| 绝缘体(塑料、木头) | 0.04(灰度值) | 几乎不反射,漫反射为主 |
| 金属(铁、金) | 0.5~1.0(RGB值) | 高反射,带金属色 |
| 半导体(铬) | 0.2~0.4 | 介于两者之间 |
我个人习惯把F0做成可调参数,这样美术同学可以自由调整金属质感。
20.3 IBL环境光照:让物体融入场景
有了BRDF,我们可以在点光源下渲染出漂亮的PBR材质了。但真实世界的光照来自四面八方——天空、墙壁、其他物体反射的光。这就是IBL(基于图像的光照)要解决的问题。
IBL的核心思路:用一张环境贴图(HDR全景图)来代表场景中的所有光源。然后对每个像素,积分所有方向上的入射光。
直接积分太慢了,实时渲染扛不住。所以我们需要预处理:
- 漫反射部分:生成辐照度图(Irradiance Map)。对每个法线方向,卷积周围所有方向的光照。说白了就是模糊一下环境图。
- 高光部分:生成预滤波环境图(Prefiltered Environment Map)。对不同粗糙度分别模糊,存成mipmap。
- BRDF LUT:生成一张2D查找表,存的是菲涅尔和几何遮蔽的积分结果。这张图跟粗糙度和视角有关。
在Shader里,IBL的采样代码大致是这样:
// 漫反射IBL
vec3 diffuseIBL = texture(irradianceMap, N).rgb * albedo;
// 高光IBL
vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
vec2 brdfLUT = texture(brdfLUTMap, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specularIBL = prefilteredColor * (F * brdfLUT.x + brdfLUT.y);
// 最终颜色
vec3 color = diffuseIBL + specularIBL;
避坑指南:我曾经在生成预滤波环境图时,忘了对粗糙度做平方处理。结果高光看起来总是不对劲——粗糙度0.2的效果跟0.4差不多。后来查资料才发现,GGX的粗糙度参数需要映射到roughness²才能得到正确的模糊效果。
IBL的好处是显而易见的——物体能自动反射周围环境,看起来就像真的放在那个场景里。金属球反射出窗户的轮廓,塑料球有柔和的漫反射光晕。这种真实感,是传统光照模型给不了的。
20.4 把一切串起来:完整的PBR Shader
最后,咱们把上面所有东西整合到一个Fragment Shader里。嗯,这才是实战的样子:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;
uniform vec3 albedo;
uniform float metallic;
uniform float roughness;
uniform vec3 camPos;
uniform samplerCube irradianceMap;
uniform samplerCube prefilterMap;
uniform sampler2D brdfLUT;
void main() {
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
vec3 R = reflect(-V, N);
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
vec3 kS = F;
vec3 kD = (1.0 - kS) * (1.0 - metallic);
// 漫反射IBL
vec3 diffuse = texture(irradianceMap, N).rgb * albedo;
// 高光IBL
vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * 4.0).rgb;
vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specular = prefilteredColor * (F * brdf.x + brdf.y);
vec3 color = kD * diffuse + specular;
color = color / (color + vec3(1.0)); // Reinhard色调映射
color = pow(color, vec3(1.0/2.2)); // Gamma校正
FragColor = vec4(color, 1.0);
}
这段代码里,mix(vec3(0.04), albedo, metallic)是我个人很喜欢的写法——绝缘体F0固定0.04,金属则用albedo作为F0。这样美术只需要调albedo和metallic两个参数,就能覆盖从塑料到黄金的所有材质。
关键点回顾:
- 微表面模型把表面拆成无数小镜面,粗糙度控制法线分布
- Cook-Torrance BRDF = 漫反射 + D·G·F / (4·NdotL·NdotV)
- IBL用环境贴图预计算漫反射和高光,实现全局光照效果
- 金属材质F0 = albedo,绝缘体F0 = 0.04
PBR这东西,上手确实有点门槛。但一旦你理解了微表面模型和Cook-Torrance公式背后的物理直觉,剩下的就是调参和优化了。我当初花了两周才把第一个PBR场景跑通,但看到金属球反射出环境贴图的那一刻——值了。
公众号:蓝海资料掘金营,微信deep3321