IBL 镜面反射、Split-Sum 与粗糙度预滤波

IBL 镜面反射、Split-Sum 与粗糙度预滤波:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能实现并改出完整的 IBL 环境光照:在上一章漫反射 IBL 的基础上加入镜面 IBL——用预滤波环境贴图和 BRDF LUT 让球面上的高光也从环境中取光
  • 能说出 Split-Sum 近似的拆法:镜面积分 = 预滤波环境颜色(R, roughness) × BRDF 积分(NdotV, roughness),以及为什么这样拆能让实时渲染变成两次贴图采样
  • 能回答:为什么预滤波环境贴图用 mipmap level 对应 roughness?如果 roughness=0 采 mip 0、roughness=1 采 mip 4,中间的 roughness 采样到的图像会有什么变化?

为什么金属会丢掉环境倒影

上一章给 PBR 球加上了漫反射 IBL——球的暗面不再死黑,从环境里收到了真实的漫反射光。但盯着光滑金属球看一眼你就会发现不对:一块光滑的金属应该像镜子一样,把整片蓝天或地面倒映在自己身上。现在却只有四盏灯的高光亮点,背景环境在球面上没有任何反射。

没有镜面 IBL,光滑金属球就像一个只有四个白点的黑球,完全不像真实金属的感觉。把粗糙度调低,期待的是清晰的环境倒影,得到的却是没有内容的纯黑背景。

这一章要做的就是:把环境贴图按不同模糊程度存成多级,光滑的查清晰版、粗糙的查模糊版,加上一张记录 Fresnel 积分的小贴图,实时渲染时只查两张表就能还原完整的镜面积分。

Split-Sum 近似

镜面 IBL 的核心挑战是:镜面 BRDF 里有两个未知量(入射方向 ωi 和观察方向 ωo),无法像漫反射那样只靠法线方向预计算。Epic Games 的 Brian Karis 提出了 :把积分拆成两个独立部分相乘——一个只依赖反射方向和粗糙度,另一个只依赖视角与法线夹角和粗糙度。

Split-Sum 近似:镜面积分拆成两个独立预计算镜面积分Li(ωi)· fr(ωo,ωi)· cos θ dωiΩ(半球)预滤波环境颜色r=00.51.0sample(envMap, R, roughness)×BRDF 积分 LUTABLUT(NdotV, roughness) → (A, B)运行时合并公式prefilteredColor× (F0·A + B)两次贴图采样即可完成积分拆成两个可独立预计算的部分 → 运行时只需两次贴图采样
Split-Sum 近似:把难以实时计算的镜面积分拆成预滤波环境颜色(左乘项)和 BRDF 积分 LUT(右乘项),两项可独立预计算,运行时各查一次表即可还原积分。

这样每部分都可以独立预计算存成贴图,运行时用参数查表,把实时积分变成两次贴图采样。

预滤波环境贴图

是 Split-Sum 的左乘项:把原始环境贴图按不同粗糙度做模糊卷积,每个粗糙度等级存成 cubemap 的一个 mipmap 层级。

预滤波环境贴图:roughness → mipmap levelmip 00.0清晰mip 10.25mip 20.5mip 30.75mip 41.0模糊r清晰 →→ 模糊mipLevel = roughness × MAX_LOD(通常 MAX_LOD = 4)
预滤波环境贴图把粗糙度对应到 mipmap 层级:roughness=0 采最清晰的 mip 0, roughness=1 采最模糊的 mip 4,运行时用 textureLod 按粗糙度采样。

卷积时不能用均匀采样(效率太低),改用对 GGX 法线分布函数做——让采样方向偏向 GGX 镜面波瓣的中心,用 1024 个样本就能获得高质量的预计算结果。

BRDF 积分 LUT

Split-Sum 的右乘项是 :一张以 NdotV 和 roughness 为轴的 2D 贴图,R 通道存 F0 的缩放系数,G 通道存偏移量。

BRDF 积分 LUT(2D 查找纹理)Rscale (A)Gbias (B)01cos θ (NdotV) →01roughness ↑R 通道 = F0 的缩放系数(A)当 NdotV↑ 或 roughness↓ 时 A 趋近 1G 通道 = F0 的偏移量(B)补偿 Fresnel 对粗糙面的基础反射率specular = prefilteredColor × (F0 · A + B)F0 从 LUT 中查出的 (A, B) 重建 Fresnel 积分
BRDF 积分 LUT:一张以 NdotV(横轴)和 roughness(纵轴)为输入的 2D 查找纹理, R 通道存 F0 的缩放系数 A,G 通道存偏移量 B。运行时用 F0·A + B 重建镜面 Fresnel 积分。

