立方体贴图、天空盒与环境映射边界

立方体贴图、天空盒与环境映射边界:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能实现 ±X/±Y/±Z 六面显式映射,并用方向测试图定位旋转、镜像和接缝错误
  • 能写出天空盒的去平移 view、xyww 深度技巧、LEQUAL 与状态恢复流程
  • 能分析世界空间位置、法线和相机位置不一致导致的环境映射漂移
  • 能修改反射为单界面折射,并判断 refract 返回零向量时的全反射分支
  • 能比较静态 cubemap 与动态六向环境 probe 的内容、更新成本和适用范围

为什么要把周围环境「拍」成六个方向

想象你站在一个空旷的地方,举起相机朝六个方向各拍一张:前、后、左、右、上、下。把这六张照片贴在一个正方体盒子的六个内壁上,再把你自己放进盒子正中央——往任意方向看出去,看到的就是当初拍下来的那片环境。盒子很大很远,你怎么走都走不到盒壁,它就成了永远在最远处的「背景天空」。

这正是这一章要做的第一件事:用六张图拼出一整圈环境当背景。更妙的是,有了这一整圈环境,还能让场景里的物体「照」到它——一个金属球可以像镜子一样把四周环境映在表面,一个玻璃球可以让背后的环境透过来、还被弯折一下。

这一章解决的就是:怎么用六张图表示「物体周围一整圈的环境」,再拿这圈环境当背景、或让物体反射 / 折射它。没有它,你的场景只有一个纯色背景,物体表面也照不出周遭的世界,金属和玻璃都会显得很假。

立方体贴图:6 张图,用方向向量去采样

把那六张环境照片打包成一个整体,就是一张 (cubemap)。它和普通 2D 纹理不同:普通纹理用二维 uv 坐标 (u, v) 取色,立方体贴图用带符号的三维方向选择面和面内 texel。

立方体贴图最关键、也最容易让人愣住的一点是:它用一根 ,而不是 uv。你给它一个三维向量 (x, y, z)——想象从盒子正中心朝这个方向射一根激光,它会戳中六个面里某一个面上的某一点,那一点的颜色就是采样结果。向量多长不重要,只看它指向哪。下面这张图把「6 张面图」和「方向向量怎么戳中某一点」一起画清楚:

六个 API 目标有固定顺序:POSITIVE_XNEGATIVE_XPOSITIVE_YNEGATIVE_YPOSITIVE_ZNEGATIVE_Z。它们的枚举值连续,所以可以从 POSITIVE_X + i 遍历;但“front.jpg 应该放 +Z 还是 −Z”取决于素材导出坐标系,不能从文件名猜。加载前建立显式表,并用六张写有 +X/-X/+Y/-Y/+Z/-Z 的测试图旋转相机验方向。

天空盒:把这圈环境当成最远的背景

有了立方体贴图,第一个用法就是 (skybox)。做法是:画一个立方体把相机罩在正中,立方体的六个内壁贴上立方体贴图。因为立方体的每个顶点本身就是一个从中心指向那个角的方向向量,所以顶点坐标直接拿来当采样方向就行——不需要另外准备 uv。

但天空盒有个非做不可的处理,否则立刻穿帮:。想想前面那个比喻——你站在盒子正中,盒子应该永远以你为中心,你往哪走它就跟着挪,你永远走不到盒壁。可如果天空盒老老实实跟着相机的位移走位,你一移动就会「撞」到某面盒壁、糊一脸。解决办法是渲染天空盒时,把观察矩阵里管位移的那部分抹掉、只留旋转——也就是取它左上角的 3×3 部分 mat3(view)。下面这张图把「去 / 不去平移」的差别摆在一起:

还有一个让天空盒永远在最远处的小技巧:在天空盒的顶点着色器里把输出位置的 z 分量设成等于 wgl_Position = pos.xyww),透视除法后深度恒为最大值 1.0;再把深度测试从默认的 GL_LESS 改成 GL_LEQUAL,天空盒就只会画在「场景里没有任何物体挡着」的地方——既当了背景,又不会盖住任何物体(细节见 §6)。

