纹理对象、采样状态与多纹理绑定

纹理对象、采样状态与多纹理绑定:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能解释图片像素、texture object、texture unit、target binding 与 sampler uniform 的区别
  • 能判断 channels、internal format、source format、type 与 unpack alignment 并正确上传图片
  • 能判断 wrap、MIN/MAG filter、mipmap levels 如何共同决定纹理完整性和采样结果
  • 能实现 UV attribute、插值、sampler2D、texture() 与两纹理 mix() 的完整链路
  • 能在 Demo 中复现平铺、镜像、边缘夹取、NEAREST、LINEAR 与 mipmap 效果

为什么贴图不只是把图片交给 GPU

上一章你已经能给三角形的每个顶点指定颜色,让 GPU 插值出一片渐变。可现实里的物体表面远不止纯色或渐变——一面砖墙、一张木桌、一个人物的脸,全是密密麻麻的花纹和细节。要靠顶点颜色画出砖墙的每一道缝,得堆上成千上万个顶点,谁也受不了。

换个思路:与其用几何体硬画细节,不如直接贴一张照片上去。就像给一面白墙贴壁纸、给玩偶穿一件印花衣服——墙还是那面墙,但看上去有了砖纹;衣服的图案不是缝出来的,是印上去的。这张「贴上去的图」就是本章的主角。

贴图要解决一个关键问题:图是方方正正的一张照片,可几何体的形状千奇百怪。这张图的每个位置,到底该贴到几何体的哪个点上? 没有一套说清「图的这个角对应物体的那个角」的规则,贴图就成了乱糊一气。这一章讲的就是这套规则,以及贴上去之后怎么把图贴得清楚、贴得自然。

纹理从图片变成可采样资源

decode · allocate · upload/mipmap · bind/sample/destroy

1. Decode imagewidth · height · channels

CPU pixels + flip policy

2. Create storageobject · target · levels

合法 texture storage

3. Upload + mipsformat/type · unpack

每级数据完整

4. Sample + destroyunit · sampler · texture()

正确颜色与资源释放

图片文件不是 texture object;必须先解码成有尺寸和 channels 的像素,再按 format/type 上传并建立完整 mip chain。

纹理:贴在表面的那张图

我们贴上去的这张图,在图形学里就叫 。图片文件先被解码成 CPU pixels,再上传到;它不是文件路径,也不是 sampler uniform。

为什么用它?因为它把「表面长什么样」和「物体是什么形状」彻底分开了:形状仍由那几个顶点定,表面细节全交给图。在管线里,纹理在片段着色器阶段起作用——上一章你已经知道,片段着色器负责给每个像素定颜色;有了纹理,这个颜色就可以不再是写死的,而是「从图里查出来的」。

纹理坐标(UV):图和几何体怎么对齐

要让 GPU 知道「图的哪个位置贴到几何体的哪个点」,靠的是 (常简称 UV)。它是一对范围在 0 到 1 之间的数:约定 (0, 0) 是图的左下角,(1, 1) 是图的右上角,正中间就是 (0.5, 0.5)。

做法和上一章给顶点配颜色一模一样:每个顶点附带一对 UV,告诉 GPU「我这个角对应图上的这个位置」。比如让三角形左下角顶点配 UV (0, 0)、右下角配 (1, 0)、顶部配 (0.5, 1),就等于说「把图的左下角钉在三角形左下角、右下角钉在右下角……」——图就被这几枚「图钉」绷在了三角形上。

下面这张图把这套对应关系画了出来:左边是 UV 坐标系(注意原点在左下),右边是纹理图,看四个顶点的 UV 如何一一钉住图的四角:

那三角形内部、不在顶点上的那些像素呢?还记得上一章讲的片段插值吗——和颜色一样,顶点的 UV 也会被光栅化按远近自动混合:顶点只钉了几个角的 UV,内部每个片段的 UV 都是「混」出来的。于是图就被平滑地、连续地铺满了整个三角形。

采样:拿着 UV 去图里取颜色