预计算时对每个 (NdotV, roughness) 点做 1024 次 GGX 重要性采样,累加 G 和 Fresnel 贡献。最终公式:specular = prefilteredColor × (F0 · scale + bias)

单步确认:把镜面积分拆成两张表

分步1 / 3

粗糙度决定预滤波层级

原始 HDR cubemap 的每个 mip 都不是自动缩小图,而是针对一个粗糙度做 GGX 卷积。光滑材质查清晰层,粗糙材质查更模糊的层。

动手:开关镜面 IBL,看高光从哪里来

猜一猜:金属度拉到最高(metallic=1),粗糙度调低(roughness≈0.1),关掉 IBL(拖到 0)和开启 IBL(拖到 1),球面的高光有什么本质差别?再把粗糙度慢慢拉高到 1.0,观察预滤波贴图的模糊效果。

可交互

实时演示加载中…

拖完之后你应该看到:

  • IBL=1 + metallic=0.9 + roughness=0.3:球面出现蓝色天空和棕色地面的反射,光滑金属球面有清晰的环境倒影,高光不只是四个亮点
  • IBL=0:背景变暗,镜面 IBL 消失,球面退回只有四盏点光源的孤立高光,完全没有环境感
  • roughness 从 0.05 慢慢拉到 1.0:高光越来越扩散模糊,对应 proceduralSpecular 把环境贴图从清晰混合到平均色(模拟 mip 从 0 到 4 的模糊)
  • metallic=0 时kD_ibl 不为零,漫反射 IBL 占主导;镜面 IBL 也存在(F0=0.04 的非金属也有菲涅尔反射)

代码逐段拆解

以下把预计算和运行时着色器里镜面 IBL 相关的核心片段逐段拆解。预计算着色器(GPU 端离线运行)负责生成两张贴图;运行时着色器负责采样并合并。

a) Hammersley 序列:低差异采样点生成

重要性采样需要一套「均匀但又不规则」的 2D 样本序列。基于二进制位翻转(Van der Corput 序列):

// 二进制位翻转,生成 Van der Corput 序列的第 i 项
float RadicalInverse_VdC(uint bits) {
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// Hammersley 2D 样本点:(i/N, VdC(i))
vec2 Hammersley(uint i, uint N) {
    return vec2(float(i) / float(N), RadicalInverse_VdC(i));
}

RadicalInverse_VdC 把整数 i 的二进制表示左右翻转,再归一化到 (0,1)——这让相邻索引产生的值尽量分散,比随机数更均匀地覆盖 (0,1)。组合成 (i/N, VdC(i)) 的二维点,在 GGX 重要性采样里用作两个随机数 (u1, u2)。

b) ImportanceSampleGGX:GGX 重要性采样

根据 Hammersley 样本点,用 GGX NDF 推导出偏向镜面波瓣的采样方向:

// 输入: Xi = Hammersley 样本 (u1, u2),roughness,N = 法线
// 输出: H = 半程向量方向(世界空间),偏向 GGX 波瓣中心
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) {
    float a = roughness * roughness;
    // 球坐标:phi 均匀,cosTheta 由 GGX CDF 逆变换得到
    float phi      = 2.0 * PI * Xi.x;
    float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
    float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
    // 切线空间 H 向量
    vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
    // 切线空间 → 世界空间
    vec3 up   = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
    vec3 tangentX = normalize(cross(up, N));
    vec3 tangentY = cross(N, tangentX);
    return normalize(tangentX * H.x + tangentY * H.y + N * H.z);
}

关键公式是 cosTheta = sqrt((1 - u2) / (1 + (a⁴ - 1) * u2))——这是 GGX NDF 的累积分布函数(CDF)反函数,使采样方向概率密度与 GGX 波瓣形状匹配,集中在高 NDF 值的方向附近(specular lobe 中心),通常以更少样本获得稳定结果。

c) 预滤波卷积核心循环:V=R=N 近似

预计算时对每个 roughness 等级做 1024 次重要性采样累加:

// 预滤波卷积:对当前 roughness 生成预滤波环境颜色
// R = normalize(localPos) 是当前立方体面的反射方向
vec3 prefilteredColor = vec3(0.0);
float totalWeight = 0.0;
const uint SAMPLE_COUNT = 1024u;
for (uint i = 0u; i < SAMPLE_COUNT; i++) {
    vec2 Xi = Hammersley(i, SAMPLE_COUNT);
    // V=R=N:独立于视角的近似,会损失掠射角的视角相关反射变化
    vec3 H  = ImportanceSampleGGX(Xi, R, roughness);
    vec3 L  = normalize(2.0 * dot(V, H) * H - V); // V = R = N
    float NdotL = max(dot(R, L), 0.0);
    if (NdotL > 0.0) {
        prefilteredColor += texture(environmentMap, L).rgb * NdotL;
        totalWeight      += NdotL;
    }
}
prefilteredColor = prefilteredColor / totalWeight;