环境映射:让物体「照」到这圈环境

天空盒只是背景。立方体贴图更精彩的用法,是让场景里的物体表面也照出这圈环境——这类技术统称 (environment mapping)。原理很统一:物体的每个片段都算出一根方向向量,拿它去采样环境立方体贴图,采到的环境颜色就是这个片段的颜色。区别只在「这根方向向量怎么算」。

第一种是 (reflection)——像镜子。从相机看向物体某点的视线打到表面,按表面法线对称弹出一根反射方向,用它采样环境,物体表面就把环境原样映了出来,像一面铬镜。

第二种是 (refraction)。视线在当前界面按两种介质的 弯折,所得方向用来查询环境。这里是原章的单界面近似:它像玻璃,但没有模拟光离开物体时的第二次折射、厚度和内部吸收。

反射与折射的数学:reflect 和 refract 在算什么

这一节把 reflectrefract 两个内置函数背后的几何讲清楚。先约定符号(都用单位向量,矩阵/向量与 GLSL 一致):

符号含义
II入射方向(incident):从相机指向表面该点的视线方向,单位向量
NN表面法线(normal):表面在该点朝外的垂直方向,单位向量
RR结果方向:反射方向或折射方向,用来采样环境立方体贴图
η\eta折射率比 ratio =n1/n2= n_1 / n_2(入射侧折射率 / 进入侧折射率)

反射:关于法线对称弹出

反射方向 RR 由内置函数 reflect(I, N) 给出,公式是:

R=I2(NI)NR = I - 2\,(N \cdot I)\,N

这个式子在说:把入射方向 II 沿法线方向的那一份分量(大小是 NIN \cdot I)减掉两倍,就得到反射方向。直觉上,NIN \cdot III 在法线上的投影;减去一倍只是把 II「拍平」到表面上,减去两倍则是让它越过表面、对称地弹到另一侧——结果就是「入射角 = 反射角」的镜面反射。下面左半张图正是这个对称弹出:

折射:按折射率比弯折

折射方向由 refract(I, N, ratio) 给出,它背后是物理里的 (Snell 定律):

n1sinθ1=n2sinθ2n_1 \sin\theta_1 = n_2 \sin\theta_2

这个式子在说:入射角 θ1\theta_1 和折射角 θ2\theta_2 的正弦之比,等于两种介质折射率的反比。直观结论只有一句话——光进入更「密」的介质(折射率更大,如从空气进玻璃)时,会弯向法线(折射角更小);进入更「稀」的介质则弯离法线。refract 内部就按这条定律算出折射方向 RR,你只要把折射率比 η=n1/n2\eta = n_1/n_2 喂给它。上面右半张图就是「进玻璃弯向法线」的情形。

举个具体的:空气折射率 n1=1.00n_1 = 1.00、玻璃 n2=1.52n_2 = 1.52,那么

η=n1n2=1.001.520.658\eta = \frac{n_1}{n_2} = \frac{1.00}{1.52} \approx 0.658

这个 η\eta 喂给 refract,就得到「从空气进玻璃」的单界面折射方向。注意 η\eta、法线朝向和介质两侧要一起判断:写成 1.52/1.001.52/1.00 表示另一种传播方向,不能只换比值而沿用原来的外向法线。从高折射率射向低折射率且入射角过大时会发生全反射,GLSL refract 返回零向量,应回退到 reflect

能力边界:静态环境、动态 probe 与单界面折射

静态 cubemap 只包含拍摄或预渲染进六张图里的内容。场景中新出现的角色、车辆和特效不会自动进入镜面;中央物体看起来在“反射环境”,其实只是按方向查询那六张旧图。

要让反射包含当前场景,可在物体位置放一个 90° 视野相机,分别朝 ±X/±Y/±Z 渲染到 cubemap 六面,再把结果作为环境 probe。一次更新至少多画六遍场景,通常还要排除反射物自身,并降低分辨率或更新频率控制成本。单界面 refract 同样是教学近似;真实厚玻璃还要处理出射界面、Fresnel 反射、粗糙度和吸收。