有了每个片段的 UV,片段着色器就能干一件事:拿这对 UV 当「图上的地址」,去纹理里取出那个位置的颜色。这个「按坐标去图里取色」的动作叫 (sampling)。

整条链路串起来就是:顶点给 UV → 光栅化把 UV 插值到每个片段 → 片段着色器拿着自己的 UV 去纹理里采样取色 → 输出。一张图就这么贴上了几何体。后面 §6 你会看到,采样在 GLSL 里只是一句 texture(...)

一次纹理采样包含四层决策

coordinate · address · LOD/filter · shader combine

1. UV attributevertex vec2 → interpolated

每片段坐标

2. Addresswrap S/T

越界坐标归约

3. LOD + filterMIN/MAG · mip selection

重建采样值

4. Combinetexture · mix · output

颜色进入 framebuffer

`texture()` 不只是数组索引;它结合 interpolated UV、wrap、derivative-derived LOD、mipmap 与 filtering 返回重建值。

纹理环绕:UV 超出 0~1 时怎么办

UV 约定在 0~1,但你完全可以给顶点配超出这个范围的 UV,比如 (0, 0)(3, 3)。这时 GPU 该怎么显示「图之外」的部分?这由 方式决定,常见三种:

  • REPEAT(重复):默认。把图一张挨一张地平铺。UV 拉到 (3, 3) 就铺成 3×3 张。
  • MIRRORED_REPEAT(镜像重复):也平铺,但每隔一张镜像翻转一次,让接缝处对齐、不突兀。
  • CLAMP_TO_EDGE(夹到边缘):超出的部分一律用最边上那排像素的颜色「拉长」填满,于是边缘像被向外抹开。

桌面 OpenGL 还提供 GL_CLAMP_TO_BORDER,越界时返回由 GL_TEXTURE_BORDER_COLOR 设置的颜色;WebGL2 不提供该 wrap mode,因此跨平台示例不能直接照搬。S/T 分别对应 U/V 两个方向,必须按需要各设一次。

光看文字不够直观——去本章 §5 的 Demo,把「UV 缩放」拉到大于 1(比如 3),再依次切这三种环绕,亲眼看平铺、镜像、边缘拉伸的差别。

纹理过滤:图被放大 / 缩小时怎么取色

UV 是连续的小数,纹理却是一格格离散的像素——当一个片段的 UV 落在「两格像素之间」(贴图被放大、一个纹理像素铺到了好几个屏幕像素时尤其明显),该取哪一格的颜色?这由 决定,两种基本方式:

  • NEAREST(邻近过滤):取离 UV 最近的那一格像素颜色。放大后能看到一块块清晰的「马赛克」方块——像素风游戏就爱用它。
  • LINEAR(线性过滤):在邻近几格之间按距离加权混合,得到平滑过渡。放大后柔和但偏糊。

同样去 §5 的 Demo:把「放大观察」拉到 12(等于把图放得很大),再对比 NEAREST 与 LINEAR——一个是硬边方块,一个是平滑渐变,差别一目了然。

多级渐远纹理(mipmap):远处缩小时怎么办

上面说的是放大。反过来,当物体离得很、一整张大图被挤进屏幕上区区几个像素时,又会出问题:屏幕一个像素要「代表」原图一大片像素,硬取一格会闪烁、出噪点。

解决办法是 mipmap):预先为这张图生成一整套逐级减半的缩小版(原图、1/2、1/4、1/8……),堆成一座「纹理金字塔」。物体离得越远、贴图显示得越小,GPU 就自动从金字塔里尺寸越合适的那一级去取色,省力又干净。

实际中通常一句 glGenerateMipmap 就让 GPU 自动算出整套金字塔(§6 会写到),你不必手动准备这些缩小版。

黑纹理通常是 Completeness Contract 破裂

base image · mip levels · legal filters · pixel transfer

1. Base leveldimensions + internal format

level 0 已定义

2. Mip chain1×1 or max level

MIN filter 所需 levels

3. Sampling paramswrap · MIN · MAG

