WebGPU中如何为Triangle Strip的每个子三角形设置不同颜色?
解决WebGPU Triangle Strip中单个三角形独立着色问题
问题根源分析
你当前代码中使用<private>变量fi来区分三角形的方式是错误的:
<private>变量属于顶点着色器的实例私有变量,每个顶点执行时会设置该值,而片元着色器阶段会对该值进行插值计算。- 第二个三角形由顶点1、2、3组成,其中顶点1/2的
fi=1,顶点3的fi=2,片元中的fi会是1到2之间的插值结果,永远不会等于1,导致所有片元都返回绿色。
基础修复方案:使用Primitive Index
正确的做法是在片元着色器中使用**@builtin(primitive_index)**内置变量,它表示当前片元所属的图元(三角形)索引,从0开始计数,且不会被插值,能精准区分每个三角形。
修改后的测试代码:
body{ background-color: #000 } canvas{ display: block; width: 600px; height: 400px; outline: 1px solid #666 }
<canvas width=900 height=600></canvas> <script type="module"> const C = document.querySelector('canvas').getContext(`webgpu`); const code=` @vertex fn vs( @builtin(vertex_index) vi: u32 ) -> @builtin(position) vec4f { if(vi<3){ var T = array<vec2f, 3>( vec2f(0,0), vec2f(.4,.7), vec2f(.8,0) ); return vec4f(T[vi],0,1); }; return vec4f(.6,-.5,0,1); } @fragment fn fs(@builtin(primitive_index) pi: u32) -> @location(0) vec4f { if(pi == 0){ // 第一个三角形(索引从0开始) return vec4f(.7,.2,.2,.5); // 红色 }; return vec4f(.3,.6,.4,.5); // 绿色 }`; const format = `bgra8unorm`; const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const Q = device.queue; const A = {loadOp: `clear`, storeOp: `store`}; const O = {colorAttachments: [ A ]}; const module = device.createShaderModule({ code }); const P = device.createRenderPipeline({ layout: `auto`, primitive: { topology: `triangle-strip` }, vertex: { module, entryPoint: `vs` }, fragment: { module, entryPoint: `fs`, targets: [{ format }] } }); C.configure({ device, format }); function F(){ A.view = C.getCurrentTexture().createView(); const E = device.createCommandEncoder(); const R = E.beginRenderPass(O); R.setPipeline(P); R.draw(4); R.end(); Q.submit([E.finish()]); requestAnimationFrame(F) } F() </script>
扩展实现:鼠标点击生成带新颜色的Triangle Strip三角形
要实现点击新增带独立颜色的三角形,需要动态维护顶点数据和颜色数据,通过uniform缓冲区存储每个三角形的颜色,再用primitive_index索引取色。
完整实现代码:
body{ background-color: #000 } canvas{ display: block; width: 600px; height: 400px; outline: 1px solid #666 }
<canvas width=900 height=600></canvas> <script type="module"> const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('webgpu'); // 初始顶点数据:第一个三角形的3个顶点 let vertices = new Float32Array([ 0, 0, // 顶点0 0.4, 0.7, // 顶点1 0.8, 0, // 顶点2 ]); // 初始颜色数组:对应每个三角形的颜色 let triangleColors = new Float32Array([ 0.7, 0.2, 0.2, 0.5, // 第一个三角形的红色 ]); const format = 'bgra8unorm'; const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const queue = device.queue; // 创建顶点缓冲区(支持动态更新) const vertexBuffer = device.createBuffer({ size: vertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); queue.writeBuffer(vertexBuffer, 0, vertices); // 创建颜色uniform缓冲区(预留100个三角形的颜色空间) const colorBufferSize = 100 * 4 * 4; const colorBuffer = device.createBuffer({ size: colorBufferSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); queue.writeBuffer(colorBuffer, 0, triangleColors); const shaderCode = ` struct VertexInput { @location(0) position: vec2f, }; struct VertexOutput { @builtin(position) position: vec4f, }; @group(0) @binding(0) var<uniform> triangleColors: array<vec4f, 100>; @vertex fn vs(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4f(input.position, 0.0, 1.0); return output; } @fragment fn fs(@builtin(primitive_index) pi: u32) -> @location(0) vec4f { return triangleColors[pi]; } `; const shaderModule = device.createShaderModule({ code: shaderCode }); const vertexLayout = { arrayStride: 2 * 4, // 每个顶点2个float,共8字节 attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2', }], }; const pipeline = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' }, vertex: { module: shaderModule, entryPoint: 'vs', buffers: [vertexLayout], }, fragment: { module: shaderModule, entryPoint: 'fs', targets: [{ format }], }, }); const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: colorBuffer }, }], }); ctx.configure({ device, format }); // 鼠标点击事件处理:添加新顶点和随机颜色 canvas.addEventListener('click', (e) => { // 转换鼠标坐标为WebGPU的NDC坐标(范围-1到1) const rect = canvas.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * 2 - 1; const y = ((rect.top - e.clientY) / rect.height) * 2 + 1; // 更新顶点数组 const newVertices = new Float32Array(vertices.length + 2); newVertices.set(vertices); newVertices[vertices.length] = x; newVertices[vertices.length + 1] = y; vertices = newVertices; // 生成随机颜色并更新颜色数组 const r = Math.random(); const g = Math.random(); const b = Math.random(); const newColors = new Float32Array(triangleColors.length + 4); newColors.set(triangleColors); newColors.set([r, g, b, 0.5], triangleColors.length); triangleColors = newColors; // 写入缓冲区 queue.writeBuffer(vertexBuffer, 0, vertices); queue.writeBuffer(colorBuffer, 0, triangleColors); }); function render() { const textureView = ctx.getCurrentTexture().createView(); const encoder = device.createCommandEncoder(); const renderPass = encoder.beginRenderPass({ colorAttachments: [{ view: textureView, loadOp: 'clear', storeOp: 'store', clearValue: { r: 0, g: 0, b: 0, a: 1 }, }], }); renderPass.setPipeline(pipeline); renderPass.setBindGroup(0, bindGroup); renderPass.setVertexBuffer(0, vertexBuffer); renderPass.draw(vertices.length / 2); renderPass.end(); queue.submit([encoder.finish()]); requestAnimationFrame(render); } render(); </script>
关键要点说明
- Primitive Index的作用:
@builtin(primitive_index)直接获取当前片元所属的三角形索引,无需插值,是区分Triangle Strip中单个三角形的标准方式。 - 动态缓冲区更新:顶点和颜色缓冲区设置
COPY_DST用法,支持鼠标点击时动态写入新数据。 - 坐标转换:将屏幕鼠标坐标转换为WebGPU的NDC坐标(-1到1范围),确保顶点位置正确映射到画布。
内容的提问来源于stack exchange,提问作者Teamur
相关产品推荐
相关产品推荐