动手:切换材质,看同一个球反射 / 折射环境

猜一猜:下面这块画布里,四周是一个程序化的「黄昏天空盒」(六个面各写了方位字 RIGHT / LEFT / TOP …),正中是一个球。现在它是反射模式,球面像镜子一样映着四周环境。如果你把材质切到**「折射」,球面会变成什么样?是依旧像镜子映出环境,还是变成玻璃**、能看到背后被弯折着透过来的环境?先切一下看看,再读解释。

这块画布严格对应本章三件事:四周的天空盒是一张程序化生成的立方体贴图(六面用代码画,没有任何外部贴图),它被设为场景背景、自动做了去平移(你拖动转视角,天空盒只跟着转、永远在最远处);正中的球按你选的材质,用反射折射方向去采样这张环境贴图。拖动画布转视角,分别切三种材质对比:

切到反射,球是一面完美的镜子,四周天空盒原样映在球面上,你甚至能在球面上读到反过来的方位字;切到折射,球变成一颗玻璃珠,看到的不再是表面映像,而是背后的环境透过玻璃、被弯折着挤进来;切到漫反射,球立刻「失去周遭感」——变成一个普通的哑光灰球,环境一点都照不上去。同一个球、同一圈环境,差别全在「片段着色器用哪根方向向量去采样」。

单步走一遍:方向向量是怎么贯穿全章的

猜一猜:天空盒、反射、折射,看起来是三件不同的事,但它们其实共用同一个核心动作。是哪一个动作?想一想上面 Demo 里「球面映出环境」和「天空盒当背景」有什么共同点,再单步走一遍。

下面把贯穿全章的那条主线拆成三步。每一步都配一张图,盯着「那根方向向量这一刻指向哪、采到环境的哪一点」:

分步1 / 3

① 核心动作:用方向向量采样立方体贴图

全章的共同核心只有一个:给一根方向向量 ,从立方体中心射出去, 戳中 6 个面中某一面的某一点, 那一点的颜色就是采样结果(注意是 3D 方向, 不是 2D 的 uv)。天空盒、反射、折射的区别,只在于这根方向向量是怎么来的。

走完这遍就清楚了:天空盒、反射、折射看着是三件事,骨子里都是「算一根方向向量、去采样同一张立方体贴图」。变的只是方向向量的来历,不变的是采样动作。

代码逐段拆解

立方体贴图的代码分三块:创建立方体贴图 + 加载 6 面天空盒着色器(去平移 + 深度技巧)反射 / 折射片段着色器。三块对照看,C++/OpenGL 与 WebGL2/TS 互为镜像。

第一块:创建立方体贴图、加载 6 个面

绑定到 GL_TEXTURE_CUBE_MAP,然后用一个循环把六张面图依次塞进六个面——靠 GL_TEXTURE_CUBE_MAP_POSITIVE_X + i 这组连续枚举遍历。

unsigned int loadCubemap(std::vector<std::string> faces) {
  if (faces.size() != 6) throw std::runtime_error("cubemap needs 6 faces");
  unsigned int texID;
  glGenTextures(1, &texID);
  glBindTexture(GL_TEXTURE_CUBE_MAP, texID);
  int w, h, n;
  for (unsigned int i = 0; i < 6; i++) {
    unsigned char *data = stbi_load(faces[i].c_str(), &w, &h, &n, 3);
    if (!data) throw std::runtime_error("cubemap face load failed");
    // faces 必须显式按 +X,-X,+Y,-Y,+Z,-Z 排列。
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB,
                 w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    stbi_image_free(data);
  }
  return texID;
}

六张图必须具有兼容的尺寸、格式和 mip 层,才能形成完整 cubemap。随后设置过滤和三个维度的 CLAMP_TO_EDGE,避免边界外重复采样把另一侧图像带进接缝;它不能修复素材本身颜色或朝向对不齐的问题:

glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 三个维度都钳到边缘,避免 wrap 造成额外接缝
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

两端逻辑一一对应,但有几处 API 差异要留神:

第二块:天空盒着色器——去平移 + 深度技巧

天空盒的顶点着色器做两件本章核心的事:把局部坐标直接当方向向量传给片段着色器(立方体顶点本身就是方向),以及用 pos.xyww 把深度顶到最远。去平移则在 CPU 端把 viewmat3 再补回 mat4

// 顶点着色器
#version 300 es
layout(location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;        // 已在 CPU 端去平移:mat4(mat3(view))
void main() {
  TexCoords = aPos;       // 局部坐标 = 采样方向(立方体顶点本身就是方向)
  vec4 pos = projection * view * vec4(aPos, 1.0);
  gl_Position = pos.xyww; // z=w,透视除法后深度恒为 1.0(最远)
}

CPU 端配合两件事:把观察矩阵去平移后再传给天空盒着色器,以及把深度函数改成 GL_LEQUAL(因为天空盒深度恒为 1.0,默认的 GL_LESS 会让它一个像素都画不出来)。天空盒通常最后画

// 场景里别的物体都画完后,最后画天空盒
glDepthFunc(GL_LEQUAL);              // 深度恒为 1.0,需 LEQUAL 才过测试
glDepthMask(GL_FALSE);               // 背景无需改写深度
skyboxShader.use();
// 去平移:取 view 的 3×3(只留旋转),再补成 4×4
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
skyboxShader.setMat4("view", view);
skyboxShader.setMat4("projection", projection);
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LESS);                // 画完恢复默认

去平移那一句是本章的命门:mat3(view) 丢掉 view 的平移、只留旋转,再补成 mat4,天空盒在视觉上始终以相机为中心。少了它,盒子固定在世界中,相机移动就会接近盒壁。深度函数和写掩码都是全局状态,画完必须成对恢复,避免污染下一 pass。

第三块:反射 / 折射片段着色器

环境映射的物体(注意是普通物体、不是天空盒)需要世界空间的位置法线,顶点着色器照常算并传出;片段着色器里算出 I,再分别用 reflectrefract 得到方向,拿它采样环境。最小教学版本只需替换内置函数,下面的稳健折射版还处理全反射:

#version 300 es
precision highp float;
in vec3 Normal;           // 世界空间法线
in vec3 Position;         // 世界空间位置
out vec4 FragColor;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main() {
  vec3 I = normalize(Position - cameraPos);   // 视线:相机 → 该点
  vec3 R = reflect(I, normalize(Normal));      // 反射:对称弹出
  FragColor = vec4(texture(skybox, R).rgb, 1.0);
}

核心改动是把 reflect(I, N) 换成 refract(I, N, ratio) 并传入折射率比;稳健版本还检查零向量并回退到反射。两个内置函数都应使用同一坐标空间的单位方向。这里 PositionNormalcameraPos 全是世界空间;若法线仍在模型空间,球一旋转反射就会“粘”在表面错误位置。

非均匀缩放下,世界空间法线不能直接乘 mat3(model),顶点着色器应使用法线矩阵:

Position = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;

完美 cubemap 反射只是材质的一部分。真实材质通常还会按 Fresnel、粗糙度和 reflection mask 混合基础颜色;否则整件物体都会像无粗糙度的铬镜。

容易踩的坑

小结

  • cubemap 用三维方向采样;六面必须显式映射到 ±X/±Y/±Z,尺寸格式兼容,CLAMP_TO_EDGE 只能防 wrap 接缝
  • 天空盒 view 去平移,xyww + LEQUAL 让它只填最远深度;depth func 和 depth mask 用后要恢复
  • 环境映射的 Position、法线、相机位置必须在同一世界空间,非均匀缩放使用逆转置法线矩阵
  • reflect 给镜面方向;refract(I,N,n1/n2) 是单界面近似,全反射时返回零向量,应回退到反射
  • 静态 cubemap 不含动态场景;动态 probe 要从物体位置重绘六个方向,质量更高但成本显著

