上一篇我们搭建了基础环境并实现了简单的自定义材质,本篇将基于完整代码,深入解析 PBR 的核心参数、光照模型的实现逻辑,以及如何通过代码将物理规律转化为视觉效果。
一、核心参数解析:从代码看 PBR 的物理基础
PlayCanvas 风格的 PBR 材质通过一组具有明确物理意义的参数描述材质特性,这些参数在代码中通过uniform
变量传递,并在着色器中参与光照计算。
1. 基础参数与代码映射
参数名 | 代码变量 | 类型 | 范围 | 物理意义 |
---|---|---|---|---|
基础色 | albedo |
vec3 |
RGB 颜色 | 物体表面的固有颜色,非金属为漫反射色,金属为反射色 |
金属度 | metalness |
float |
0~1 | 区分金属(1)与非金属(0),控制漫反射 / 镜面反射比例 |
粗糙度 | roughness |
float |
0~1 | 控制表面光滑度,0 = 镜面,1 = 完全粗糙 |
折射率 | ior |
float |
1.0+ | 决定光线折射 / 反射比例,计算基础反射率(F0) |
镜面反射率 | specularity |
vec3 |
RGB 颜色 | 控制非金属的高光强度,金属材质忽略此参数 |
环境反射强度 | envMapReflectivity |
float |
0~1 | 控制环境贴图的反射强度 |
2. 金属度(Metalness)的核心作用
金属与非金属的光学特性差异是 PBR 的核心,代码通过calcSpecularModulate
函数实现这种差异:
vec3 calcSpecularModulate(in vec3 specularity, in vec3 albedo, in float metalness, in float f0) {
vec3 dielectricF0 = f0 * specularity; // 非金属的基础反射率(由折射率和镜面反射率计算)
return mix(dielectricF0, albedo, metalness); // 金属直接使用基础色作为反射率
}
代码逻辑解析:
- 当
metalness=0
(非金属):镜面反射率 = 折射率计算的 F0 ×specularity
- 当
metalness=1
(金属):镜面反射率 =albedo
(基础色) - 中间值:通过
mix
函数平滑过渡,模拟半金属特性
同时,金属材质几乎没有漫反射,代码通过以下逻辑实现:
// 计算漫反射率:金属漫反射为0,非金属使用albedo作为漫反射率
litArgs_albedo = litArgs_albedo * (1.0 - litArgs_metalness);
3. 粗糙度(Roughness)与光泽度(Glossiness)
粗糙度描述表面的微观不平度,代码中通过calcGlossiness
函数将其转换为光泽度(与粗糙度相反):
void calcGlossiness() {
dGlossiness = 1.0 - roughness; // 光泽度 = 1 - 粗糙度
dGlossiness += 0.0000001; // 避免零值,防止后续计算异常
}
作用:
- 光泽度越高(粗糙度越低):高光越集中,反射越清晰
- 光泽度越低(粗糙度越高):高光越扩散,反射越模糊
在镜面反射计算中,光泽度直接影响采样的环境贴图层级(后续环境贴图章节详细讲解)。
4. 折射率(IOR)与基础反射率(F0)
折射率决定光线从空气进入材质时的反射比例,代码中通过以下公式计算垂直入射时的反射率(F0):
// 在evaluateBackend中计算F0
float f0 = litArgs_ior;
f0 = (f0 - 1.0) / (f0 + 1.0); // 折射率转反射率公式
f0 *= f0; // 垂直入射时的反射光比例
物理意义:F0 表示光线垂直入射时,反射光占总入射光的比例。例如:
- 空气(IOR=1.0):F0=0(无反射)
- 玻璃(IOR=1.5):F0≈0.04(4% 反射率)
- 钻石(IOR=2.42):F0≈0.17(17% 反射率)
F0 是菲涅尔效应计算的基础,直接影响材质的反射强度。
二、光照模型的核心计算:从参数到颜色的转化
PBR 光照模型的核心是模拟光线与物体表面的交互,代码通过 “前端数据准备→后端光照计算” 的流程实现,核心逻辑集中在片元着色器的evaluateFrontend
和evaluateBackend
函数中。
1. 前端计算(evaluateFrontend):数据准备与向量计算
前端计算的作用是将原始参数转换为光照计算所需的中间变量,包括读取材质参数、计算核心向量(法线、视线方向等)。
void evaluateFrontend() {
// 1. 读取材质参数到d前缀变量(原始数据存储)
calcAlpha(); // dAlpha = opacity
calcAlbedo(); // dAlbedo = albedo
calcMetalness(); // dMetalness = metalness
calcGlossiness(); // dGlossiness = 1 - roughness
calcIor(); // dIor = ior
calcSpecularity();// dSpecularity = specularity
calcEmission(); // dEmissive = emissive
// 2. 计算核心向量(用于光照计算)
calcNormal(); // dNormal = 世界空间法线
calcViewDir(); // dViewDir = 视线方向(相机→顶点)
calcReflectDir(); // dReflectDir = 反射方向(基于法线和视线)
// 3. 将d变量传递给litArgs变量(光照计算参数)
litArgs_albedo = dAlbedo;
litArgs_specularity = dSpecularity;
litArgs_gloss = dGlossiness;
litArgs_metalness = dMetalness;
litArgs_ior = dIor;
litArgs_worldNormal = dNormal;
litArgs_viewDir = dViewDir;
litArgs_reflectDir = dReflectDir;
}
核心向量解析:
- 法线向量(
dNormal
):normalize(vNormalW)
,表面朝向,决定光线反射方向 - 视线方向(
dViewDir
):normalize(cameraWorldPos - vPositionW)
,从顶点指向相机 - 反射方向(
dReflectDir
):normalize(reflect(-dViewDir, dNormal))
,环境光反射的方向
2. 后端计算(evaluateBackend):光照核心逻辑
后端计算是 PBR 的 “大脑”,实现从参数到最终颜色的转化,包含 F0 计算、菲涅尔效应、漫反射与镜面反射计算等核心步骤。
步骤 1:计算基础反射率(F0)
// 根据折射率计算垂直入射的光照 反射光/总入射光的比例 f0
float f0 = litArgs_ior;
f0 = (f0 - 1.0) / (f0 + 1.0);
f0 *= f0;
这一步将折射率转换为物理上的基础反射率,是后续所有反射计算的基础。
步骤 2:修正高光反射率(考虑金属度)
// 根据f0和金属度调节高光反射率
litArgs_specularity = calcSpecularModulate(litArgs_specularity, litArgs_albedo,