RealTime-Rendering25-Primitive Restart

Primitive Restart

Primitive Restart是渲染中的一个重要特性,主要用于高效渲染多个独立的图元序列(例如三角形条带、线条条带等),而无需发出多个绘制调用或在索引数据中插入“退化”图元(即面积为零或不可见的图元)。下面从概念、工作原理、使用场景、优缺点以及在不同图形API中的具体实现等方面进行详细解释。

基本概念

在实时渲染中,我们常用索引缓冲来复用顶点数据,以减少重复顶点传输和处理。当使用条带(strip)类型的图元(如 GL_TRIANGLE_STRIP、GL_LINE_STRIP)时,顶点顺序决定了几何连接方式。例如,三角形条带中,每新增一个顶点就会与前两个顶点构成一个新三角形。
然而,条带只能描述单个连续的序列。如果需要绘制多个不相连的条带(比如多个独立的三角形条带组成的不同物体),传统做法有两种:

  • 多个绘制调用:每个条带调用一次 glDrawElements,但会增加CPU/GPU之间传输的IO开销。
  • 插入退化三角形:在条带之间添加额外的顶点索引,使生成的三角形面积为零(退化),从而视觉上不产生新几何。这种方法浪费带宽和处理周期,且需额外计算。

Primitive Restart提供了一种更优雅的方案:在索引序列中插入一个特殊的“重启标记”,告诉GPU“当前条带在此结束,从下一个顶点开始新的条带”。

工作原理

  • 重启索引(Restart Index):开发者指定一个不会用作真实顶点索引的数值(例如对于16位索引,常用 0xFFFF;对于32位索引,常用 0xFFFFFFFF)。当GPU在处理索引缓冲时遇到这个值,它会结束当前条带的装配,并忽略该索引本身(不访问顶点数据),后续的索引则作为全新条带的开始。
  • 硬件支持:现代GPU均原生支持该特性,无需软件模拟,因此效率极高。
  • 示例:
    假设有两个三角形条带:
    • 条带 A:顶点索引 [0,1,2,3] → 生成三角形 (0,1,2) 和 (1,2,3)
    • 条带 B:顶点索引 [4,5,6,7] → 生成三角形 (4,5,6) 和 (5,6,7)

若不使用Primitive Restart,你需要两个绘制调用。若使用,可以将索引合并为:[0,1,2,3, RESTART, 4,5,6,7].在一次绘制调用中,GPU会正确处理两个条带。

为什么需要 Primitive Restart

  • 减少绘制调用:将多个条带合并为一个批次,降低CPU向GPU提交命令的开销。
  • 节省内存和带宽:无需插入退化三角形(每个条带交界处通常需要2个退化顶点),索引缓冲更紧凑。
  • 简化数据生成:某些建模或地形分块算法直接产生多个条带,利用重启标记可轻松合并。

API示例

openGL

  • 启用:glEnable(GL_PRIMITIVE_RESTART)
  • 设置重启索引:glPrimitiveRestartIndex(index)
  • 在索引缓冲中使用指定索引作为重启标记
  • 注意:OpenGL 3.1 之后还提供了 GL_PRIMITIVE_RESTART_FIXED_INDEX,针对特定索引格式使用固定重启值(如16位的0xFFFF),无需单独设置。
1
2
3
4
5
6
7
8
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(0xFFFF); // 对于16位索引
// 或
glPrimitiveRestartIndex(0xFFFFFFFF); // 对于32位索引

glDrawElements(GL_TRIANGLE_STRIP, totalIndexCount, GL_UNSIGNED_SHORT, 0);

glDisable(GL_PRIMITIVE_RESTART);

vulkan

  • 在创建图形流水线时,通过 VkPipelineInputAssemblyStateCreateInfo结构体的 primitiveRestartEnable成员开启。
  • 重启索引是固定的:对于16位索引为 0xFFFF,32位为 0xFFFFFFFF,不可自定义。
  • 在绘制命令中,索引缓冲内直接插入该值即可。
1
2
3
4
5
6
7
8
// 在创建 Pipeline 时配置
VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
inputAssembly.primitiveRestartEnable = VK_TRUE; // 启用!

// 重启索引始终是 0xFFFF(16位)或 0xFFFFFFFF(32位)
// 由 vkCmdBindIndexBuffer 的 indexType 参数决定