练习

问题 1(改 Demo 代码) 上面的 CubemapDemo 折射模式用的是「空气→玻璃」(ratio = 1.00 / 1.52)。现在要把它改成(折射率 1.33)做的球。① 写出新的 ratio 表达式;② 与玻璃相比,水球里的环境弯折会更强还是更弱?为什么?

问题 2(推导 / 排错题) 某人写了天空盒,画出来一切正常;但他一让相机前进,天空盒就「贴到脸上」、整个穿模。他确认深度函数已改成 GL_LEQUAL、立方体贴图也加载对了。请指出最可能的那一步漏了什么,并写出修法的关键代码。

问题 3(推导题) 反射公式是 R=I2(NI)NR = I - 2(N \cdot I)N。设入射方向 I=(0,1,0)I = (0, -1, 0)(垂直向下打),表面法线 N=(0,1,0)N = (0, 1, 0)(朝上)。手算反射方向 RR,并说明结果是否符合「镜面正对」的直觉。

名词解释

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

立方体贴图

一种特殊纹理,由 6 张 2D 图组成,分别贴在一个立方体的 6 个带符号轴向目标 +X/-X/+Y/-Y/+Z/-Z 上。它用方向向量采样,而不是二维 uv。素材里的 front/back 命名不等于 API 方向,必须按导出坐标系显式映射并用方向测试图验证。详见本章「立方体贴图」一节。

方向向量来采样

采样立方体贴图的方式:给一根从立方体中心射出的三维方向向量 (x, y, z),它会戳中 6 个面里某一面的某一点,那一点的颜色就是采样结果。向量多长不重要,只看方向。这和普通 2D 纹理用二维 uv 坐标采样完全不同。详见本章「立方体贴图」一节。

天空盒

用立方体贴图当背景的一个大盒子:相机放在正中央,盒子 6 个内壁贴着环境图,往任意方向看都是那圈环境。关键是它要永远在「最远处」当背景——相机怎么移动都到不了盒壁。常用来做天空、远山、室内全景等背景。详见本章「天空盒」一节。

天空盒去平移

渲染天空盒时让它只随相机旋转、不随相机位移。做法是取观察矩阵的左上 3×3 部分 mat3(view)(只留旋转、丢掉位移),再补成 mat4。这样相机往哪走,天空盒都跟着、永远以相机为中心、在最远处当背景。不去平移的话,相机一移动天空盒就糊到脸上。详见本章「天空盒」一节。

环境映射

用立方体贴图近似物体周围环境的一类技术。每个片段计算反射或折射方向,再查询 cubemap。静态 cubemap 只会显示预先捕获的环境;若要包含动态物体,需要从 probe 位置向六个方向重绘场景。详见本章「环境映射」一节。

反射

环境映射的一种:物体表面像镜子一样把周围环境原样映出来。对每个片段,从相机看向该点的视线 I 打到表面,按法线 N 算出反射方向 R = reflect(I, N)(关于法线对称弹出,入射角 = 反射角),用 R 采样环境立方体贴图。这是完美镜面近似;真实金属或水面还要结合粗糙度、Fresnel 和基础颜色。详见本章「环境映射」一节。

折射

本章使用的单界面近似:refract(I, N, n1/n2) 计算光穿过当前表面一次后的方向,再采样 cubemap。它没有模拟物体厚度、内部传播和出射界面的第二次弯折,因此只能近似玻璃外观。详见本章「环境映射」一节。

折射率比

光从一种介质进入另一种介质时弯折程度的决定因素,等于两介质折射率之比 ratio = n1/n2n1 是入射侧如空气、n2 是进入侧如玻璃)。常见折射率:空气 1.00、水 1.33、玻璃 1.52、钻石 2.42。它必须和入射方向、法线朝向及介质两侧一致;从高折射率到低折射率时可能发生全反射,此时 refract 返回零向量。详见本章「环境映射」一节与「反射与折射的数学」一节。

斯涅尔定律

