实例化、批次边界与每实例矩阵

实例化、批次边界与每实例矩阵:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能实现一次实例化绘制一大片相同网格:把 glDrawArrays 换成 glDrawArraysInstanced(..., instanceCount),一次调用画 instanceCount 个,而不是循环调用 instanceCount
  • 能实现「每个实例摆在不同位置」:用 gl_InstanceID 配 uniform 数组取偏移,或建一个实例化数组glVertexAttribDivisor(loc, 1),让每个实例读到属于自己那一份的偏移 / 变换矩阵
  • 能判断:实例化数组的属性忘了设 glVertexAttribDivisor(loc, 1),画面会变成什么样?为什么?一个多子网格模型实际还会保留多少绘制批次?

为什么重复小网格会卡住 CPU

想象你要在纸上印一万个一模一样的小图章。笨办法是:拿起图章、蘸墨、盖一个,再拿起、再蘸、再盖……重复一万次。盖图章这个动作本身很快,但每次「拿起、吩咐手去盖」的来回才是真正拖慢你的——一万次来回,胳膊先废了。

聪明办法是:跟工厂说一句「照这一个模子,盖一万个,每个盖在哪、盖多大,看这张表」。模子只有一个,表上一行写一个位置。工厂一口气盖完一万个,而你只吩咐了一遍

这一章解决的就是:要画很多个一模一样的东西时,怎么只「吩咐」一次、而不是一个一个地吩咐一万遍。没有它,画一千个绕行星飞的小行星就得发起一千次绘制请求,CPU 可能被反复提交命令拖成瓶颈;有了它,提交次数会显著减少。它不保证任何数量都流畅:GPU 仍要处理每个实例的顶点和片段,最终瓶颈要靠 profiler 判断。

实例化:一次 draw call 画一整片

把「吩咐一次、盖一万个」翻译成图形术语,就是 (instancing)。它的前提是:你要画的这一大片东西在同一个批次里共享网格、材质和兼容状态。区别只在每个的位置、大小、朝向(统称「变换」)或每实例属性不同。实例化就是把「这一份几何」连同「每个实例各自的变换」一次性交给 GPU,让它一口气画完。

为什么要这么做?关键在 。每次你调用 glDrawArraysglDrawElements,CPU 都要把绘制命令打包、发给 GPU。当每个物体很小而调用数很大时,这段提交成本常比实际栅格化更早成为瓶颈;实例化只针对这段 CPU 成本,不会让 GPU 少处理每个实例的顶点和片段。下面这张图把「N 次喊话 vs 1 次喊话」摆在一起:

不实例化:CPU 喊 N 遍CPUGPUdraw call 0draw call 1draw call 2draw call 3… draw call NN 次通信 → CPU 瓶颈实例化:CPU 喊 1 遍CPUGPU1 次:画 N 个instanceCount = N1 次通信 → 流畅实例化省的是「CPU 反复喊话发起 draw call」的开销:从 N 次降到 1 次
每次 draw call 都是一趟「CPU 喊话 → GPU 干活」的通信,开销在喊话本身。画 N 个相同物体:不实例化要喊 N 遍(CPU 瓶颈);实例化只喊 1 遍instanceCount = N),GPU 一口气画完。

实例化把同一个兼容批次的 N 次 draw call 压成 1 次。你只调用一次带 instanceCount 的绘制函数,GPU 内部就把同一份几何重复画 instanceCount 遍。那「每个实例摆在哪」的信息怎么进去?这是接下来两节的事。先看这张「模子 + 每实例变换表 → 盖出 N 个」的总览:

一个网格「模子」同一份几何只存一份 VBO配一张表每实例变换表(每行一个实例)[0]offset / 变换 #0[1]offset / 变换 #1[2]offset / 变换 #2[3]offset / 变换 #3… 第 N 行第 i 行 = 着色器里 gl_InstanceID == i 那一份照表盖出盖出 N 个实例01234一次 draw call 全画完同一个模子只存一份 + 一张每实例变换表 → GPU 照表盖出 N 份,一次 draw call
实例化的核心:同一个网格只存一份,配一张「每实例一行」的变换表(第 i 行对应着色器里 gl_InstanceID == i);GPU 照表把这个模子盖出 N 份、各按自己那行的偏移摆好,整批只用一次 draw call