MAG 不接受 mip filters

4. Pixel transferchannels · alignment · flip

行距和方向正确

使用 mipmap MIN filter 时必须定义所需 levels;MAG 只能 NEAREST/LINEAR。RGB 行距不满足默认 4-byte alignment 时要调整 unpack alignment。

是黑纹理排查的第一项。若 MIN_FILTER 使用 *_MIPMAP_*,从 base level 到所需最小尺寸的 levels 必须已定义或由 glGenerateMipmap 生成;MAG_FILTER 只能使用 NEARESTLINEAR,没有 mipmap 版本。

纹理单元:一次想用好几张图

到这里都是贴一张图。但很多效果要同时用好几张图——比如一张底图叠一张笑脸贴纸。问题来了:片段着色器里怎么分清「这是第 1 张、那是第 2 张」?

办法是 (texture unit):显卡提供一排带编号的「插槽」(0 号、1 号……至少 16 个)。流程是——先用 activeTexture 选中某个编号的插槽,把一张图绑上去;再告诉着色器里的某个采样器「你去几号插槽取图」。这样一来,着色器里就能区分多张纹理了。这也是为什么用多张图时既要 activeTexture、又要给每个采样器单独指定单元号。具体写法见 §6 最后一步。

Object、Unit、Binding 与 Sampler 是四个角色

select unit · bind object · point sampler · lookup

1. Active unitglActiveTexture(GL_TEXTURE0+i)

选择第 i 个插槽

2. Target bindingglBindTexture(TEXTURE_2D,id)

该 unit 的 2D binding

3. Sampler uniformuniform1i(location,i)

shader 保存 unit index

4. Texture lookuptexture(sampler,uv)

按状态返回 texel

Sampler uniform 保存整数 unit index,不保存 texture object ID;同一 unit 还可分别拥有不同 texture targets 的 bindings。

采样器 uniform 保存的是整数 unit index,例如 0,不是 GLuint texture 的 object ID,也不是 GL_TEXTURE0 枚举值。glActiveTexture(GL_TEXTURE0+i) 选择 unit,glBindTexture(GL_TEXTURE_2D,id) 修改该 unit 的 2D target binding,glUniform1i(location,i) 才把 shader sampler 指向同一 unit。可用 GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 等 limits 查询能力,不能把“至少 16”当作所有 stage 的任意可用数量。

图片上传:尺寸、Channels、Format 与行对齐

stbi_load 返回 width、height、channel count 和 pixel pointer。灰度、RGB、RGBA 输入应分别选择匹配的 source format;internal format 决定 GPU 怎样存储,source format/type 描述 CPU bytes 怎样解释。初学示例可令两者都为 GL_RGBGL_RGBA,但不能对四通道图片硬写 GL_RGB 后期待 alpha 保留。

int width = 0, height = 0, channels = 0;
stbi_set_flip_vertically_on_load(true);
unsigned char* pixels = stbi_load(path, &width, &height, &channels, 0);
if (!pixels) throw std::runtime_error(stbi_failure_reason());
 
GLenum format = channels == 4 ? GL_RGBA : channels == 3 ? GL_RGB : GL_RED;
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0,
             format, GL_UNSIGNED_BYTE, pixels);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(pixels);

默认 GL_UNPACK_ALIGNMENT=4。RGB 图片一行若是 width*3 bytes 且不能被 4 整除,驱动会按错误行距读下一行;上传前设 alignment 1,或提供满足对齐的 row layout。WebGL2 对应 UNPACK_ALIGNMENTUNPACK_FLIP_Y_WEBGL pixel-store state。颜色空间的 sRGB internal format 会在后续 Gamma Correction 章系统处理,本章先保持线性数据契约。

动手:换张图,调环绕、过滤、看采样