描述光从一种介质进入另一种介质时如何弯折的物理定律:入射角与折射角的正弦之比,等于两介质折射率的反比。直观结论是——光进入更「密」(折射率更大)的介质会弯向法线,进入更「稀」的介质会弯离法线。GLSL 的 refract 内部就按这条定律算折射方向。详见本章「反射与折射的数学」一节。

版本、来源与运行边界

本章以 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 结果的验证。

正式概念与状态责任

  • cubemap:在“立方体贴图、天空盒与环境映射边界”中由GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view负责解释其输入、受控状态和可观察结果;运行时以六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素定位它的第一处变化。
  • skybox:在“立方体贴图、天空盒与环境映射边界”中由GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view负责解释其输入、受控状态和可观察结果;运行时以六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素定位它的第一处变化。
  • reflection:在“立方体贴图、天空盒与环境映射边界”中由GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view负责解释其输入、受控状态和可观察结果;运行时以六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素定位它的第一处变化。
  • refraction:在“立方体贴图、天空盒与环境映射边界”中由GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view负责解释其输入、受控状态和可观察结果;运行时以六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“上传六面,先/后画天空盒并以方向向量采样,再恢复 depth state”发生后,GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view应怎样改变六面图像、方向向量、samplerCube、去平移 view、depth func 和绘制顺序;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

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

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

Context · resource · observable result

立方体贴图、天空盒与环境映射边界:状态合同

按六个方向建立 cubemap,并区分天空盒、反射和折射的采样向量与深度状态

验证场景

官方教程正式概念

logl-23 · 基线帧

cubemap固定 context、资源内容与输入事件,执行“上传六面,先/后画天空盒并以方向向量采样,再恢复 depth state”

状态所有者GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view
受控状态/资源六面图像、方向向量、samplerCube、去平移 view、depth func 和绘制顺序
触发命令上传六面,先/后画天空盒并以方向向量采样,再恢复 depth state

冻结输入:cubemap

GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view记录六面图像、方向向量、samplerCube、去平移 view、depth func 和绘制顺序

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

观测:六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素中的初始快照

预期:GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view得到可复查结果,并持续满足“六面尺寸/格式一致;天空盒不继承相机平移且不会覆盖已绘制近景”

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

逐段执行“上传六面,先/后画天空盒并以方向向量采样,再恢复 depth state”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“六面尺寸/格式一致;天空盒不继承相机平移且不会覆盖已绘制近景”。

CPU command · GL state · GPU result

立方体贴图、天空盒与环境映射边界:五段轨迹

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

当前观测:六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素中的初始快照

不变量:六面尺寸/格式一致;天空盒不继承相机平移且不会覆盖已绘制近景

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

注入“天空盒使用完整 view 矩阵,摄像机平移时盒体边界像普通物体一样移动”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素一起恢复才算修复。

Single fault · first divergence · replay

立方体贴图、天空盒与环境映射边界:反例与恢复

故障:天空盒使用完整 view 矩阵,摄像机平移时盒体边界像普通物体一样移动

1. 冻结输入一致

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

2. 注入单故障一致

保持其余输入不变,仅注入“天空盒使用完整 view 矩阵,摄像机平移时盒体边界像普通物体一样移动”

3. 定位首差一致

六面尺寸/格式一致;天空盒不继承相机平移且不会覆盖已绘制近景

4. 清理并重放一致

六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素

最小可重放检查

unit: logl-23
owner: GL_TEXTURE_CUBE_MAP 对象、skybox VAO/program 与 camera view
state_or_resource: 六面图像、方向向量、samplerCube、去平移 view、depth func 和绘制顺序
command: 上传六面,先/后画天空盒并以方向向量采样,再恢复 depth state
pass_invariant: 六面尺寸/格式一致;天空盒不继承相机平移且不会覆盖已绘制近景
single_fault: 天空盒使用完整 view 矩阵,摄像机平移时盒体边界像普通物体一样移动
required_evidence: 六面 target/尺寸、采样方向、view 矩阵、depth func 与环境像素

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

出处声明

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

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

讨论

评论区加载中…