V=R=N 是一个工程近似:预计算时无法知道运行时实际的视角 V,所以假设 V=R=法线。代价是掠射角的视角相关反射会有误差,但对大多数材质在视觉上可接受。NdotL 权重给正对反射方向的采样赋更高贡献。

d) IntegrateBRDF:BRDF LUT 预计算

对每个 (NdotV, roughness) 点做重要性采样,分别累加 scale(A)和 bias(B):

// 输入: NdotV, roughness → 输出: (scale=A, bias=B)
vec2 IntegrateBRDF(float NdotV, float roughness) {
    vec3 V = vec3(sqrt(1.0 - NdotV*NdotV), 0.0, NdotV);
    float A = 0.0, B = 0.0;
    vec3 N = vec3(0.0, 0.0, 1.0);
    const uint SAMPLE_COUNT = 1024u;
    for (uint i = 0u; i < SAMPLE_COUNT; i++) {
        vec2 Xi = Hammersley(i, SAMPLE_COUNT);
        vec3 H  = ImportanceSampleGGX(Xi, N, roughness);
        vec3 L  = normalize(2.0 * dot(V, H) * H - V);
        float NdotL = max(L.z, 0.0);
        float NdotH = max(H.z, 0.0);
        float VdotH = max(dot(V, H), 0.0);
        if (NdotL > 0.0) {
            // IBL 版几何函数:k = roughness^2 / 2(与直接光照不同)
            float k = roughness * roughness / 2.0;
            float G_Vis = GeometrySchlickGGX(NdotV, k) * GeometrySchlickGGX(NdotL, k);
            G_Vis *= VdotH / (NdotH * NdotV + 0.0001);
            float Fc = pow(1.0 - VdotH, 5.0); // Fresnel 插值因子
            A += (1.0 - Fc) * G_Vis;           // F0 的缩放系数
            B += Fc * G_Vis;                   // 偏移量(不依赖 F0)
        }
    }
    return vec2(A, B) / float(SAMPLE_COUNT);
}

注意 IBL 版几何函数直接按 k = roughness²/2 计算(直接光照版是 k = (roughness+1)²/8,§7 第 1 条误区)。把 Fresnel 因子 Fc 拆开分别累加,就把 F0 的依赖分离到了 A 系数上——运行时用任意 F0 代入 F0·A + B 即可,LUT 与场景材质解耦。

e) 最终合并:diffuse IBL + specular IBL + direct lighting

运行时着色器把三部分组装在一起:

// 1. IBL 版 Fresnel(粗糙度修正,用 dot(N,V) 而非 dot(H,V))
vec3 F_ibl = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
vec3 kS_ibl = F_ibl;
vec3 kD_ibl = (1.0 - kS_ibl) * (1.0 - metallic);
// 2. 漫反射 IBL(同上一章)
vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuseIBL = irradiance * albedo;
// 3. 镜面 IBL:预滤波环境 × BRDF LUT
vec3 R = reflect(-V, N);
float lod = roughness * float(MAX_LOD);
vec3 prefilteredColor = textureLod(prefilterMap, R, lod).rgb;
vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N,V), 0.0), roughness)).rg;
vec3 specularIBL = prefilteredColor * (F_ibl * envBRDF.x + envBRDF.y);
// 4. 合并(注意:specularIBL 不再额外乘 kS!已包含在 Fresnel 中)
vec3 ambient = kD_ibl * diffuseIBL + specularIBL;
// 5. 加上直接光照
vec3 color = ambient + Lo;

关键细节:specularIBL 不额外乘 kS——镜面 Fresnel 已经在 BRDF LUT 里积分进去了,F_ibl * envBRDF.x + envBRDF.y 整体就是把 Fresnel 效果合并进去的系数。如果再乘 kS,会造成能量守恒破坏(§7 第 4 条误区)。

常见误区

小结与练习

这一章完成了 PBR + IBL 的最后一块拼图:Split-Sum 近似把镜面积分拆成预滤波环境贴图(roughness → mip level)和 BRDF 积分 LUT(NdotV × roughness → scale, bias),两项分别离线预计算,运行时各查一次表,加上上一章的漫反射 IBL,组装出完整的 IBL 环境光照。

  • Split-Sum 拆法的关键:把双变量积分拆成两个单变量积分相乘,每项都可独立预计算
  • 预滤波卷积用 GGX 重要性采样(Hammersley + ImportanceSampleGGX),比均匀采样快得多
  • BRDF LUT 预计算时 k = roughness²/2(IBL 版),与直接光照的 k = (roughness+1)²/8 不同
  • 合并时 specular IBL 不额外乘 kS,避免能量守恒破坏
  • 完整 IBL:ambient = kD·irradiance·albedo + prefilteredColor·(F·A + B) + Lo