下面就是本章主 Demo。最上面一排「纹理」按钮可以切换贴在方块上的图:默认的 UV 测试图(棋盘格 + 网格线 + 四角异色 + 中心紫圆,专门用来看图被怎样平铺、采样),以及三张一眼认得出的真实感纹理——木板砖墙笑脸。同一套环绕/过滤/缩放规则作用在哪张图上都一样,换图只是为了让「贴图」这件事更直观。其余四组控件对应本章核心概念:环绕方式、过滤方式、UV 缩放(看环绕)、放大观察(看过滤)。

猜一猜:先切到砖墙,把「UV 缩放」拉到 3——一面砖墙会平铺成几面?再切 MIRRORED_REPEAT,砖缝在接缝处会不会对齐?然后切到笑脸,把「放大观察」拉到 12,对比 NEAREST 与 LINEAR:一张笑脸的眼睛边缘,一个是硬邦邦的马赛克块、一个是平滑发糊,差别一眼可见。先动手拖一拖,再看下面的代码。

可交互

纹理演示加载中…

调「UV 缩放」改的是顶点 UV 的范围(拉到 3 等于把 UV 推到了 (0,0)~(3,3),于是越界部分交给环绕方式处理);调「放大观察」相当于把图凑近放大看(一个纹理像素铺到很多屏幕像素,于是过滤方式的差别被放大)。这两件事正是 §3 讲的环绕与过滤——现在它们就在你手里。

代码逐段拆解

把贴图的全过程摊开看。大致分两摊:一摊在 CPU 侧(创建纹理、上传图片、设参数),一摊在着色器里(拿 UV 采样)。C++/OpenGL 与 WebGL2/TS 互为镜像。

第一步:创建并绑定一个纹理对象

先向显卡申请一个纹理对象拿到「句柄」,再把它绑定为「当前正在操作的纹理」——之后所有设参数、传数据的调用都作用在这个被绑定的纹理上。

unsigned int texture;
glGenTextures(1, &texture);              // 申请 1 个纹理对象,拿到句柄
glBindTexture(GL_TEXTURE_2D, texture);   // 绑定为「当前的 2D 纹理」
// 之后的 glTexImage2D / glTexParameteri 都作用在它身上

逻辑一一对应,差别在「申请」这一步的返回值:

第二步:设环绕和过滤参数

趁纹理还绑着,把 §3 讲的环绕(U / V 两个方向各设一次)和过滤(放大 MAG / 缩小 MIN 各设一次)定下来。这些参数就是 Demo 里那几个控件背后改的东西。

// 环绕:S=横向(U),T=纵向(V)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// 过滤:缩小用 LINEAR,放大用 NEAREST(举例)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

WRAP_S 对应 U(横向)、WRAP_T 对应 V(纵向),可以分开设。把 GL_REPEAT 换成 GL_MIRRORED_REPEAT / GL_CLAMP_TO_EDGE 就切换环绕方式;把 GL_NEAREST / GL_LINEAR 互换就切换过滤方式。

第三步:上传图片数据,并生成 mipmap

把图片的像素数据交给绑定中的纹理,靠 glTexImage2D / gl.texImage2D。它参数最多、最容易写错,逐个对照:

// data 是 stb_image 读出的像素;参数顺序见下方解说
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
             width, height, 0,
             GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);   // 自动生成整套缩小版(mipmap 金字塔)

C++ 版的 glTexImage2D 九个参数依次是:① 目标 GL_TEXTURE_2D;② mipmap 级别 0(基础级);③ 显卡里存成什么格式(内部格式,如 GL_RGB);④⑤ 图宽、图高;⑥ 边框,永远填 0;⑦ 你传进来的数据是什么格式(源格式);⑧ 数据类型 GL_UNSIGNED_BYTE;⑨ 数据指针。glGenerateMipmap 则让 GPU 一次性算出 §3 那座金字塔。

第四步:顶点带上 UV 坐标

光有纹理还不够,得告诉每个顶点它对应图上的哪个位置——也就是把 UV 加进顶点数据,再像上一章配置颜色属性那样,给它配一个顶点属性指针。

