- 图形学
【免费下载链接】gpuweb
Where the GPU for the Web work happens!
本文基于 gpuweb 仓库中的 primitive-index 提案文档 及其在 WebGPU/WGSL 规范中的落地实现,系统讲解primitive_index这一片元(fragment)着色器内置变量的语义、原生 API 对照、WGSL 启用方式与实战用法。读完本文,你将掌握如何在 WGSL 中声明并消费primitive_index、如何通过GPUDevice的requestDevice特性开关启用primitive-index功能,以及该索引在不同图元拓扑下的精确计数规则。
一、什么是 primitive_index
primitive_index是 WebGPU 引入的一个 WGSL 内置输入变量,为单次绘制实例(draw instance)内的每一个图元(三角形、线段或点)提供一个唯一的整数编号。它之于片元着色器,类似于vertex_index之于顶点着色器、instance_index之于实例:
- 它在片元着色器中可读,类型为
u32; - 它在当前绘制操作内从 0 开始,每处理完一个点、线或三角形图元即自增 1;
- 它在每两个实例(instance)之间重置为 0。
这一能力在底层原生图形 API(D3D、Metal、Vulkan、OpenGL)中早已是常见特性,因此在 WebGPU 中支持它,能显著提升现有着色器直接移植到 Web 的可行性。
典型应用场景
- 扁平着色(flat shading):用图元索引驱动每个图元的统一着色,实现"每个三角形一个颜色"的经典可视化效果;
- 高级材质效果:为每个图元分配独立的伪随机种子或材质 ID,避免顶点级数据插值带来的歧义;
- 虚拟化几何管线:现代大规模场景管理(如几何裁剪、LOD 切换、GPU 驱动的实例化)需要精确知道"当前正在处理第几个图元",以便从结构化缓冲中检索对应元数据。
二、提案状态:已合并进规范
该提案创建于 2025-09-03,对应的 GitHub Issue 为 #1786。根据 proposals/README.md 中的状态分类,primitive-index已归入Status: Merged——即该提案已经过工作组批准,正式加入 WebGPU 与 WGSL 规范。仓库保留该文档作为解释性说明与历史存档,因此对于行为细节的最终裁决,应始终以规范为准。
这一点在规范源码中有直接证据:
- spec/index.bs 的
GPUFeatureName枚举中已加入字符串字面量"primitive-index"; - spec/index.bs 给出了该 feature 的正式定义:启用后允许在 WGSL 中使用
primitive_index,并引入对应的 WGSL 扩展primitive_index。
三、原生 API 可用性对照
提案文档给出了各主流原生 API 中对应能力的一览表,这也是该内置变量之所以需要"特性门控"的原因——并非所有平台都无条件提供:
| 平台 | 类型 | 说明 |
|---|---|---|
| SPIR-V | 32 位整数 | 需要几何着色器、网格着色器或光线追踪;以内建量PrimitiveID提供 |
| HLSL | u32 | 需要 D3D10;以语义SV_PrimitiveID提供 |
| GLSL | i32 | 需要 GLSL 1.50 及以上、ESSL 3.2(ESSL 3.1 需配合GL_EXT_geometry_shader);以内建量gl_primitiveID提供 |
| Metal | u32 | macOS 需 Metal 2.2,iOS 需 Metal 2.3;以属性[[primitive_id]]提供 |
正因为这种非普适可用性,WebGPU 将该能力封装为可选的primitive-index特性,并要求在使用时于 WGSL 中显式书写enable primitive_index语句。
四、行为语义详解
提案文档对primitive_index的运行时行为给出了精确约束,这些语义同样被 WGSL 规范采纳。逐条解读如下:
- 作用域受限:
primitive_index仅可在fragment(片元)着色器中使用,不能出现在顶点或计算着色器。 - 起始值与步长:第一个图元的索引为 0,之后每个后续图元递增 1。
- 实例间重置:每绘制一个实例后索引归零,即索引计数独立于实例 ID。
- 图元内均匀:同一个图元的所有片元读到相同的
primitive_index值(该值在图元范围内是 uniform 的),这使其天然适合驱动"逐图元"而非"逐片元"的着色逻辑。 - 与 primitive restart 无关:图元重启(primitive restart)不会改变
primitive_index变量的值,计数在条带(strip)重启后仍按顺序延续。 - 溢出语义不适用:HLSL 规定图元 ID 超过 2³²−1 时回绕为 0;但 WebGPU 所有适用的绘制调用计数参数都是无符号 32 位整数,不可能超过该上限,因此该回绕规则对 WebGPU 不适用。
- 相邻图元不支持自动生成:WebGPU 没有为相邻图元(adjacent primitives)自动生成索引的能力;对于相邻图元,索引仅为内部非相邻图元维护。
作为佐证,wgsl/index.bs 中primitive_index内置值的规范条目写道:"基于当前绘制操作开始以来、当前实例所处理图元数量的逐图元索引。从 0 开始,每处理完一个点、线或三角形图元即自增 1;在每个实例之间重置为 0。使用图元重启值重启条带图元不会影响图元索引。该索引在图元的所有片元间保持一致。"
五、拓扑支持范围
GPUPrimitiveTopology中的全部拓扑类型都受支持。一般而言,相邻拓扑(adjacency topologies)不会得到支持,但 WebGPU 本身就没有相邻拓扑,因此不存在该限制。具体到每种拓扑,"一个图元"的划分如下:
| 拓扑 | 图元划分 |
|---|---|
| point-list | 每个顶点即一个图元 |
| line-list | 每对顶点为一个图元 |
| line-strip | 每对相邻顶点为一个图元 |
| triangle-list | 每三个顶点为一个图元 |
| triangle-strip | 每 3 个相邻顶点组成一个图元 |
这也解释了primitive_index的计数粒度:在triangle-list下它按"每 3 个顶点一计"递增,在triangle-strip下则按"每滑过一个顶点产生一个新三角形"递增。
六、WGSL 规范中的落地方式
从源码层面看,primitive_index的加入涉及 WGSL 规范的四处关键改动:
1. 内置值名称表新增条目(wgsl/index.bs):builtin_value_name语法允许的标识符列表中加入'primitive_index',同时扩展名称列表也加入'primitive_index'(wgsl/index.bs)。
2. 内置输入/输出值表新增一行(wgsl/index.bs),各字段如下:
| 字段 | 值 |
|---|---|
| Name | primitive_index |
| Stage | fragment |
| Direction | input |
| Type | u32 |
| Extension | primitive_index |
3. 扩展定义(wgsl/index.bs):定义 WGSL 扩展primitive_index,对应GPUFeatureName/"primitive-index"。其语义为:当该扩展启用时,WGSL 模块中使用primitive_index内置变量合法;否则任何使用都会导致 shader-creation error(着色器创建错误)。
4. 特性门控:WebGPU 侧的"primitive-index"特性启用后,允许在 WGSL 中使用primitive_index(见 spec/index.bs)。
七、实战示例
1. 在 GPUDevice 上启用 primitive-index 特性
启用过程遵循 WebGPU 标准特性协商模式:先检查adapter.features是否包含该特性,再通过requestDevice传入requestedFeatures:
const adapter = await navigator.gpu.requestAdapter(); const requestedFeatures = []; if (adapter.features.has('primitive-index')) { requestedFeatures.push('primitive-index'); } else { // 使用替代代码路径或向用户反馈错误。 } const device = await adapter.requestDevice({ requestedFeatures });需要说明:如果请求了适配器不支持的特性,requestDevice会直接拒绝(Promise reject),因此上文这种"先检测、后请求"的防御式写法是推荐做法。若设备不支持该特性,应走备用路径,例如退回基于vertex_index的近似方案(其局限见下文"备选方案"一节)。
2. 在 WGSL 着色器中使用 primitive_index
使用时必须在模块顶部书写enable primitive_index扩展声明,否则会产生着色器创建错误:
enable primitive_index @fragment fn fs_main(@builtin(primitive_index) prim_index: u32) -> @location(0) vec4f { return vec4f(f32(prim_index)); }该示例将每个图元的索引转换为灰度颜色输出:prim_index = 0的图元显示为黑色,索引越大颜色越亮,适合快速验证索引计数是否符合预期。注意prim_index的类型必须是u32(与规范的 Type 字段一致),如需参与浮点运算应显式做f32()转换。
3. 与顶点/实例索引的协同
primitive_index常与以下两个内置变量配合使用:
vertex_index(wgsl/index.bs):当前顶点在绘制命令内的索引,仅限 vertex 着色器;非索引绘制时等于firstVertex参数,索引绘制时等于索引缓冲条目值加上baseVertex。instance_index(wgsl/index.bs):当前顶点所属实例的索引,仅限 vertex 着色器;第一个实例的索引等于绘制命令的firstInstance参数。
典型组合场景:顶点着色器用vertex_index采样每个顶点的几何属性,用instance_index采样实例级数据;片元着色器用primitive_index为每个图元检索独立的材质或随机种子。三者在各自阶段各司其职,共同支撑 GPU 驱动的虚拟化几何渲染。
八、备选方案与设计取舍
提案文档明确指出:primitive_index提供的值在所有场景下都无法仅靠 API 之外的手段完全模拟。
最务实的替代思路是从已有的vertex_index内置变量推导图元索引,但这一方案不支持索引几何(indexed geometry)——因为顶点索引在索引缓冲中的顺序与图元装配顺序并不对应,无法可靠地反推出"这是第几个图元"。而索引几何恰恰是大型网格最常用的格式,能显著降低内存占用。因此该限制不可接受,最终需要专用的primitive_index内置变量。
从原生 API 对照表可以看出,各平台对图元 ID 的呈现方式(类型、阶段、扩展门槛)并不一致,这正是 WebGPU 将其封装为可选特性并配套 WGSL 扩展门控的根本原因——让 Web 开发者获得统一、可预测的跨平台语义。
九、总结
primitive_index是 WebGPU 片元着色器的u32类型内置输入,为单次绘制的每个图元提供从 0 开始的递增唯一编号,实例间重置。- 该特性已完成提案阶段并合并入 WebGPU/WGSL 规范,仓库中的 primitive-index 提案 保留为历史存档;行为以 wgsl/index.bs 与 spec/index.bs 为准。
- 使用前需同时满足两个前置条件:在
GPUDevice上启用"primitive-index"特性,并在 WGSL 模块顶部声明enable primitive_index,否则产生着色器创建错误。 - 它支持全部
GPUPrimitiveTopology拓扑,不受图元重启影响,在图元内保持 uniform,且无需担心 HLSL 式的 32 位回绕问题。 - 对于大型网格的虚拟化渲染、逐图元材质与扁平着色等效果,它是不可替代的底层能力。
- 图形学
【免费下载链接】gpuweb
Where the GPU for the Web work happens!
相关推荐
从SkSL到WGSL:解锁WebGPU渲染新范式
从SkSL到WGSL:解锁WebGPU渲染新范式 你是否在WebGPU项目中为着色器移植头疼?是否因SkSL与WGSL语法差异而反复调试?本文将带你掌握Skia
图形学gpuweb 仓库贡献实战:WebGPU 与 WGSL 规范的本地构建、验证与预览指南
gpuweb 仓库贡献实战:WebGPU 与 WGSL 规范的本地构建、验证与预览指南 本文是一份面向 gpuweb 仓库贡献者的技术操作指南。gpuweb 是
图形学WebGPU GPUBuffer 内存映射完全指南:从 BufferOperations 设计草案到 mapAsync 规范实现
WebGPU GPUBuffer 内存映射完全指南:从 BufferOperations 设计草案到 mapAsync 规范实现 GPU 与 Web 之间的数据
图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考