这里的“1 次”有边界:同一个网格、材质和兼容渲染状态可组成一个实例化批次;不同网格、不同材质、不同 shader / pipeline 仍要分批。例如一个行星模型若有 M 个子网格,通常要对每个子网格各做一次 glDrawElementsInstanced。实例化减少的是 CPU / 驱动提交次数,不是让 GPU 少画 instanceCount 份三角形。

怎么让每个实例不一样:gl_InstanceID 与实例化数组

同一个模子盖出来的 N 个实例,如果都摆在同一个位置,就会完全重叠成一个——没意义。所以必须让每个实例读到「属于自己那一份」的变换。OpenGL 给了两条路。

第一条路靠一个内置变量 。在顶点着色器里,它告诉你「现在画的是第几个实例」——第 0 个实例时它是 0,第 1 个实例时是 1,依此类推。你可以拿它当下标去一个 uniform 数组里取偏移:offsets[gl_InstanceID],于是第 i 个实例就读到 offsets[i]、摆到不同位置。简单直接,但有个硬上限:uniform 数组不能太大(受 GPU 的 uniform 容量限制),塞个一百个偏移还行,要塞一万个就爆了。

原书的第一个基准:一次调用画 100 个彩色四边形

原教程先用最小案例建立直觉:一个四边形由 2 个三角形、共 6 个顶点组成;在 NDC 中准备一个 10 x 10translations[100] 偏移表,顶点着色器用 gl_InstanceID 选自己的偏移。关键链路只有三步:

// vertex shader
uniform vec2 offsets[100];
void main() {
  vec2 offset = offsets[gl_InstanceID];
  gl_Position = vec4(aPos + offset, 0.0, 1.0);
  fColor = aColor;
}
// CPU:生成 10 x 10 偏移并上传 offsets[0..99]
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);

这说明 gl_InstanceID 不是“第几个顶点”,而是第几个四边形实例:6 个顶点共享同一个 ID;下一个四边形的 6 个顶点才切换到下一个 ID。这个 uniform 数组版本适合学习和少量实例,后面才用 VBO 解决容量上限。

第二条路没有这个 uniform 上限,叫 (instanced array)。思路是把「每实例的数据」(每个实例的偏移、甚至整个变换矩阵)放进一个 VBO,像普通顶点属性那样在着色器里 in 进来。但这里有个关键差别要交代清楚:普通顶点属性是每个顶点读一条,而实例化数组要的是每个实例才读一条。这个「多久读一条」的开关,就是下一节的主角。

核心难点:glVertexAttribDivisor —— 逐顶点还是逐实例

实例化里最该一次搞懂的,就是 。函数是 glVertexAttribDivisor(location, divisor),它给某个顶点属性设一个「步进频率」:

  • divisor = 0(默认):逐顶点步进。着色器每处理一个顶点,就往下读这个属性的下一条。普通的位置、法线、纹理坐标都是这样——每个顶点各有各的。
  • divisor = 1每实例步进。着色器只有每开始画一个新实例时,才往下读一条;同一个实例内部的所有顶点,共用同一条

实例化数组要的正是 divisor = 1——一个实例的所有顶点应该共享同一份「这个实例的偏移 / 矩阵」。下面这张图把两种步进并排掰碎,是本章最该盯着看的一张:

divisor = 0:逐顶点步进(每个顶点读一条)v0v1v2v3顶点条目 0条目 1条目 2条目 3属性表每处理一个顶点 → 往下读一条(4 个顶点读 4 条)divisor = 1:每实例步进(一条覆盖整个实例的所有顶点)实例 0(4 个顶点)实例 1(4 个顶点)实例 2(4 个顶点)实例条目 0(偏移/矩阵)条目 1(偏移/矩阵)条目 2(偏移/矩阵)实例化数组每开始一个新实例才往下读一条(3 个实例只读 3 条)
glVertexAttribDivisor(loc, 0) 逐顶点(每个顶点读一条)vs (loc, 1) 每实例(一条覆盖整个实例所有顶点)。实例属性忘设 1=被当逐顶点读、所有实例叠一起。