// 顶点数据每行:3 个位置 + 2 个 UV(这里省略位置,只看 UV 在第 4、5 列)
// 例:位置 x,y,z, u,v —— 每个顶点 5 个 float
// 把第 2 号属性(UV,vec2)指向每行偏移 3*float 处,步长 5*float:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE,
                      5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(2);

2 是属性位置编号(对应顶点着色器里 layout (location = 2) in vec2 aTexCoord;);2 是每个 UV 有 2 个分量;步长和偏移按每行 5 个 float、UV 在第 4 个起算。WebGL2 里 sizeof(float) 就是固定的 4 字节。

第五步:着色器里采样

顶点着色器把 UV 用 out 递给片段着色器(中途被插值);片段着色器声明一个 接住纹理,再用 拿 UV 采样取色。两端 GLSL 源码几乎一致,只差版本声明那行:

// —— 顶点着色器 —— 把 UV 递下去
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoord;
out vec2 TexCoord;
void main() {
  gl_Position = vec4(aPos, 1.0);
  TexCoord = aTexCoord;        // 递给片段着色器(会被插值)
}

片段着色器接住插值后的 TexCoord,用 texture() 去采样——这就是 §3「采样」落到代码上的那一句:

// —— 片段着色器 —— 采样取色
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D ourTexture;     // 代表那张纹理
void main() {
  FragColor = texture(ourTexture, TexCoord);   // 按 UV 采样
}

第六步:用多张图——纹理单元 + mix()

要同时用两张图,就轮到 §3 的纹理单元了:把两张图分别绑到 0 号、1 号单元,再告诉着色器里两个采样器各去几号单元取图。CPU 侧这样安排:

glUseProgram(program);
// 告诉着色器:采样器 texture1 去 0 号单元,texture2 去 1 号(用 1i 传整数单元号)
glUniform1i(glGetUniformLocation(program, "texture1"), 0);
glUniform1i(glGetUniformLocation(program, "texture2"), 1);
// 每帧绘制前:选中单元,绑上对应纹理
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex1);
glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, tex2);

注意采样器接的不是纹理本身,而是「几号单元」——所以用 glUniform1i 传的是单元的整数编号01),不是纹理对象。着色器里则声明两个 sampler2D,用 把两次采样结果按比例混合:

#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D texture1;
uniform sampler2D texture2;
void main() {
  // 80% 第一张 + 20% 第二张
  FragColor = mix(texture(texture1, TexCoord),
                  texture(texture2, TexCoord), 0.2);
}

mix(a, b, 0.2) = 0.8*a + 0.2*b:第三个参数从 0 调到 1,画面就从全是第一张图渐变到全是第二张图。完整可运行源码将在章末提供(M5)。

三步验证纹理采样链

分步1 / 3

第一步:先验证 Pixels 与 Texture Object

用 2×2 已知颜色测试图核对 width、height、channels、Y flip、source/internal format 和 unpack alignment;只在 level 0 正确后生成 mipmaps。

一次纹理采样包含四层决策

coordinate · address · LOD/filter · shader combine

1. UV attributevertex vec2 → interpolated

每片段坐标

2. Addresswrap S/T

越界坐标归约

3. LOD + filterMIN/MAG · mip selection

重建采样值

4. Combinetexture · mix · output

颜色进入 framebuffer

`texture()` 不只是数组索引;它结合 interpolated UV、wrap、derivative-derived LOD、mipmap 与 filtering 返回重建值。

容易踩的坑

小结

  • 纹理就是贴到表面的一张图,把「长什么样」和「是什么形状」分开,省去用海量顶点硬画细节
  • **纹理坐标(UV)**逐顶点指定(原点左下、范围 0~1),内部每个片段的 UV 由插值「混」出来
  • 环绕决定 UV 越界(>1<0)时怎么显示:REPEAT 平铺 / MIRRORED 镜像 / CLAMP_TO_EDGE 拉边
  • 过滤决定放大缩小时怎么取色:NEAREST 出马赛克硬块 / LINEAR 平滑;远处缩小用 mipmap 防闪烁
  • 着色器里用 sampler2D + texture() 采样;多张图靠纹理单元区分,再用 mix() 按比例混合

