几何着色器、图元扩增与坐标契约
几何着色器、图元扩增与坐标契约:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能解释几何着色器在渲染管线里处在什么位置(图元装配之后、光栅化之前),以及它比顶点 / 片段着色器多出来的独门本事:凭空增删改图元(输入 1 个图元 → 输出 0 个、1 个或多个)
- 能实现并改造一个 explode 爆破几何着色器:用
GetNormal算出三角形的面法线,把三个顶点沿法线整体推开一段magnitude,再EmitVertex/EndPrimitive发出去 - 能判断:本站的 Demo 跑在 WebGL2 上,可这章讲的几何着色器在 WebGL2 里根本不存在——那本站的「爆破」效果到底是怎么做出来的?它和真几何着色器差在哪?
为什么逐顶点处理还不够
前面那条流水线,工位是固定死的:顶点着色器一次只搬一个顶点、片段着色器一次只涂一个像素,谁都不能凭空多造或扔掉零件。送进去三个顶点,出来还是这三个顶点围的那一片,形状增不了也减不了。
这一章在流水线中段插一道能动剪刀的加工工位:上一道工序送来一整个零件(一个点、一根线、一片三角形),这道工位可以把它改造、复制、裁掉、甚至凭空多生出几个新零件再往下送。于是一个孤零零的点能当场长成一面小广告牌,一片三角形能炸成漫天飞溅的碎片,每个面还能顺手长出一根「法线毛刺」。
没有它也不是「什么都动不了」:顶点着色器仍能移动已有顶点,实例化或 CPU 也能预先准备更多数据。缺的是这道按完整图元临时扩增或重组的工位。想让一个点长出房子、在一次调用里把多条线段分组输出、或把顶点法线画出来,就得换工具。但这里有个本站绕不过去的坎:这道工位在桌面 OpenGL 上有,在本站跑的 WebGL2 上压根没有——这一点 §3 一开口就得跟你讲清楚,绝不糊弄。
几何着色器:管线中段那把能动的剪刀
这道能动剪刀就是 ↡经典桌面 OpenGL 管线里一道可选的着色阶段,夹在顶点着色器和光栅化之间。它一次收下一个完整图元(点 / 线 / 三角形),可以对它增、删、改,输出零个、一个或多个新图元再送往光栅化。曲面细分、mesh shader 或计算着色器也能在别的管线模型中生成几何,因此『唯一』只限定在这里讨论的经典顶点到光栅化路径。注意:WebGL2(GLSL ES 3.0)没有该阶段。(geometry shader):它是经典桌面 OpenGL 顶点着色器到光栅化路径里的一道可选阶段。顶点着色器逐顶点处理完后,它一次收下一个完整图元;它可以增、删、改这个图元,输出零个、一个或多个新图元,再送往光栅化。
它在管线里的位置很关键:
和前两个着色器最大的不同在于「能不能按完整图元改变输出序列」:顶点着色器一进一出(一个顶点进、一个顶点出),片段着色器一片段一片段地涂;而几何着色器一次拿到完整图元,再决定发多少顶点、怎样分成输出图元。这正是它能做点造房子、长法线、图元级爆破的根。
图元、输入输出类型:这把剪刀收什么、出什么
几何着色器处理的单位是 ↡渲染管线里最小的可绘制单元:一个点(point)、一根线段(line)、或一片三角形(triangle)。顶点着色器处理单个顶点,而几何着色器一次收下一整个图元——即组成它的那一组顶点。(primitive)——点、线、三角形这种最小绘制单元。用它之前,你得先在着色器顶上声明这把剪刀收什么、出什么,靠两行 layout:
- 输入类型:
layout(points) in;/layout(lines) in;/layout(triangles) in;——告诉它每次收到的是 1 个点、2 个顶点的线、还是 3 个顶点的三角形。收进来的这组顶点放在内建数组gl_in[]里,gl_in[0].gl_Position、gl_in[1].gl_Position…… 就是它们各自的位置。 - 输出类型 + 上限:
layout(triangle_strip, max_vertices = 3) out;——告诉它你打算输出哪种图元(点points/ 线带line_strip/ 三角形带triangle_strip),以及最多发多少个顶点,也就是 ↡几何着色器输出声明里的上限:layout(triangle_strip, max_vertices = N) out; 中的 N,表示单次 main 调用承诺最多发 N 个顶点。发射次数超过它的结果不可依赖;应按任何执行路径的最大 EmitVertex 次数声明足够的 N。。
输入和输出类型完全可以不一样——这正是它的魔力所在:输入一个点,输出一片三角形带(点造广告牌);输入一片三角形,输出还是三角形但顶点被推开了(爆破)。怎么把声明好的顶点真正「发出去」,看下一节这两个动作。
EmitVertex 与 EndPrimitive:一个一个发、发完收尾
声明好类型,几何着色器在 main 里靠两个内建函数干活。先是 ↡几何着色器里『发出一个顶点』的动作:把当前赋给 gl_Position 和其它输出变量的值快照为输出图元的一个顶点。每调用一次就多发一个顶点;调用后所有输出变量的值都应在下一次 EmitVertex 前重新写入。:你先写好一个位置和要传给下一阶段的数据,再调一次 EmitVertex(),就等于「把这个顶点发出去」;想要几个顶点就调几次。然后是 ↡几何着色器里『结束当前输出图元』的动作:显式结束当前 strip,随后 EmitVertex 会开始一个新的输出图元。它用于同一次几何着色器调用里输出多个图元;若只输出一个图元,main 结束时会自动完成它。:当一次调用要输出多个图元时,调它把当前一段和下一段分开;若只输出一个图元,调用末尾的 EndPrimitive() 是清晰的显式写法,但不是语义上的必需条件。
下面这张图把「一个点 → 发 4 个顶点 → 收尾成一个四边形」整套走一遍:
EmitVertex()在它周围依次发出 4 个顶点(①→④),再用 EndPrimitive() 收尾,triangle_strip 就把它们连成了一个四边形——0 维的点凭空长成了一片面。记住这个节奏:写好所有输出 → EmitVertex(),重复 N 次;需要在同一次调用内再开一条输出 strip 时才用 EndPrimitive() 分段。max_vertices 必须覆盖实际最坏路径的 EmitVertex 次数;超过它的执行结果不应依赖。
原书的第一个完整例子:一个点造一栋三角形带房子
原教程并不止造一个四边形:它让每个输入点输出 5 个顶点。triangle_strip 的前 3 个顶点组成第一个三角形,之后每多一个顶点再多一个三角形,所以 5 个顶点正好形成房身两片 + 屋顶一片的 3 个三角形:
layout(points) in;
layout(triangle_strip, max_vertices = 5) out;
void build_house(vec4 p) {
gl_Position = p + vec4(-.2, -.2, 0, 0); EmitVertex();
gl_Position = p + vec4( .2, -.2, 0, 0); EmitVertex();
gl_Position = p + vec4(-.2, .2, 0, 0); EmitVertex();
gl_Position = p + vec4( .2, .2, 0, 0); EmitVertex();
gl_Position = p + vec4( 0, .4, 0, 0); EmitVertex();
EndPrimitive();
}这就是原书的核心转换:一次 GL_POINTS 绘制没有改变,几何阶段却把每个输入点扩增为一栋由三角形带组成的房子。max_vertices = 5 不是随手填的性能开关,而是该执行路径的明确输出契约。
面法线与爆破:让三角形沿法线飞出去
本章的招牌效果是爆破 explode:把模型的每个三角形沿着它自己朝外的方向整体推开一段,模型就像被炸成漫天飞溅的碎片。这「朝外的方向」就是这个三角形的 ↡一个三角形所在平面的垂直朝外方向。几何着色器用三角形的两条边做叉乘(cross)再归一化得到它:normalize(cross(边1, 边2))。爆破时让三个顶点都沿这个方向平移,整片三角形就被整体推离原位。(face normal)——用三角形的两条边叉乘再归一化就能算出来(normalize(cross(a, b)))。
有了面法线,爆破就是一句话:让三角形的三个顶点都沿这个法线方向平移同样一段距离 magnitude,三角形整体被推离原位、又不变形。下面这张图把「紧凑的原模型」和「各片沿法线炸开」并排画出来(绿箭头就是各片的面法线 / 推开方向):
注意一个细节:爆破推的是面法线(整片三角形共一个、朝外),不是顶点法线(每个顶点一个、用于光照)。因为几何着色器手里正好攥着一个三角形的三个顶点,当场叉乘算面法线最方便。magnitude 让它随时间从 0 涨到某个值,模型就有了「逐渐炸开」的动画——这正是 §5 和 §6 要拆的主菜。
这里要多补一条工程契约:叉乘求法线、加位移的 position,必须在同一个线性坐标空间。原教程的 gl_in[].gl_Position 版本适合把几何阶段和 EmitVertex 的机制讲清;做带透视投影的通用工程实现时,更稳妥的路径是顶点着色器另行输出模型 / 世界 / 观察空间的位置,几何着色器在该空间算 cross 和位移,最后才乘投影矩阵。不要把已做透视投影的裁剪坐标当作通用的法线和位移空间。
单步走一遍:几何着色器到底在干什么
把上面几节串起来,用三步走完「几何着色器在管线哪、怎么发顶点造图元、怎么爆破」整套直觉。
猜一猜:几何着色器输入一个孤零零的点,最后却能在屏幕上画出一片四边形——一个 0 维的点,凭空变出了一片有面积的面。它是怎么「无中生有」造出这片面的?先想一想,再单步走下面三步。
每一步左边画这一步的几何、右边的小字说这一步做了什么,盯着「图元怎么被增删改造」,就懂了:
① 管线位置:它夹在顶点着色器和光栅化之间
顶点着色器逐顶点处理完,把一个完整图元 (这里是一个点)交给几何着色器。几何着色器卡在 图元装配之后、光栅化之前 ,是管线里唯一能凭空增删图元 的工位——处理完再送往光栅化拆成片段。
走完这一遍就明白:几何着色器的全部魔力,归根结底就是 「在 main 里自己决定赋什么 gl_Position、EmitVertex 发几次、EndPrimitive 怎么分段」——发的顶点比收的多,就是「造」;少,就是「删」;位置变了,就是「改」。
对比一下:爆破前 vs 爆破后
猜一猜:把下面这个滑块从左拖到右,左边是「没炸开的原模型」、右边是「炸开后」。两边的三角形数量一样不一样?炸开后各片是随便乱飞,还是朝着各自一个固定的方向飞?拖动看看,再回想 §3——它们正是沿各自的面法线飞出去的。
左边是紧凑的原模型(六个三角形严丝合缝拼成一圈),右边是几何着色器把每片沿它的面法线推开 magnitude 之后的样子——三角形一个没多也没少,只是各自朝外飞了一段。拖动中间滑块左右对比:
看出来了吧——爆破不增删三角形,它只是把每片沿面法线整体平移了一下(magnitude 控制飞多远)。这是几何着色器「改图元」的典型用法。这两张是 SVG 示意图:本站跑在 WebGL2 上、没有几何着色器阶段,真实渲染里这个效果是用顶点着色器近似的——下一节代码对照把「桌面真几何着色器」和「WebGL2 近似」原原本本摆在一起。
代码逐段拆解:从骨架到爆破,再到 WebGL2 的诚实近似
下面三段层层递进:先看最简的「原样直通」骨架认全套语法,再看真正有用的 explode 爆破,最后看本站在 WebGL2 上只能怎么近似它。
第一段:最简骨架——原样直通一个三角形
先认全套语法。这个几何着色器什么花活都不做,收到一个三角形、把三个顶点原封不动发出去——但它把 layout 声明、gl_in[] 取值、EmitVertex / EndPrimitive 的节奏全摆齐了。这是桌面 OpenGL 3.3 的真几何着色器:
#version 330 core
layout (triangles) in; // 收:一片三角形(3 个顶点)
layout (triangle_strip, max_vertices = 3) out; // 出:一条三角形带,最多发 3 个顶点
void main() {
for (int i = 0; i < 3; i++) {
gl_Position = gl_in[i].gl_Position; // 取第 i 个输入顶点的位置,原样赋给输出
EmitVertex(); // 发出这个顶点
}
EndPrimitive(); // 3 个发完 → 收尾成一个三角形
}读懂这段,几何着色器的骨架就清楚了:顶上两行 layout 定好「收三角形、出三角形带最多 3 顶点」;gl_in[i].gl_Position 是顶点着色器送来的第 i 个顶点位置;循环里赋值 + EmitVertex 把三个顶点逐个发出,EndPrimitive 收尾。把循环里的位置改一改,就从「原样直通」变成各种花活。
第二段:explode 爆破——沿面法线推开三个顶点
把骨架里「原样赋值」换成「沿面法线推开一段」,就是爆破。多两件事:GetNormal 用两条边叉乘算出这片三角形的↡见上文:一个三角形所在平面的垂直朝外方向,用两条边叉乘归一化得到。爆破让三个顶点都沿它平移。;explode 把一个顶点沿法线推开 magnitude(让它随 time 从 0 涨起来就有了「逐渐炸开」的动画):
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
uniform float time;
vec3 GetNormal() { // 两条边叉乘 → 这片三角形的面法线
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
return normalize(cross(a, b));
}
vec4 explode(vec4 pos, vec3 normal) { // 沿法线推开 magnitude(随 time 起伏)
float magnitude = 2.0;
vec3 dir = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return pos + vec4(dir, 0.0);
}
void main() {
vec3 normal = GetNormal();
for (int i = 0; i < 3; i++) {
gl_Position = explode(gl_in[i].gl_Position, normal); // 推开后再发
EmitVertex();
}
EndPrimitive();
}和骨架的唯一区别,就是 EmitVertex 之前把 gl_in[i].gl_Position 换成了 explode(...)——三个顶点都加上同一个「面法线 × magnitude」的位移,整片三角形就被整体推离原位、形状不变。三角形数量一个没改,这是几何着色器的「改图元」用法。(sin(time) + 1.0) / 2.0 把 magnitude 系数在 0~1 之间来回拉,模型就一张一合地呼吸式爆破。
第三段:⚠ WebGL2 没有几何着色器——本站只能这样近似
现在面对本章红线。上面两段桌面代码在本站的 WebGL2 里一行都跑不了:WebGL2(GLSL ES 3.0)压根没有几何着色器阶段。那本站的「爆破」效果怎么办?答案是退一步、在顶点着色器里做↡因为 WebGL2 没有几何着色器,本站把『沿法线推开』这件事挪到顶点着色器里做:每个顶点用它自带的顶点法线 aNormal,按 aPos + aNormal * uExplode 自己往外位移。它能近似爆破的『膨胀 / 飞溅』观感,但顶点着色器一进一出、不能增删图元,所以做不了几何着色器那些『造新几何』的活。:
// 几何着色器阶段:手里攥着整片三角形的 3 个顶点
// 当场叉乘算「面法线」,把三个顶点沿它整体推开,还能增删图元
vec3 normal = GetNormal(); // 面法线(整片共一个)
gl_Position = explode(gl_in[i].gl_Position, normal);
EmitVertex(); // ← 能多发 / 少发顶点,凭空造新几何#version 300 es
in vec3 aPos;
in vec3 aNormal; // 顶点自带的「顶点法线」(建模时备好)
uniform float uExplode; // 0 = 原样,越大越炸开
uniform mat4 uMVP;
void main() {
// 每个顶点各自沿「自己的顶点法线」往外位移——膨胀 / 飞溅观感的近似
vec3 pos = aPos + aNormal * uExplode;
gl_Position = uMVP * vec4(pos, 1.0);
}两者根本不是一回事,差异必须讲清:
一句话收口:本章前两段是桌面真几何着色器,最后这段是本站 WebGL2 的诚实近似——上面那两张爆破示意图,对应的就是这第三段近似在跑。完整可运行示例将在章末提供(M5)。
原书真正实用的用途:把顶点法线画出来
爆破很直观,但原书最后给出的工程用途是法线可视化。排查光照异常时,先正常绘制模型;再用第二个 program 重绘一次,让几何着色器把每个顶点的法线扩成一小段线。看到线段方向错误,就能回查模型数据、顶点属性或法线矩阵。
关键是先在顶点着色器把位置与法线变到同一观察空间;法线在有非均匀缩放时必须走 normal matrix:
// vertex shader: gl_Position 暂存 view-space 位置,给几何阶段使用
gl_Position = view * model * vec4(aPos, 1.0);
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
vs_out.normal = normalize(normalMatrix * aNormal);几何阶段对每个输入三角形发 3 条线,每条线用 2 个顶点,因此 max_vertices = 6。它在同一次调用里输出 3 个 line strip,所以每条线后都必须 EndPrimitive():
layout(triangles) in;
layout(line_strip, max_vertices = 6) out;
uniform mat4 projection;
void generateLine(int i) {
gl_Position = projection * gl_in[i].gl_Position; EmitVertex();
gl_Position = projection * (gl_in[i].gl_Position
+ vec4(gs_in[i].normal, 0.0) * 0.4); EmitVertex();
EndPrimitive();
}
void main() { generateLine(0); generateLine(1); generateLine(2); }这段把前面所有契约落到实处:gl_in[] 和 gs_in[] 都按输入图元变成数组;每个 EmitVertex() 前都重新写输出;6 次发射刚好匹配上限;EndPrimitive() 在三条独立线之间明确切分。WebGL2 没有这条路径时,调试法线可改用单独的线网格、实例化线段或 CPU 端生成。
容易踩的坑
小结
- 几何着色器夹在顶点着色器和光栅化之间,是经典桌面 OpenGL 路径里按完整图元决定输出序列的可选阶段(输入 1 个图元 → 输出 0/1/多个)
- 用前先声明输入输出图元类型:
layout(triangles) in;/layout(triangle_strip, max_vertices=N) out;;输入输出类型可以不一样(点造四边形、三角形炸成碎片) - 干活靠两个动作:
EmitVertex()快照一个顶点的全部输出、EndPrimitive()在同一次调用的多个输出图元之间分段;只输出一个图元时调用结束会自动完成它,max_vertices必须覆盖最坏路径 - explode 爆破:
GetNormal叉乘算面法线,把三个顶点都沿法线推开magnitude,三角形数量不变、整体飞溅;法线、位移和位置必须保持同一线性坐标空间 - ⚠ WebGL2(本站)没有几何着色器阶段——爆破只能在顶点着色器里用顶点位移近似(
aPos + aNormal * uExplode),它做不了真几何着色器那种「造新几何」的活
练习
问题 1(读懂 / 改几何着色器代码题) 下面这个几何着色器收到一个点,想把它造成一个四边形(4 个顶点)。找出两处需要改进的地方:① 输出上限是否满足最坏路径?② 在这次调用只输出一个四边形时,末尾没有 EndPrimitive() 会不会让下一个点的调用自动接到同一条 strip?
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 3) out; // (A)
void main() {
vec4 p = gl_in[0].gl_Position;
gl_Position = p + vec4(-0.1, -0.1, 0.0, 0.0); EmitVertex();
gl_Position = p + vec4( 0.1, -0.1, 0.0, 0.0); EmitVertex();
gl_Position = p + vec4(-0.1, 0.1, 0.0, 0.0); EmitVertex();
gl_Position = p + vec4( 0.1, 0.1, 0.0, 0.0); EmitVertex();
// (B)
}问题 2(问答题:为什么本站只能近似) 本站的爆破 Demo 跑在 WebGL2 上。有人问:「既然原书用几何着色器做爆破,直接把那段几何着色器搬进本站不就行了?」请回答:① 这样做会发生什么?② 本站实际用什么办法近似爆破?③ 这个近似和真几何着色器在能力上差在哪?
问题 3(改效果题) §6 第二段的 explode 几何着色器里,magnitude 写死成 2.0,方向用 (sin(time)+1.0)/2.0 在 0~1 间起伏,所以模型反复一张一合地呼吸式爆破。如果你想让它炸开后就保持炸开、不再合回去(一次性爆破),该怎么改这两行?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 几何着色器
渲染管线里一道可选的着色阶段,夹在顶点着色器和光栅化之间。它一次收下一个完整图元(点 / 线 / 三角形),可以对它增、删、改,输出零个、一个或多个新图元再送往光栅化——是经典桌面 OpenGL 路径里专门处理图元级输出的阶段。曲面细分、mesh shader 或计算着色器也可在别的管线模型中生成几何。**注意:桌面 OpenGL 3.2+ 才有它,本站跑的 WebGL2(GLSL ES 3.0)没有这个阶段。**详见本章「几何着色器」一节。
- 图元
渲染管线里最小的可绘制单元:一个点(point)、一根线段(line)、或一片三角形(triangle)。顶点着色器处理单个顶点,而几何着色器一次收下一整个图元——也就是组成它的那一组顶点(放在内建数组
gl_in[]里)。详见本章「图元、输入输出类型」一节。- EmitVertex
几何着色器里「发出一个顶点」的动作:把你当前赋给
gl_Position(以及其它要传给片段着色器的输出)的值,作为输出图元的一个顶点提交出去。每调一次就多发一个顶点,连续发出的顶点按输出类型(如triangle_strip)依次连接。详见本章「EmitVertex 与 EndPrimitive」一节。- EndPrimitive
几何着色器里「结束当前图元」的动作:告诉 GPU 到此为止发出的这一串顶点凑成一个完整图元,下一次
EmitVertex将开始一个全新图元。它用于同一次几何着色器调用内输出多个 strip;若只输出一个图元,调用结束会自动完成它。详见本章「EmitVertex 与 EndPrimitive」一节。- max_vertices
几何着色器输出声明里的上限:
layout(triangle_strip, max_vertices = N) out;中的N,表示单次main里最多能发多少个顶点。EmitVertex调用次数超过它后的结果不可依赖;把它设成 ≥ 任一执行路径的实际最大发顶点数,并受实现的GL_MAX_GEOMETRY_OUTPUT_VERTICES限制。详见本章「图元、输入输出类型」一节与 §7。- 面法线
一个三角形所在平面的垂直朝外方向。几何着色器用三角形的两条边做叉乘(
cross)再归一化得到它:normalize(cross(边1, 边2))。爆破时让三个顶点都沿这个方向平移,整片三角形就被整体推离原位。注意它是整片三角形共一个的「面法线」,区别于每个顶点各一个、用于光照的「顶点法线」。详见本章「面法线与爆破」一节。- 顶点位移近似
因为 WebGL2 没有几何着色器,本站把「沿法线推开」这件事挪到顶点着色器里做:每个顶点用它自带的顶点法线
aNormal,按aPos + aNormal * uExplode自己往外位移。它能近似爆破的「膨胀 / 飞溅」观感,但顶点着色器一进一出、不能增删图元,所以做不了几何着色器那些「造新几何」的活。详见本章「代码逐段拆解」第三段。
版本、来源与运行边界
本章以 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 结果的验证。
正式概念与状态责任
- geometry shader:在“几何着色器、图元扩增与坐标契约”中由已链接 program 的 geometry stage 与每次 invocation负责解释其输入、受控状态和可观察结果;运行时以program resource 查询、输入图元、发射序列、primitive count、链接日志与输出定位它的第一处变化。
- emitvertex:在“几何着色器、图元扩增与坐标契约”中由已链接 program 的 geometry stage 与每次 invocation负责解释其输入、受控状态和可观察结果;运行时以program resource 查询、输入图元、发射序列、primitive count、链接日志与输出定位它的第一处变化。
- endprimitive:在“几何着色器、图元扩增与坐标契约”中由已链接 program 的 geometry stage 与每次 invocation负责解释其输入、受控状态和可观察结果;运行时以program resource 查询、输入图元、发射序列、primitive count、链接日志与输出定位它的第一处变化。
- exploding:在“几何着色器、图元扩增与坐标契约”中由已链接 program 的 geometry stage 与每次 invocation负责解释其输入、受控状态和可观察结果;运行时以program resource 查询、输入图元、发射序列、primitive count、链接日志与输出定位它的第一处变化。
章专属 OpenGL 状态实验
先预测“接收完整图元,修改/生成顶点,分段 EmitVertex 并 EndPrimitive”发生后,已链接 program 的 geometry stage 与每次 invocation应怎样改变输入 primitive、gl_in、输出 primitive、发射顶点、条带边界和坐标空间;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。
实验一:Context—资源—结果合同
选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。
Context · resource · observable result
几何着色器、图元扩增与坐标契约:状态合同
把 geometry shader 的输入图元、输出类型、EmitVertex/EndPrimitive 与 max_vertices 对齐
验证场景
官方教程正式概念
logl-26 · 基线帧
geometry shader:固定 context、资源内容与输入事件,执行“接收完整图元,修改/生成顶点,分段 EmitVertex 并 EndPrimitive”
冻结输入:geometry shader
已链接 program 的 geometry stage 与每次 invocation记录输入 primitive、gl_in、输出 primitive、发射顶点、条带边界和坐标空间
结果:得到可重复的初始 GL 状态与资源身份
观测:program resource 查询、输入图元、发射序列、primitive count、链接日志与输出中的初始快照
预期:已链接 program 的 geometry stage 与每次 invocation得到可复查结果,并持续满足“每次 invocation 发射数不超过 max_vertices,输入输出 layout 与 draw primitive 匹配”
实验二:CPU 命令到 GPU 结果的五段轨迹
逐段执行“接收完整图元,修改/生成顶点,分段 EmitVertex 并 EndPrimitive”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“每次 invocation 发射数不超过 max_vertices,输入输出 layout 与 draw primitive 匹配”。
CPU command · GL state · GPU result
几何着色器、图元扩增与坐标契约:五段轨迹
当前观测:program resource 查询、输入图元、发射序列、primitive count、链接日志与输出中的初始快照
不变量:每次 invocation 发射数不超过 max_vertices,输入输出 layout 与 draw primitive 匹配
实验三:单故障与同输入恢复
注入“声明输入 points 却用 GL_TRIANGLES 绘制,geometry invocation 合同不匹配”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有program resource 查询、输入图元、发射序列、primitive count、链接日志与输出一起恢复才算修复。
Single fault · first divergence · replay
几何着色器、图元扩增与坐标契约:反例与恢复
故障:声明输入 points 却用 GL_TRIANGLES 绘制,geometry invocation 合同不匹配
第 1 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“声明输入 points 却用 GL_TRIANGLES 绘制,geometry invocation 合同不匹配”
每次 invocation 发射数不超过 max_vertices,输入输出 layout 与 draw primitive 匹配
program resource 查询、输入图元、发射序列、primitive count、链接日志与输出
最小可重放检查
unit: logl-26
owner: 已链接 program 的 geometry stage 与每次 invocation
state_or_resource: 输入 primitive、gl_in、输出 primitive、发射顶点、条带边界和坐标空间
command: 接收完整图元,修改/生成顶点,分段 EmitVertex 并 EndPrimitive
pass_invariant: 每次 invocation 发射数不超过 max_vertices,输入输出 layout 与 draw primitive 匹配
single_fault: 声明输入 points 却用 GL_TRIANGLES 绘制,geometry invocation 合同不匹配
required_evidence: program resource 查询、输入图元、发射序列、primitive count、链接日志与输出复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。