一句话记住:普通属性逐顶点读(divisor=0),实例化数组逐实例读(divisor=1)。忘了把实例化数组设成 divisor=1,它就会被当成逐顶点属性,每个实例的偏移全错——这是本章头号大坑(§7 第一个)。

行星带:把整个变换矩阵当实例化属性

把上面这套用到极致,就是原书的 (asteroid field):上千上万颗小行星绕着一颗行星排成一圈飞。每颗小行星是同一个网格,但位置、缩放、朝向各不相同——这正好是「整个变换矩阵每实例一份」。于是给每个实例准备一个 mat4 变换矩阵,作为实例化数组属性。只是 mat4 太大、一个属性位置只能装一个 vec4,所以它要占用 4 个连续的属性位置,每个都设 divisor=1(§6 第三段代码就是这么做的)。本章 Demo 画的就是这样一条程序化的行星带——你能亲手把它从一百颗拖到一万颗。

动手:把实例数从 100 拖到 10000

猜一猜:下面这块画布里是一整条「行星带」,成百上千颗小行星全是同一个程序化小立方体。现在拖动「实例数」滑块,把它从 100 一路拉到 10000:哪一项成本被保持为常数,哪一项仍在增长?先拖一下试试,再读解释。

这块画布严格对应本章核心:所有小行星由一个 <instancedMesh> 画出(three.js 的实例化网格,底层就是 gl.drawElementsInstanced 一次 draw call)。同一份立方体几何只存一份,每颗小行星的位置 / 缩放 / 朝向由一份「每实例变换矩阵」决定,颜色按实例编号程序化生成——没有任何外部模型或贴图。拖动滑块改的只是「画多少个实例」(mesh.count),不重建任何几何:

可交互

实例化「行星带」演示加载中…

拖到 10000 时,这个演示仍维持一个实例化网格,因此右上角的示意提交数恒为 1。作为对比,旁边标着「不实例化需要的次数」会随滑块一路涨到一万:如果真的一颗一颗画,CPU 要发起一万次 draw call。这里的画布刻意只有一个网格和材质,用来隔离“提交次数”这个变量;真实模型有多个子网格 / 材质时,实例化提交数是每个批次一次,而不是全场景永远一次。GPU 的顶点、片段、带宽和过绘仍随实例数增加,是否卡顿要看设备和 profiler。

单步走一遍:divisor 到底改了什么

猜一猜:普通顶点属性(位置、法线)和实例化数组属性(每实例的偏移矩阵),它们读数据的频率一样吗?如果把实例化数组当成普通属性那样「逐顶点读」,会发生什么?单步走一遍就明白了。

下面把 glVertexAttribDivisor 的语义拆成三步,每一步配一张图,盯着「着色器多久才往下读一条属性」:

分步1 / 3

① divisor=0(默认):每个顶点读一条

divisor = 0:逐顶点步进(每个顶点读一条)v0v1v2v3顶点条目 0条目 1条目 2条目 3属性表每处理一个顶点 → 往下读一条(4 个顶点读 4 条)
divisor=0(逐顶点):每处理一个顶点就读表的下一条—— 普通的位置 / 法线 / uv 都这样。

普通顶点属性(位置 / 法线 / uv)的步进方式:着色器 每处理一个顶点 ,就从属性表里往下读一条。一个网格有 4 个顶点,就读 4 条,一条对一个顶点。这是逐顶点步进,divisor 的默认值就是 0

走完这遍就清楚了:divisor 只决定一件事——这个属性多久往下读一条。普通属性逐顶点读(0),实例化数组逐实例读(1)。一个数字之差,整片实例排得齐还是乱。

代码逐段拆解

实例化的代码分三块:实例化绘制调用gl_InstanceID + uniform 数组取偏移实例化数组 + divisor。三块对照看,C++/OpenGL 与 WebGL2/TS 互为镜像。好消息是 WebGL2 原生支持实例化gl.drawArraysInstanced / gl.vertexAttribDivisor 等),不像几何着色器那样缺席。

第一块:实例化绘制调用

把普通的 glDrawArrays / glDrawElements 换成带 instanceCount 的实例化版本,一次画 instanceCount 个:

// 普通:一次画一个网格(36 个顶点的立方体)
glDrawArrays(GL_TRIANGLES, 0, 36);
 