练习

问题 1: 在上面的 Demo 里调出两个指定效果:①把同一张图平铺成 3×3;②让放大后的棋盘格呈现清晰的马赛克硬块(而非平滑过渡)。分别说出你动了哪个 / 哪些控件、为什么。

问题 2: 顶点只有 3 个角、各配了一对 UV,可三角形内部成千上万个片段,每个片段的 UV 是哪来的?这和上一章顶点颜色变成渐变是同一个机制吗?

问题 3: 不看上面的代码,写出「在 WebGL2 里把一张已加载好的图片 image 贴到几何体上」的关键步骤:从创建纹理到能在片段着色器里采样。目标效果:方块上原样显示这张图。

名词解释

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

纹理

贴到几何体表面的一张图(通常就是一张图片)。用它给物体表面加细节——砖纹、木纹、花色,省得用成千上万个顶点去硬画。本质是一块存在显卡上、能按坐标查颜色的数据。详见本章「纹理」一节。

纹理对象

保存纹理尺寸、internal format、mip levels 和采样参数的 OpenGL resource;图片文件必须先解码并上传,sampler 再通过 texture unit 间接访问它。

纹理坐标

给每个顶点附带的一对坐标 (u, v),告诉 GPU「这个顶点对应纹理图上的哪个位置」。范围 0~1:(0,0) 是图的左下角(1,1) 是右上角。常简称 UV。详见本章「纹理坐标」一节。

采样

片段着色器拿着自己那对 UV 坐标,去纹理图里取出对应位置颜色的动作。在 GLSL 里就是一句 texture(纹理, UV)。是「贴图」最终落到每个像素上的那一步。详见本章「采样」一节。

纹理环绕

UV 坐标超出 0~1 时,GPU 怎么取色的规则。三种常见:REPEAT(一张张重复平铺)、MIRRORED_REPEAT(平铺但每隔一张镜像翻转)、CLAMP_TO_EDGE(用最边上的像素拉长填满)。横向(U)、纵向(V) 可分开设。详见本章「纹理环绕」一节。

纹理过滤

片段的 UV 落在「两格纹理像素之间」时怎么取色的规则。NEAREST:取最近一格,放大后是马赛克硬块;LINEAR:在邻近几格间按距离混合,放大后平滑。分放大(MAG)、缩小(MIN) 两种情况设。详见本章「纹理过滤」一节。

多级渐远纹理

为一张纹理预先生成的一整套逐级减半的缩小版(原图、1/2、1/4、1/8……),堆成一座「纹理金字塔」。物体离得远、贴图被缩得小时,GPU 自动从尺寸合适的那一级取色,避免远处闪烁、噪点。常简称 mipmap。详见本章「多级渐远纹理」一节。

纹理完整性

Base level、所需 mip levels、尺寸格式和采样参数满足 OpenGL 可采样规则的状态;不完整纹理常表现为黑色采样结果。

纹理单元

显卡上一排带编号的「插槽」(0 号、1 号……至少有 16 个)。把每张纹理绑到一个编号的单元上,再告诉着色器里每个采样器去第几号单元取图,就能在一次绘制里同时用多张纹理。详见本章「纹理单元」一节。

sampler2D

GLSL 里代表一张 2D 纹理的采样器类型。它本身不存图,而是「指向某个纹理单元」的句柄;配合 texture() 用它去采样取色。详见本章「第五步:着色器里采样」。

texture()

GLSL 的内建采样函数。texture(sampler, uv) 表示「拿着 uv 坐标,去 sampler 指向的那张纹理里取出对应颜色」,返回一个 vec4(rgba)。是「采样」落到代码上的那一句。详见本章「第五步:着色器里采样」。

mix()

GLSL 的线性插值函数。mix(a, b, t) 按比例 t 混合:t=0 全取 at=1 全取 bt=0.2 是 80%a + 20%b。常用来把两张纹理的采样结果按比例叠在一起。详见本章「第六步:用多张图」。

