本节摘要:PBR(基于物理的渲染)用两条硬约束替换 Blinn-Phong 的经验参数:能量守恒(反射总能量不超过入射能量)与菲涅尔效应(掠射角反射增强)。本节解释金属度粗糙度工作流的物理内涵,并给一份能直接编译的简化 PBR 片元着色器——参数虽少,却是现代引擎标准材质的内核。
Blinn-Phong 时代,材质参数(shininess、高光强度)全靠美术手调,换个光照环境整套参数重新来过。裂缝在两处:其一,参数没有任何物理含义,暗部与亮部的能量关系不受约束,"又亮又假"的画面比比皆是;其二,同一种材质在不同场景里表现不一致,资产复用成本高。
PBR 用两条物理事实收编乱局。能量守恒:出射光能量不得超过入射光能量,高光越强越窄,漫反射必须相应减弱——物体不可能既漫反射一片又镜面反射一片还更亮。菲涅尔效应:以掠射角看向表面时,反射率急剧上升(想想接近水平看湖面,倒影突然清晰),公式是一个简洁的近似:
vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); }
F0 是"正对表面时的基础反射率"。非金属的 F0 普遍在 0.04 附近(2.5% 到 5% 一档的反射),金属则可达 0.7 以上且带颜色——这条物理分界线,正是金属度参数的来源。
现代工作流只要求两张关键输入:albedo 贴图(本体色)与金属粗糙度贴图(金属度装 B 通道、粗糙度装 G 通道)。两个参数的语义:
金属度 metalness 是一个开关式的插值:0 表示非金属——漫反射承担主体,高光用 0.04 的灰白 F0;1 表示金属——漫反射归零(金属把入射光全部在表面反射掉,没有次表面染色),高光 F0 直接取本体色,所以黄金的高光是金色的。
粗糙度 roughness 描述表面微结构的凹凸尺度。物理图像是:真实表面布满肉眼难辨的微小镜面,粗糙度决定这些小镜面朝向的散布程度——越粗糙,高光越大越暗;越光滑,高光越小越锐。它把 Blinn-Phong 里 shininess 与 strength 两个游离参数统一成一个有物理含义的旋钮。

下面这份片元着色器实现了 Cook-Torrance 镜面项的三个因子:D(法线分布,决定高光形状)、G(几何遮蔽,微表面互相遮挡)、F(菲涅尔)。代码偏长但结构清晰,每一块都是标准形态:
#version 330 core in vec3 vNormal; in vec3 vWorldPos; in vec2 vUv; out vec4 FragColor; uniform vec3 uViewPos; uniform vec3 uLightDir; uniform vec3 uLightColor; uniform sampler2D uAlbedo; // 本体色贴图 uniform sampler2D uMetalRough; // G 通道粗糙度,B 通道金属度 const float PI = 3.14159265359; float D_GGX(float NdotH, float a) { // 法线分布:高光的形状 float a2 = a * a; float d = NdotH * NdotH * (a2 - 1.0) + 1.0; return a2 / (PI * d * d); } float G_Smith(float NdotV, float NdotL, float a) { // 几何遮蔽 float k = (a + 1.0) * (a + 1.0) / 8.0; float gv = NdotV / (NdotV * (1.0 - k) + k); float gl = NdotL / (NdotL * (1.0 - k) + k); return gv * gl; } vec3 F_Schlick(float cosT, vec3 F0) { // 菲涅尔 return F0 + (1.0 - F0) * pow(clamp(1.0 - cosT, 0.0, 1.0), 5.0); } void main() { vec3 albedo = texture(uAlbedo, vUv).rgb; float roughness = max(texture(uMetalRough, vUv).g, 0.045); float metalness = texture(uMetalRough, vUv).b; vec3 N = normalize(vNormal); vec3 V = normalize(uViewPos - vWorldPos); vec3 L = normalize(uLightDir); vec3 H = normalize(V + L); float NdotL = max(dot(N, L), 0.0); float NdotV = max(dot(N, V), 0.001); float NdotH = max(dot(N, H), 0.0); float HdotV = max(dot(H, V), 0.0); float a = roughness * roughness; // 平方映射,手感更线性 vec3 F0 = mix(vec3(0.04), albedo, metalness); // 非金属 0.04,金属吃本体色 vec3 F = F_Schlick(HdotV, F0); float D = D_GGX(NdotH, a); float G = G_Smith(NdotV, NdotL, a); vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001) * NdotL; vec3 kd = (vec3(1.0) - F) * (1.0 - metalness); // 能量守恒:高光吸走的,漫反射让出 vec3 diffuse = kd * albedo / PI * NdotL; vec3 color = (diffuse + specular) * uLightColor; color = color / (color + vec3(1.0)); // 临时 Reinhard 压制,第 7 章换正式版 FragColor = vec4(pow(color, vec3(1.0 / 2.2)), 1.0); // gamma 校正输出 }
三个工程细节点一下。粗糙度下限 0.045 防止完全光滑时 D 项爆炸成噪点;kd 的推导就是能量守恒的操作化——菲涅尔拿走多少进高光,漫反射就让出多少,金属再整体砍掉漫反射;末尾两行(色调压制与 gamma)是直出屏幕的临时方案,第 7 章会换成线性管线里的正式流程。引擎里这套内核不需要你手写——标准材质面板上那两个滑杆背后正是它,调参时知道物理含义,判断就不会飘。
💡 关键直觉:PBR 的价值不在"更像真的"这一句空话,而在参数可迁移——同一套金属度粗糙度贴图,在白天、夜晚、室内打光下都保持材质身份,因为能量关系是守恒的,不随光照环境穿帮。这是资产工业化的地基。
光照链完成。下一章跳出单物体视角:把整帧画面当素材,后处理登场。