// 实例化:一次画 amount 个(最后多一个 instanceCount 参数)
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, amount);
 
// 带索引的版本(行星带用这个)
glDrawElementsInstanced(GL_TRIANGLES, indexCount,
                        GL_UNSIGNED_INT, 0, amount);

就这一行的差别:Instanced 版本末尾多一个 instanceCount(这里是 amount),GPU 就把同一份几何重复画 amount 遍。但光这样画出来的 amount 个会完全重叠——还得告诉每个实例「你摆哪」,这是下面两块的事:

第二块:gl_InstanceID + uniform 数组取偏移(少量实例)

实例少时,最简单的做法是把每实例偏移塞进一个 uniform 数组,顶点着色器用 gl_InstanceID 当下标取出自己那一份:

#version 300 es
layout(location = 0) in vec2 aPos;
uniform vec2 offsets[100];   // 每实例一个偏移,上限受 GPU uniform 容量限制
void main() {
  // gl_InstanceID:当前是第几个实例(0,1,2…),拿它取自己那份偏移
  vec2 offset = offsets[gl_InstanceID];
  gl_Position = vec4(aPos + offset, 0.0, 1.0);
}

这招简单,但 offsets[100] 这个 uniform 数组有大小上限——GPU 的 uniform 容量有限,塞一百个还行,要画一万个就远远不够了。实例一多,就得换成下面的实例化数组:

第三块:实例化数组 + glVertexAttribDivisor(海量实例)

把每实例数据放进 VBO、当顶点属性传,并用 glVertexAttribDivisor(loc, 1) 设成「每实例读一条」。先看最简单的 vec2 偏移:

// 把每实例偏移放进一个 VBO
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100,
             &translations[0], GL_STATIC_DRAW);
// 像普通属性那样设 location=2
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
// 关键:每实例才读一条(不是每顶点)
glVertexAttribDivisor(2, 1);

顶点着色器把它当普通 in 属性收:layout(location = 2) in vec2 aOffset;,然后 gl_Position = vec4(aPos + aOffset, 0.0, 1.0);。重点是那句 glVertexAttribDivisor(2, 1)——少了它,aOffset 会被当逐顶点读、整片实例错乱。

行星带要的不是 vec2 偏移、而是整个 mat4 变换矩阵。mat4 一个属性位置装不下(一个属性位最多 vec4),所以它占 4 个连续的属性位置,每个都要单独设 divisor=1

glBindBuffer(GL_ARRAY_BUFFER, matrixVBO); // 存了 amount 个 mat4
GLsizei vec4Size = sizeof(glm::vec4);
// mat4 = 4 列 vec4,占 location 3、4、5、6 四个属性位
for (int i = 0; i < 4; i++) {
  glEnableVertexAttribArray(3 + i);
  glVertexAttribPointer(3 + i, 4, GL_FLOAT, GL_FALSE,
                        4 * vec4Size, (void*)(i * vec4Size));
  glVertexAttribDivisor(3 + i, 1); // 4 个位置都要逐实例
}

对照看:mat4 instanceMatrix 在着色器里只占一个 layout(location = 3),但 CPU 端要把它拆成 4 个 vec4 属性位3456)逐个 vertexAttribPointer + vertexAttribDivisor(., 1)。配好后每个兼容子网格调用一次 glDrawElementsInstanced(..., amount),就能高效绘制整条行星带——这正是上面 Demo 抽象出的底层做法。

原书的行星带在每颗小行星上预先生成一份 mat4:沿半径为 radius 的圆环平移,再加入有限范围的随机位移、缩放和旋转。上传矩阵 VBO 后,每个 rock 子网格的 VAO都必须记录这四个实例属性;渲染时按子网格循环调用 glDrawElementsInstanced。这既保留模型的多网格结构,也把“每颗小行星一次调用”降为“每种子网格一次调用”。

容易踩的坑

