第二十章:物理基础渲染(PBR)入门:微表面模型,Cook-Torrance BRDF,IBL环境光照

好,咱们终于聊到PBR了。说实话,这是OpenGL进阶路上的一道分水岭。我记得自己刚接触PBR那会儿,看着满屏的公式和术语,心里直打鼓——这玩意儿也太数学了吧?但后来真正上手做项目才发现,PBR其实没那么玄乎。说白了,它就是用物理规律来模拟光怎么跟物体表面打交道。

这一章,我会带你拆解PBR的三个核心模块:微表面模型、Cook-Torrance BRDF、以及IBL环境光照。别怕公式,我会用我踩过的坑帮你理解。

本章核心知识体系

PBR 核心三要素 微表面模型 表面由无数微小镜面组成 粗糙度决定微表面法线分布 法线分布函数(NDF) 几何遮蔽函数(G) Cook-Torrance BRDF 漫反射 + 高光反射 菲涅尔效应(F) 法线分布(D) × 几何(G) × 菲涅尔(F) 分母 4(N·L)(N·V) IBL 环境光照 环境贴图采样 漫反射辐照度图 高光预滤波环境图 BRDF LUT 积分 最终渲染方程:Lo = ∫ f_r · Li · (n·ωi) dωi f_r = kd · (1/π) + ks · (D·G·F) / (4(N·L)(N·V)) 漫反射项 + 高光项(Cook-Torrance)

20.1 微表面模型:从宏观到微观的视角转换

先问个问题:为什么一张磨砂纸看起来是粗糙的,而一面镜子看起来是光滑的?

你可能会说「因为磨砂纸表面凹凸不平啊」。没错,但更准确的说法是——所有表面在微观尺度上都是凹凸不平的。区别只在于凹凸的尺度大小。

微表面模型的核心思想很简单:把物体表面想象成由无数个极其微小的完美镜面组成。每个微表面都有自己的法线方向。当光线打过来时,只有那些法线恰好对准了入射光和观察方向中间位置的微表面,才会产生高光反射。

我的经验:刚开始学微表面模型时,我总纠结「微表面到底有多小」。其实不用钻牛角尖,你只要知道它比像素还小就行了。在项目中,我们通常用粗糙度参数来控制微表面法线的分布范围——粗糙度越高,法线越散乱,高光就越模糊。

微表面模型涉及两个关键函数:

  • 法线分布函数(NDF):描述微表面法线朝向半向量的概率。常用的有GGX(Trowbridge-Reitz)模型。
  • 几何遮蔽函数(G):描述微表面之间互相遮挡光线的程度。粗糙表面更容易出现自遮挡。

这两个函数配合起来,就能模拟从镜面到磨砂的各种质感。

20.2 Cook-Torrance BRDF:PBR的黄金公式

好了,微表面模型给了我们理论基础,但怎么把它写成代码呢?这就轮到Cook-Torrance BRDF登场了。

Cook-Torrance BRDF把反射拆成两部分:

  1. 漫反射项:用Lambert模型,简单粗暴——kd / π。kd是漫反射颜色,除以π是为了能量守恒。
  2. 高光反射项:这才是重头戏,公式长这样:
// 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