版本、来源与运行边界

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

正式概念与状态责任

  • texture:在“纹理对象、采样状态与多纹理绑定”中由纹理对象和当前 active texture unit负责解释其输入、受控状态和可观察结果;运行时以纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素定位它的第一处变化。
  • texture unit:在“纹理对象、采样状态与多纹理绑定”中由纹理对象和当前 active texture unit负责解释其输入、受控状态和可观察结果;运行时以纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素定位它的第一处变化。
  • wrapping:在“纹理对象、采样状态与多纹理绑定”中由纹理对象和当前 active texture unit负责解释其输入、受控状态和可观察结果;运行时以纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素定位它的第一处变化。
  • filtering:在“纹理对象、采样状态与多纹理绑定”中由纹理对象和当前 active texture unit负责解释其输入、受控状态和可观察结果;运行时以纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“上传 texel,配置采样参数,生成 mipmap,并把 sampler 指向已绑定 unit”发生后,纹理对象和当前 active texture unit应怎样改变像素格式、尺寸、wrap/filter、mipmap 完整性、unit 绑定和 sampler 整数;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

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

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

Context · resource · observable result

纹理对象、采样状态与多纹理绑定:状态合同

把图片字节、纹理对象、采样状态、mipmap、texture unit 与 sampler 对齐

验证场景

官方教程正式概念

logl-05 · 基线帧

texture固定 context、资源内容与输入事件,执行“上传 texel,配置采样参数,生成 mipmap,并把 sampler 指向已绑定 unit”

状态所有者纹理对象和当前 active texture unit
受控状态/资源像素格式、尺寸、wrap/filter、mipmap 完整性、unit 绑定和 sampler 整数
触发命令上传 texel,配置采样参数,生成 mipmap,并把 sampler 指向已绑定 unit

冻结输入:texture

纹理对象和当前 active texture unit记录像素格式、尺寸、wrap/filter、mipmap 完整性、unit 绑定和 sampler 整数

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

观测:纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素中的初始快照

预期:纹理对象和当前 active texture unit得到可复查结果,并持续满足“采样所需 mip 层完整,sampler 的整数值表示纹理单元编号而不是对象 ID”

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

逐段执行“上传 texel,配置采样参数,生成 mipmap,并把 sampler 指向已绑定 unit”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“采样所需 mip 层完整,sampler 的整数值表示纹理单元编号而不是对象 ID”。

CPU command · GL state · GPU result

纹理对象、采样状态与多纹理绑定:五段轨迹

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

当前观测:纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素中的初始快照

不变量:采样所需 mip 层完整,sampler 的整数值表示纹理单元编号而不是对象 ID

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

注入“min filter 要求 mipmap,却既未上传也未生成 mip 层,纹理变成不完整”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素一起恢复才算修复。

Single fault · first divergence · replay

纹理对象、采样状态与多纹理绑定:反例与恢复

故障:min filter 要求 mipmap,却既未上传也未生成 mip 层,纹理变成不完整

1. 冻结输入一致

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

2. 注入单故障一致

保持其余输入不变,仅注入“min filter 要求 mipmap,却既未上传也未生成 mip 层,纹理变成不完整”

3. 定位首差一致

采样所需 mip 层完整,sampler 的整数值表示纹理单元编号而不是对象 ID

4. 清理并重放一致

纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素

最小可重放检查

unit: logl-05
owner: 纹理对象和当前 active texture unit
state_or_resource: 像素格式、尺寸、wrap/filter、mipmap 完整性、unit 绑定和 sampler 整数
command: 上传 texel,配置采样参数,生成 mipmap,并把 sampler 指向已绑定 unit
pass_invariant: 采样所需 mip 层完整,sampler 的整数值表示纹理单元编号而不是对象 ID
single_fault: min filter 要求 mipmap,却既未上传也未生成 mip 层,纹理变成不完整
required_evidence: 纹理级别查询、unit/target 绑定、sampler 值、GL 错误与采样像素

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

出处声明

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

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

讨论

评论区加载中…