练习

实现题

  1. 在 Demo 的 proceduralSpecular 函数里,把天空颜色从 vec3(0.5, 0.7, 1.2) 改为橙红色日落 vec3(1.2, 0.5, 0.2)。观察 metallic=0.9、roughness=0.1 时球面倒影颜色的变化;再把 roughness 拉到 0.8,看倒影是否还保留橙色调(模拟粗糙面趋向平均色)。
  1. 把 BRDF LUT 近似函数 approxBrdfLUT 的返回值改为 vec2(1.0, 0.0)(scale=1, bias=0,即假装 F0=1 的完美导体)。观察金属球(metallic=0.9)和非金属球(metallic=0)在 IBL=1 下的镜面反射各有什么异常——特别是掠射角边缘的 Fresnel 效果是否消失。

概念题

  1. 预滤波卷积时用了 V=R=N 近似,这意味着独立于视角方向预计算——失去了什么?在哪类材质或视角下这个误差最明显?
  1. 为什么 BRDF LUT 与场景中 F0 的具体值无关?同一张 LUT 如何同时服务于 F0=0.04 的非金属和 F0=0.56 的铜金属?

名词解释

本章出现的专业名词,用大白话再讲一遍。

Split-Sum 近似

把镜面积分拆成两个只依赖单一变量的独立积分相乘——预滤波环境颜色(反射方向 R + roughness)× BRDF 积分(NdotV + roughness)。两项可独立离线预计算成贴图,运行时各查一次表即可还原积分。出自 Epic Games Karis 2013「Real Shading in Unreal Engine 4」。

预滤波环境贴图

Pre-filtered Environment Map:对原始环境贴图按不同粗糙度做 GGX 重要性采样卷积,把结果存成 cubemap mipmap 各级。roughness=0 存 mip 0(清晰高频),roughness=1 存最高 mip(接近环境平均色)。运行时用 textureLod(prefilterMap, R, roughness × MAX_LOD) 采样。

重要性采样

Importance Sampling:蒙特卡洛积分的优化策略,集中在对结果贡献大的方向采样(这里是 GGX 波瓣中心附近),减少方差,用较少样本(1024)收敛到与数千次均匀采样相当的精度。Hammersley 序列 + GGX CDF 逆变换共同实现。

BRDF 积分 LUT

一张以 NdotV(横轴)和 roughness(纵轴)为输入的 2D 查找纹理,R 通道存 F0 的缩放系数 A,G 通道存偏移量 B。与 F0 无关——任意材质用同一张 LUT,运行时用 F0·A + B 重建镜面 Fresnel 积分。

Hammersley 序列

Van der Corput 序列(二进制位翻转归一化)× 均匀递增序列组合成的二维低差异序列。比伪随机采样在同样样本数下均匀覆盖积分域,蒙特卡洛积分收敛更快。常用于 GPU 端 1024 样本的预计算着色器。

fresnelSchlickRoughness

IBL 专用 Fresnel 变体:把普通 Fresnel-Schlick 的反射率上限 vec3(1.0) 替换成 max(vec3(1-roughness), F0),用粗糙度压低掠射角时的最大反射率,防止粗糙面 kS 趋近 1 导致 kD 归零。IBL 里输入 dot(N,V) 而非 dot(H,V)。

V=R=N 近似

预滤波环境贴图预计算时,因为无法知道运行时的实际视角 V,将 V 近似为反射方向 R,进而近似为法线 N。会丢失掠射角的视角相关反射变化,光滑金属边缘的误差最明显;高粗糙度材质的视觉误差通常可接受。

版本、来源与运行边界

本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。