小结

  • 实例化 = 一次 draw call 画一个兼容批次中的很多个相同网格:把 glDrawArrays 换成 glDrawArraysInstanced(..., instanceCount),省的是 CPU 反复发起 draw call 的通信开销,不消除 GPU 的每实例工作
  • 让每个实例不一样有两条路:gl_InstanceID 当下标取 uniform 数组偏移(实例少时简单,有大小上限),或实例化数组(每实例一份属性,画海量实例)
  • glVertexAttribDivisor(loc, divisor) 是核心:0 逐顶点读、1 每实例读;实例化数组必须设 1,忘了就被当逐顶点、整片错乱
  • 行星带mat4 实例化矩阵:一个 mat44 个连续属性位,每个都要 divisor=1;对每个 rock 子网格各提交一次实例化绘制
  • 实例化只能在同一调用内画同一网格 / 材质批次的多份(变换 / 属性可不同、几何不能换)

练习

问题 1(改 Demo 代码) 上面的 InstancingDemo 只有一个网格和材质,所以 10000 个实例仍属于 1 个兼容批次。现在假设有人去掉实例化、改成循环逐个绘制(每颗小行星调一次 gl.drawElements)。① 画 5000 颗时会发起多少次 draw call?② 这种写法优先放大了哪一段成本?③ 若真实小行星模型有 3 个兼容子网格,实例化后最少还要几次 draw call?

问题 2(排错题) 某人把每实例偏移 vec2 放进 VBO、glVertexAttribPointer(2, 2, GL_FLOAT, ...) 设好了 location=2,顶点着色器也写了 layout(location=2) in vec2 aOffset;,然后 glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100)。结果实例的位置错乱,若 VBO 只准备了 100 条偏移还可能发生越界属性读取。他漏了哪一句?为什么?

问题 3(独立实现题) 你要画一片 1000 棵的草地,每棵草是同一个网格,但位置不同、且各自有不同的随机朝向(绕 Y 轴旋转一个随机角度)。问:① 每棵草的「位置 + 旋转」该用什么数据结构当实例化属性最合适?② 这个数据结构在 CPU 端配置 vertexAttribPointer 时有什么特别之处?

名词解释

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

实例化

一种绘制技术:用一次 draw call 画出一个兼容批次中很多个相同的网格(同一份几何),而不是循环调用很多次。每个实例几何完全一样,只是各自的位置 / 缩放 / 朝向(变换)或某些属性不同。不同网格、材质或不兼容状态仍要分批。常用来画大量重复物体:草地、森林、粒子、行星带。详见本章「实例化」一节。

draw call 开销

一次 draw call(如 glDrawArrays)背后是一趟「CPU 把绘制命令打包、发给 GPU」的通信,含驱动校验、状态切换和命令提交等成本。当物体小而调用数大时,CPU 反复发起成千上万次 draw call 往往更早成为瓶颈;GPU 的顶点和片段工作仍取决于实际绘制量。实例化把一个兼容批次的 N 次 draw call 压成 1 次,正是省这笔提交成本。详见本章「实例化:一次 draw call 画一整片」一节。

gl_InstanceID

顶点着色器里的一个内置整数,表示「当前画的是第几个实例」,从 0 开始递增(第 0 个实例是 0、第 1 个是 1……),只在实例化绘制时有意义。常拿它当下标去取每实例数据,比如 offsets[gl_InstanceID],给每个实例一个不同偏移。详见本章「怎么让每个实例不一样」一节。

实例化数组

一种「每个实例一份」的顶点属性:把每实例的数据(偏移向量、或整个变换矩阵)放进一个 VBO,像普通顶点属性那样设好,再用 glVertexAttribDivisor(loc, 1) 告诉 GPU「这个属性每个实例才读一条、不是每个顶点」。它不受 vertex uniform 数组容量限制,但仍受 VBO 内存、顶点属性槽和传输带宽约束。详见本章「怎么让每个实例不一样」一节。

实例化步进 divisor

glVertexAttribDivisor(loc, divisor) 设某个顶点属性「多久往下读一条」:divisor=0(默认)是逐顶点——每处理一个顶点就读下一条;divisor=1每实例——每开始一个新实例才读下一条,同一实例内所有顶点共用一条。实例化数组必须设 1,否则被当逐顶点读、整片错乱。详见本章「核心难点:glVertexAttribDivisor」一节。

行星带

实例化的经典案例:上千上万颗小行星绕一颗行星排成一圈飞。每颗是同一个网格,但位置 / 缩放 / 朝向各异,于是给每个实例一个 mat4 变换矩阵当实例化数组属性。每个兼容 rock 子网格各做一次实例化绘制;因为 mat4 太大,要占 4 个连续属性位置、各设 divisor=1。详见本章「行星带」一节。

