点阴影、深度立方图与距离域比较
点阴影、深度立方图与距离域比较:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能解释为什么点光源要用「深度立方体贴图」:点光向四面八方发光,上一章方向光那张 2D 深度图只罩得住一个方向,要用 6 个面各渲一张距离图把光源 360° 全包住——说得清「6 个面分别朝哪、为什么单张 2D 不够」
- 能在下面的 demo 里把点光源在房间内移动,让四面墙和地面的影子全向同时变化;并能说出第一遍存的是片元到光源的线性距离(不是裁剪空间深度)、第二遍是用方向向量
fragPos − lightPos去 cubemap 采样判阴影 - 能回答:第二遍判阴影时,从 cubemap 采样回来的「最近距离」是除以
far_plane归一化存的,那要和片元到光源的实际距离比之前,得先对它做一步什么操作?漏了这步会怎样?
为什么一台相机不够,得朝 6 个方向各拍一张
上一章你让太阳那样的方向光投出了影子:把一台「相机」搬到光的位置朝场景拍一张,拍不到的角落就是影子。可那台相机只朝一个方向看——太阳的光是平行的、就一个方向,一张就够。
但屋里那盏灯泡不一样:它向四面八方发光,左边的墙、右边的墙、头顶、脚下,处处都该有影子。这时只朝一个方向拍的那一张,就只罩得住一个方向,其余方向全是没记录的盲区——墙上该有的影子直接没了。
这一章要解决的就是这件事:怎么让一盏向 360° 发光的灯,把四面八方的遮挡全都记下来。办法很直接——既然一张照片只看一个方向,那就让相机站在灯的位置、朝上下左右前后 6 个方向各拍一张,6 张拼起来正好把灯整个包住。没有它,点光源的影子只能照顾一个方向;有了它,屋里每面墙才都稳稳落上影子。
核心思想:让光源朝 6 个方向各看一眼
上一章的整套两遍法你已经会了:第一遍从光源视角渲一张深度图记下每个方向最近的遮挡距离,第二遍从相机渲场景、把片元变到光的视角下做深度比较判阴影。点阴影没有推翻这套,它只改了一处——把「一张深度图」换成「6 张」,好让点光源向四面八方的遮挡都被记下来。这种把光源周围所有方向的遮挡都覆盖到的阴影,就叫 ↡点光源(向四面八方 360° 发光的光源,如灯泡)产生的阴影。和上一章方向光只朝一个方向不同,点光的光线射向所有方向,所以它的遮挡必须在「所有方向」上都被记录下来——一张只看一个方向的 2D 深度图罩不住,要用 6 个面的深度立方体贴图把光源整个包住。(omnidirectional shadows)。
为什么一张不够?想象灯泡在屋子正中:朝右拍的那张只记得住右墙方向的遮挡,左墙、天花板、地板方向它一概看不见。下面这张图把方向光(一张就够)和点光源(一张只罩一个方向、其余漏掉)摆在一起,一眼就明白差在哪:
补这个缺口的办法是:让光源朝 +X / −X / +Y / −Y / +Z / −Z 这 6 个方向各放一台朝外的小相机,每台各渲一张深度图,6 张拼成一个把光源严严实实包住的立方体——这就是 ↡把点光源周围 6 个方向(+X/−X/+Y/−Y/+Z/−Z 上下左右前后)各渲染一张深度图,拼成的一个立方体贴图(cubemap)。它的每个面记录的是「沿这个方向望出去,离光源最近的遮挡物有多远」。6 个面合起来正好把光源 360° 全包住,所以点光源任何方向上的遮挡都能查到。也叫 depth cubemap。(depth cubemap)。它和上一章用过的立方体贴图(天空盒那种)是同一种结构——6 张面图贴在立方体的 6 个内壁上,只不过这里每面存的不是颜色,而是距离:
存什么、怎么查:距离 + 方向向量
6 个面里到底存什么?上一章方向光存的是「裁剪空间深度」(一个被透视/正交投影压过的非线性 z)。点光源换了个更自然的量:直接存 ↡第一遍渲深度立方图时,每个面存的不是被投影压过的裁剪空间深度,而是「这个方向上,离光源最近的遮挡物到光源的真实直线距离」(length(fragPos − lightPos))。因为点光向所有方向发光,用「到光源的距离」描述遮挡比某个方向的投影深度更直接、更对称。存的时候除以 far_plane 归一化到 [0,1],第二遍采样回来再乘回 far_plane 还原成真实距离。——也就是每个遮挡点离灯泡有多远(length(fragPos − lightPos))。点光向所有方向对称地发光,用「直线距离」描述遮挡比某个方向的投影深度更自然。存进 cubemap 前把这个距离除以 far_plane 归一化到 [0,1](深度纹理只存 [0,1]),第二遍采样回来再乘回 far_plane 还原成真实距离:
第二遍判阴影时,上一章是用片元在光空间的 proj.xy 当 2D 坐标去采样。点光的 cubemap 不用 2D 坐标,而是和天空盒一样用一根方向向量去采样——这就是 ↡从立方体贴图取值的方式:不用二维 uv 坐标,而是用一根从立方体中心射出的方向向量,方向指向哪个面的哪个点,就取那一点的值。点阴影里这根方向向量就是「从光源指向当前片元」的向量 fragToLight = fragPos − lightPos:用它去深度立方图采样,取回的就是「这个方向上离光源最近的遮挡距离」。。具体地,对每个片元算出「从光源指向它」的方向 fragToLight = fragPos − lightPos,用这根方向去 cubemap 采样,取回这个方向上存的最近距离(乘回 far_plane 还原),再和片元自己到光源的实际距离 length(fragToLight) 做深度比较:实际距离更大 ⇒ 前面有更近的东西先挡住了光 ⇒ 在阴影;两者基本相等 ⇒ 它自己就是最近的 ⇒ 受光。
单步建立:从六面深度到全向判断
猜一猜:点光源的每一面都必须记录什么,第二遍为什么不能拿一张面图的二维坐标继续比较?按下面三步走一遍,再回看 6 面立方图的职责。
① 六个 90 度面包住光源
点光要覆盖完整 360 度,每一面都使用同一个 90 度透视投影;否则 cubemap 面之间会留下缺口或重叠失配。
这一步的判定逻辑和上一章的深度比较一模一样,只是「比的量」从「投影深度」换成了「到光源的距离」、「取数据的方式」从「2D 坐标」换成了「方向向量」。上一章踩过的 shadow acne(自遮挡条纹)、peter panning(bias 过大阴影脱离)、PCF(采样邻域软化边缘)在点阴影里全都还在——只是 PCF 这次要沿多个三维方向偏移采样 cubemap,而不再是在 2D 平面上偏一圈。下面 Demo 先让你把点光源在屋里移起来、看四壁全向变化,代码对照再把 6 个面和方向向量采样落到 shader。
动手:在房间里移动点光源,看四壁全向变
猜一猜:下面是一间四壁朝内的房间,正中飘着一盏点光源,地上摆了几个物体。把光源方位拖一圈、再把光源移到房间一个角落,你觉得哪面墙上物体的影子会被拉得最长?是离光最近的墙,还是离光最远的那面墙?先动手拖一拖,再看下面的解释。
这个 demo 用的是 three.js 的内建点光阴影(PointLight 开 castShadow)——它的底层做的正是本章讲的事:朝 6 个方向各渲一张距离图、拼成深度立方体贴图把光源 360° 全包住,再用方向向量采样判阴影。我们把要教的参数全做成了实时控件:光源方位 / 高度让光在房间里移动、看四面墙和地面的影子全向同时变(这是点阴影 vs 上一章方向光单向阴影最直观的差异,也是本 demo 头牌);阴影图分辨率切 256↔2048(点光是 6 面 cubemap、每面这个尺寸)、看阴影边缘的锯齿;depth bias 往大拖、造出又修掉 peter panning 阴影脱离;物体自转让影子随物体动。
点阴影演示加载中…
谜底:把光源移到房间一角,离光最远的那面墙上影子最长——光从角落斜斜地照过去,物体投在远墙上的影子被拉得又细又长(就像傍晚太阳低垂时影子拖得老远)。最该上手的是头牌那条:拖「光源方位」转一圈,盯着四面墙看——你会看到每面墙上的影子方向、长短都在同时变化,没有哪个方向是「盲区」,这正是 6 面深度立方图把光源全包住的效果。再把分辨率从 2048 切到 256,阴影边缘从平滑变成粗粗的阶梯锯齿;把 bias 拖到最大,影子和物体脚下脱节、往后缩,物体像重新飘起来——这就是上一章见过的 peter panning,点光的 bias 比方向光更娇气,得更细地调(原因见误区)。
代码对照:6 个面 + 方向向量采样怎么落到 shader
上面的 demo 是 three 帮你封装好的;这一节把底层「裸写」一遍。先看第一遍:点光阴影相机是透视投影(视场角 90°、宽高比 1,正好一格一面无缝拼接),然后为 6 个方向各算一个 lookAt 观察矩阵——+X 看向右、−X 看向左、+Y 看向上……6 个朝向把光源周围全覆盖。注意每个面的 up 向量要选对,否则相邻面会扭转拼不上:
// 点光阴影相机:透视、90° 视场、宽高比 1(一格一面无缝拼接)
float aspect = (float)SHADOW_W / (float)SHADOW_H; // = 1
float near = 0.1f, far = 25.0f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far);
// 6 个朝向的观察矩阵:+X/−X/+Y/−Y/+Z/−Z,注意每面的 up 选对
std::vector<glm::mat4> shadowTransforms;
glm::vec3 P = lightPos;
shadowTransforms.push_back(shadowProj * glm::lookAt(P, P + glm::vec3( 1,0,0), glm::vec3(0,-1, 0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(P, P + glm::vec3(-1,0,0), glm::vec3(0,-1, 0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(P, P + glm::vec3(0, 1,0), glm::vec3(0, 0, 1)));
shadowTransforms.push_back(shadowProj * glm::lookAt(P, P + glm::vec3(0,-1,0), glm::vec3(0, 0,-1)));
shadowTransforms.push_back(shadowProj * glm::lookAt(P, P + glm::vec3(0,0, 1), glm::vec3(0,-1, 0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(P, P + glm::vec3(0,0,-1), glm::vec3(0,-1, 0)));// 点光阴影相机:透视、90° 视场、宽高比 1(一格一面无缝拼接)
const aspect = SHADOW_W / SHADOW_H; // = 1
const near = 0.1,
far = 25.0;
const shadowProj = mat4.perspective(Math.PI / 2, aspect, near, far);
// 6 个朝向的观察矩阵:+X/−X/+Y/−Y/+Z/−Z,注意每面的 up 选对
const P = lightPos;
const dirs: [number[], number[]][] = [
[
[1, 0, 0],
[0, -1, 0],
],
[
[-1, 0, 0],
[0, -1, 0],
],
[
[0, 1, 0],
[0, 0, 1],
],
[
[0, -1, 0],
[0, 0, -1],
],
[
[0, 0, 1],
[0, -1, 0],
],
[
[0, 0, -1],
[0, -1, 0],
],
];
const shadowTransforms = dirs.map(([d, up]) =>
mat4.multiply(shadowProj, mat4.lookAt(P, vec3.add(P, d), up)),
);有了这 6 个矩阵就该渲深度立方图了——这里桌面和 WebGL2 走的是两条不同的路,是本章最大的一处跨端差异:
第一遍的片段着色器是关键:它不靠硬件的投影深度,而是手动算「片元到光源的距离」,除以 far_plane 归一化后写入深度。两端这段 GLSL 逻辑一致,只差版本声明:
#version 330 core
in vec4 FragPos; // 几何着色器透传的世界坐标
uniform vec3 lightPos;
uniform float far_plane;
void main() {
// 片元到光源的真实直线距离
float lightDistance = length(FragPos.xyz - lightPos);
// 除以 far_plane 归一化到 [0,1] 再写入(深度图只存 [0,1])
gl_FragDepth = lightDistance / far_plane;
}#version 300 es
precision highp float;
in vec4 FragPos; // 顶点着色器透传的世界坐标
uniform vec3 lightPos;
uniform float far_plane;
void main() {
// 片元到光源的真实直线距离
float lightDistance = length(FragPos.xyz - lightPos);
// 除以 far_plane 归一化到 [0,1],写进当前绑定的 cubemap 深度面
gl_FragDepth = lightDistance / far_plane;
}第二遍是相机正常渲场景,判阴影全在片段着色器里。核心三步:① 算方向 fragToLight = fragPos − lightPos;② 用它去 cubemap 采样取存的最近距离、乘回 far_plane 还原;③ 和片元到光的实际距离 length(fragToLight) 比 + bias。注意采样直接传方向向量,不需要归一化、也不需要 2D 坐标:
float ShadowCalculation(vec3 fragPos) {
// ① 从光源指向片元的方向向量(cubemap 就用它采样)
vec3 fragToLight = fragPos - lightPos;
// ② 用方向采样 cubemap,取回存的最近距离,乘回 far_plane 还原成真实距离
float closestDepth = texture(depthCubemap, fragToLight).r * far_plane;
// ③ 片元到光的实际距离
float currentDepth = length(fragToLight);
// 实际距离比最近遮挡还远(+bias 防自遮挡)⇒ 被挡 ⇒ 在阴影
float bias = 0.05;
return currentDepth - bias > closestDepth ? 1.0 : 0.0;
}float ShadowCalculation(vec3 fragPos) {
// ① 从光源指向片元的方向向量(cubemap 就用它采样)
vec3 fragToLight = fragPos - lightPos;
// ② 用方向采样 cubemap,取回存的最近距离,乘回 far_plane 还原成真实距离
float closestDepth = texture(depthCubemap, fragToLight).r * far_plane;
// ③ 片元到光的实际距离
float currentDepth = length(fragToLight);
// 实际距离比最近遮挡还远(+bias 防自遮挡)⇒ 被挡 ⇒ 在阴影
float bias = 0.05;
return currentDepth - bias > closestDepth ? 1.0 : 0.0;
}上面只采样了一个方向,阴影边缘会是硬边、还带锯齿。点阴影的 PCF 修法和上一章同理——多采样几次取平均,只是这次沿多个三维方向偏移采样 cubemap(在 fragToLight 周围的小立方体邻域里取一圈样),而不是在 2D 平面上偏一圈。把上面的单次采样换成对一组方向偏移的循环:
// 沿一组三维方向偏移采样 cubemap,平均「在阴影」的比例(软化边缘)
vec3 sampleOffsets[20] = vec3[](
vec3( 1, 1, 1), vec3( 1,-1, 1), vec3(-1,-1, 1), vec3(-1, 1, 1),
vec3( 1, 1,-1), vec3( 1,-1,-1), vec3(-1,-1,-1), vec3(-1, 1,-1),
vec3( 1, 1, 0), vec3( 1,-1, 0), vec3(-1,-1, 0), vec3(-1, 1, 0),
vec3( 1, 0, 1), vec3(-1, 0, 1), vec3( 1, 0,-1), vec3(-1, 0,-1),
vec3( 0, 1, 1), vec3( 0,-1, 1), vec3( 0,-1,-1), vec3( 0, 1,-1));
float shadow = 0.0, diskRadius = 0.05, bias = 0.05;
float currentDepth = length(fragToLight);
for (int i = 0; i < 20; ++i) {
float d = texture(depthCubemap, fragToLight + sampleOffsets[i] * diskRadius).r * far_plane;
shadow += currentDepth - bias > d ? 1.0 : 0.0;
}
shadow /= 20.0; // 20 次取平均 → 软边// 沿一组三维方向偏移采样 cubemap,平均「在阴影」的比例(软化边缘)
vec3 sampleOffsets[20] = vec3[](
vec3( 1, 1, 1), vec3( 1,-1, 1), vec3(-1,-1, 1), vec3(-1, 1, 1),
vec3( 1, 1,-1), vec3( 1,-1,-1), vec3(-1,-1,-1), vec3(-1, 1,-1),
vec3( 1, 1, 0), vec3( 1,-1, 0), vec3(-1,-1, 0), vec3(-1, 1, 0),
vec3( 1, 0, 1), vec3(-1, 0, 1), vec3( 1, 0,-1), vec3(-1, 0,-1),
vec3( 0, 1, 1), vec3( 0,-1, 1), vec3( 0,-1,-1), vec3( 0, 1,-1));
float shadow = 0.0, diskRadius = 0.05, bias = 0.05;
float currentDepth = length(fragToLight);
for (int i = 0; i < 20; ++i) {
float d = texture(depthCubemap, fragToLight + sampleOffsets[i] * diskRadius).r * far_plane;
shadow += currentDepth - bias > d ? 1.0 : 0.0;
}
shadow /= 20.0; // 20 次取平均 → 软边最后把 shadow(0 受光 ~ 1 全影)乘进光照,和上一章一样:(ambient + (1.0 - shadow) * (diffuse + specular)) * color。这几段计算两端 GLSL 一字不差,差异只有老几样:
容易踩的坑
小结
- 点阴影 = 阴影映射的全向版:点光向 360° 发光,一张 2D 深度图只罩一个方向,要用 6 个面(+X/−X/+Y/−Y/+Z/−Z)各渲一张拼成深度立方体贴图把光源全包住
- 第一遍每个面存的是片元到光源的线性距离(不是裁剪空间深度),存前 ÷
far_plane归一化、采样回来 ×far_plane还原——这两步必须成对 - 第二遍用方向向量
fragToLight = fragPos − lightPos去 cubemap 采样取最近距离,再和片元的实际距离length(fragToLight)做深度比较 + bias 判阴影 - 上一章的 shadow acne / peter panning / PCF 在点阴影里照旧:bias 仍是双刃剑(点光更需细调),PCF 改为沿多个三维方向偏移采样 cubemap 软化边缘
- 跨端最大差异在第一遍:桌面可用几何着色器一遍渲 6 面,WebGL2 无几何着色器需循环渲 6 次;房间盒子要
side = BackSide才能从里面看到四壁并 receiveShadow
练习
问题 1(改 Demo 代码型) 在上面的 demo 里,先验证「全向」:把光源方位拖一整圈,盯着四面墙看——说出你观察到墙上影子怎么变,并解释为什么没有哪个方向是「盲区」。再把光源移到房间一角,说出哪面墙影子最长、为什么。最后切阴影图分辨率到 256,描述阴影边缘的变化并解释成因。
问题 2(独立实现题 · C 型) 不依赖 three 这类封装,自己用裸 WebGL2(无几何着色器)渲一张点光源的深度立方体贴图。列出你需要的关键步骤(不必写全部代码,但要点要全,尤其是「WebGL2 怎么补几何着色器的缺」)。
问题 3(距离域排错) 第一遍把 length(FragPos - lightPos) / far_plane 写进深度立方图;第二遍你直接把 texture(depthCubemap, fragToLight).r 与 length(fragToLight) 比较。会发生什么?修复时除了乘回 far_plane,还要不要把 fragToLight 归一化?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 全向阴影
点光源(向四面八方 360° 发光的光源,比如屋里一盏灯泡)产生的阴影。和上一章方向光只朝一个方向不同,点光的光线射向所有方向,所以它的遮挡必须在所有方向上都被记录下来——一张只看一个方向的 2D 深度图罩不住,要用一个把光源整个包住的「深度立方体贴图」。详见本章「核心思想」一节。
- 深度立方体贴图
把点光源周围 6 个方向(+X/−X/+Y/−Y/+Z/−Z,也就是上下左右前后)各渲染一张深度图,拼成的一个立方体贴图。它和天空盒那种立方体贴图是同一种结构——6 张面图贴在立方体的 6 个内壁上,只是这里每面存的不是颜色,而是「沿这个方向望出去,离光源最近的遮挡物有多远」。6 个面合起来正好把光源 360° 全包住,点光任何方向的遮挡都能查到。也叫 depth cubemap。详见本章「核心思想」一节。
- 到光源的距离
第一遍渲深度立方图时,每个面里存的那个值。上一章方向光存的是被投影压过的「裁剪空间深度」,点阴影换成更自然的量——直接存「这个方向上,最近的遮挡物到光源的真实直线距离」(
length(fragPos − lightPos))。因为点光向所有方向对称发光,用「距离」描述遮挡比某个方向的投影深度更直接。存的时候除以far_plane归一化到[0,1],采样回来再乘回far_plane还原。详见本章「存什么、怎么查」一节。- 方向向量采样
从立方体贴图取值的方式:不用二维 uv 坐标,而是用一根从立方体中心射出的方向向量——方向指向哪个面的哪个点,就取那一点的值。点阴影里这根方向向量就是「从光源指向当前片元」的
fragToLight = fragPos − lightPos:用它去深度立方图采样,取回的就是「这个方向上离光源最近的遮挡距离」。方向不必归一化,cubemap 采样只看方向。详见本章「存什么、怎么查」一节。
版本、来源与运行边界
本章以 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 结果的验证。
正式概念与状态责任
- point shadows:在“点阴影、深度立方图与距离域比较”中由depth cubemap、六个 light-space transforms 与相机 pass负责解释其输入、受控状态和可观察结果;运行时以六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图定位它的第一处变化。
- depth cubemap:在“点阴影、深度立方图与距离域比较”中由depth cubemap、六个 light-space transforms 与相机 pass负责解释其输入、受控状态和可观察结果;运行时以六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图定位它的第一处变化。
- omnidirectional:在“点阴影、深度立方图与距离域比较”中由depth cubemap、六个 light-space transforms 与相机 pass负责解释其输入、受控状态和可观察结果;运行时以六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图定位它的第一处变化。
章专属 OpenGL 状态实验
先预测“一次几何阶段或六次 pass 写距离,再按方向采样并还原真实深度”发生后,depth cubemap、六个 light-space transforms 与相机 pass应怎样改变六面矩阵、片段到光源距离、far_plane、cubemap depth 和 PCF 偏移;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。
实验一:Context—资源—结果合同
选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。
Context · resource · observable result
点阴影、深度立方图与距离域比较:状态合同
从点光源向六个方向写入线性距离 cubemap,并用同一 far_plane 恢复比较
验证场景
官方教程正式概念
logl-32 · 基线帧
point shadows:固定 context、资源内容与输入事件,执行“一次几何阶段或六次 pass 写距离,再按方向采样并还原真实深度”
冻结输入:point shadows
depth cubemap、六个 light-space transforms 与相机 pass记录六面矩阵、片段到光源距离、far_plane、cubemap depth 和 PCF 偏移
结果:得到可重复的初始 GL 状态与资源身份
观测:六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图中的初始快照
预期:depth cubemap、六个 light-space transforms 与相机 pass得到可复查结果,并持续满足“写入归一化距离和读取乘数使用同一个 far_plane,六面接缝方向一致”
实验二:CPU 命令到 GPU 结果的五段轨迹
逐段执行“一次几何阶段或六次 pass 写距离,再按方向采样并还原真实深度”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“写入归一化距离和读取乘数使用同一个 far_plane,六面接缝方向一致”。
CPU command · GL state · GPU result
点阴影、深度立方图与距离域比较:五段轨迹
当前观测:六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图中的初始快照
不变量:写入归一化距离和读取乘数使用同一个 far_plane,六面接缝方向一致
实验三:单故障与同输入恢复
注入“depth pass 使用 far_plane=25,lighting pass 却按 100 还原,几乎全场误判阴影”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图一起恢复才算修复。
Single fault · first divergence · replay
点阴影、深度立方图与距离域比较:反例与恢复
故障:depth pass 使用 far_plane=25,lighting pass 却按 100 还原,几乎全场误判阴影
第 1 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“depth pass 使用 far_plane=25,lighting pass 却按 100 还原,几乎全场误判阴影”
写入归一化距离和读取乘数使用同一个 far_plane,六面接缝方向一致
六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图
最小可重放检查
unit: logl-32
owner: depth cubemap、六个 light-space transforms 与相机 pass
state_or_resource: 六面矩阵、片段到光源距离、far_plane、cubemap depth 和 PCF 偏移
command: 一次几何阶段或六次 pass 写距离,再按方向采样并还原真实深度
pass_invariant: 写入归一化距离和读取乘数使用同一个 far_plane,六面接缝方向一致
single_fault: depth pass 使用 far_plane=25,lighting pass 却按 100 还原,几乎全场误判阴影
required_evidence: 六面矩阵、写入距离、far_plane uniforms、采样方向、比较值与阴影截图复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。