正式概念与状态责任

  • prefilter map:在“IBL 镜面反射、Split-Sum 与粗糙度预滤波”中由prefilter cubemap、BRDF 2D LUT 与 PBR environment pass负责解释其输入、受控状态和可观察结果;运行时以mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular定位它的第一处变化。
  • brdf lut:在“IBL 镜面反射、Split-Sum 与粗糙度预滤波”中由prefilter cubemap、BRDF 2D LUT 与 PBR environment pass负责解释其输入、受控状态和可观察结果;运行时以mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular定位它的第一处变化。
  • split sum:在“IBL 镜面反射、Split-Sum 与粗糙度预滤波”中由prefilter cubemap、BRDF 2D LUT 与 PBR environment pass负责解释其输入、受控状态和可观察结果;运行时以mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular定位它的第一处变化。
  • specular ibl:在“IBL 镜面反射、Split-Sum 与粗糙度预滤波”中由prefilter cubemap、BRDF 2D LUT 与 PBR environment pass负责解释其输入、受控状态和可观察结果;运行时以mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“生成各 roughness mip 与 BRDF LUT,运行时选择 mip 并组合 prefilteredColor*(F*scale+bias)”发生后,prefilter cubemap、BRDF 2D LUT 与 PBR environment pass应怎样改变roughness→mip、GGX samples、prefilter color、NdotV、LUT scale/bias 和 F0;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

实验一:Context—资源—结果合同

选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。

Context · resource · observable result

IBL 镜面反射、Split-Sum 与粗糙度预滤波:状态合同

用 roughness 分级预滤波环境和 BRDF LUT 实现 split-sum specular IBL

验证场景

官方教程正式概念

logl-42 · 基线帧

prefilter map固定 context、资源内容与输入事件,执行“生成各 roughness mip 与 BRDF LUT,运行时选择 mip 并组合 prefilteredColor*(F*scale+bias)”

状态所有者prefilter cubemap、BRDF 2D LUT 与 PBR environment pass
受控状态/资源roughness→mip、GGX samples、prefilter color、NdotV、LUT scale/bias 和 F0
触发命令生成各 roughness mip 与 BRDF LUT,运行时选择 mip 并组合 prefilteredColor*(F*scale+bias)

冻结输入:prefilter map

prefilter cubemap、BRDF 2D LUT 与 PBR environment pass记录roughness→mip、GGX samples、prefilter color、NdotV、LUT scale/bias 和 F0

结果:得到可重复的初始 GL 状态与资源身份

观测:mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular中的初始快照

预期:prefilter cubemap、BRDF 2D LUT 与 PBR environment pass得到可复查结果,并持续满足“prefilter 最大 mip 与运行时 roughness 映射一致;LUT 坐标限定在有效范围”

实验二:CPU 命令到 GPU 结果的五段轨迹

逐段执行“生成各 roughness mip 与 BRDF LUT,运行时选择 mip 并组合 prefilteredColor*(F*scale+bias)”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“prefilter 最大 mip 与运行时 roughness 映射一致;LUT 坐标限定在有效范围”。

CPU command · GL state · GPU result

IBL 镜面反射、Split-Sum 与粗糙度预滤波:五段轨迹

选择一段命令—资源—结果1 / 5

当前观测:mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular中的初始快照

不变量:prefilter 最大 mip 与运行时 roughness 映射一致;LUT 坐标限定在有效范围

实验三:单故障与同输入恢复

注入“运行时假定 5 个 mip,但预滤波纹理只生成 4 层,粗糙材质采到未定义层”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular一起恢复才算修复。

Single fault · first divergence · replay

IBL 镜面反射、Split-Sum 与粗糙度预滤波:反例与恢复

故障:运行时假定 5 个 mip,但预滤波纹理只生成 4 层,粗糙材质采到未定义层

1. 冻结输入一致

第 1 次沿用同一 context、资源、uniform 与 draw 输入

2. 注入单故障一致

保持其余输入不变,仅注入“运行时假定 5 个 mip,但预滤波纹理只生成 4 层,粗糙材质采到未定义层”

3. 定位首差一致

prefilter 最大 mip 与运行时 roughness 映射一致;LUT 坐标限定在有效范围

4. 清理并重放一致

mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular

最小可重放检查

unit: logl-42
owner: prefilter cubemap、BRDF 2D LUT 与 PBR environment pass
state_or_resource: roughness→mip、GGX samples、prefilter color、NdotV、LUT scale/bias 和 F0
command: 生成各 roughness mip 与 BRDF LUT,运行时选择 mip 并组合 prefilteredColor*(F*scale+bias)
pass_invariant: prefilter 最大 mip 与运行时 roughness 映射一致;LUT 坐标限定在有效范围
single_fault: 运行时假定 5 个 mip,但预滤波纹理只生成 4 层,粗糙材质采到未定义层
required_evidence: mip count/roughness、预滤波 texel、LUT 坐标/值、Fresnel 与最终 specular

复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。

出处声明

本文为改编重写,改编自 Joey de Vries 的 LearnOpenGL。原文:learnopengl.com

原作及译作以 CC BY-NC 4.0 协议授权,本改编版同样遵循该协议(署名—非商业性使用)。

讨论

评论区加载中…