版本、来源与运行边界

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

正式概念与状态责任

  • instancing:在“实例化、批次边界与每实例矩阵”中由VAO 的逐顶点/逐实例属性与 instanced draw call负责解释其输入、受控状态和可观察结果;运行时以VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换定位它的第一处变化。
  • gldrawarraysinstanced:在“实例化、批次边界与每实例矩阵”中由VAO 的逐顶点/逐实例属性与 instanced draw call负责解释其输入、受控状态和可观察结果;运行时以VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换定位它的第一处变化。
  • gl_instanceid:在“实例化、批次边界与每实例矩阵”中由VAO 的逐顶点/逐实例属性与 instanced draw call负责解释其输入、受控状态和可观察结果;运行时以VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换定位它的第一处变化。
  • instanced array:在“实例化、批次边界与每实例矩阵”中由VAO 的逐顶点/逐实例属性与 instanced draw call负责解释其输入、受控状态和可观察结果;运行时以VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“上传实例数据,为每列配置 attribute+divisor,再调用 instanced draw”发生后,VAO 的逐顶点/逐实例属性与 instanced draw call应怎样改变instance count、attribute locations、stride/offset、divisor、gl_InstanceID 和矩阵;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

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

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

Context · resource · observable result

实例化、批次边界与每实例矩阵:状态合同

把共享 mesh 与 per-instance 偏移/mat4 属性分开,并用 divisor 控制推进频率

验证场景

官方教程正式概念

logl-27 · 基线帧

instancing固定 context、资源内容与输入事件,执行“上传实例数据,为每列配置 attribute+divisor,再调用 instanced draw”

状态所有者VAO 的逐顶点/逐实例属性与 instanced draw call
受控状态/资源instance count、attribute locations、stride/offset、divisor、gl_InstanceID 和矩阵
触发命令上传实例数据,为每列配置 attribute+divisor,再调用 instanced draw

冻结输入:instancing

VAO 的逐顶点/逐实例属性与 instanced draw call记录instance count、attribute locations、stride/offset、divisor、gl_InstanceID 和矩阵

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

观测:VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换中的初始快照

预期:VAO 的逐顶点/逐实例属性与 instanced draw call得到可复查结果,并持续满足“mat4 四列占连续 location 且每列 divisor=1;instance count 不越过缓冲”

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

逐段执行“上传实例数据,为每列配置 attribute+divisor,再调用 instanced draw”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“mat4 四列占连续 location 且每列 divisor=1;instance count 不越过缓冲”。

CPU command · GL state · GPU result

实例化、批次边界与每实例矩阵:五段轨迹

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

当前观测:VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换中的初始快照

不变量:mat4 四列占连续 location 且每列 divisor=1;instance count 不越过缓冲

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

注入“只给 mat4 第一列设置 divisor,后三列每个顶点推进,实例矩阵被撕裂”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换一起恢复才算修复。

Single fault · first divergence · replay

实例化、批次边界与每实例矩阵:反例与恢复

故障:只给 mat4 第一列设置 divisor,后三列每个顶点推进,实例矩阵被撕裂

1. 冻结输入一致

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

2. 注入单故障一致

保持其余输入不变,仅注入“只给 mat4 第一列设置 divisor,后三列每个顶点推进,实例矩阵被撕裂”

3. 定位首差一致

mat4 四列占连续 location 且每列 divisor=1;instance count 不越过缓冲

4. 清理并重放一致

VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换

最小可重放检查

unit: logl-27
owner: VAO 的逐顶点/逐实例属性与 instanced draw call
state_or_resource: instance count、attribute locations、stride/offset、divisor、gl_InstanceID 和矩阵
command: 上传实例数据,为每列配置 attribute+divisor,再调用 instanced draw
pass_invariant: mat4 四列占连续 location 且每列 divisor=1;instance count 不越过缓冲
single_fault: 只给 mat4 第一列设置 divisor,后三列每个顶点推进,实例矩阵被撕裂
required_evidence: VAO 属性/divisor 查询、instance buffer 大小、实例 ID、draw 参数与实例变换

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

出处声明

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

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

讨论

评论区加载中…