I am trying to set color per triangle.
But it is not working.
Only setting color per vertex works!
This is what I made:
<!DOCTYPE html><title>Color Per Triangle</title><style>
body{ background-color: #000 }
canvas{ display: block; width: 600px; height: 400px; outline: 1px solid #343438 }
</style><canvas width=900 height=600></canvas><script type=module>
const canvas=document.body.firstChild, C=canvas.getContext(`webgpu`),
red=[.9,.3,.3,1], yel=[.7,.7,.3,1],
tri=[
0,0,0,...red, 0, 1,0,...red, 1,0,0,...red, // ⚠️ Wish: avoid color repeating
0,0,0,...yel, 0,-1,0,...yel, -1,0,0,...yel,
];
/* ♻️ WANT THIS:
tri=[
0,0,0, 0, 1,0, 1,0,0, ...red, // set color per triangle
0,0,0, 0,-1,0, -1,0,0, ...yel,
]
*/
VB_$=window.VB_$=new Float32Array(tri); // Vertex Buffer Source
let code=`
struct _V {
@location(0) p: vec3f,
@location(1) c: vec4f,
};
struct V_ {
@builtin(position) p: vec4f,
@location(0) c: vec4f,
};
@vertex fn vs(_v: _V) -> V_ {
var v_: V_;
v_.p = vec4f(_v.p, 1);
v_.c = _v.c;
return v_;
}
@fragment fn fs(v_: V_) -> @location(0) vec4f {
return v_.c;
}
`,
format = `bgra8unorm`,
adapter = await navigator.gpu.requestAdapter(),
device = await adapter.requestDevice(),
Q = device.queue,
A = { loadOp: `clear`, storeOp: `store` }, // Attachments
O = { colorAttachments: [ A ] }, // Render Pass Descriptor
module = device.createShaderModule({ code }),
PO = { layout: `auto`,
vertex: { module, entryPoint: `vs`,
buffers:[
{
arrayStride: 28, // (3+4)*4.; (xyz+rgba)*4.
attributes: [
{ shaderLocation:0, offset:0, format:`float32x3` }, // p: vec3f [xyz]
{ shaderLocation:1, offset:12, format:`float32x4` }, // c: vec4f [rgba]; 3*4.
]
}
]
},
fragment: { module, entryPoint: `fs`, targets: [{ format }] }
},
P = device.createRenderPipeline( PO ),
E,R, VB = device.createBuffer({ size: VB_$.byteLength, usage: 40 }); // VERTEX | COPY_DST
function draw(){
A.view=C.getCurrentTexture().createView();
E=device.createCommandEncoder();
R=E.beginRenderPass(O);
R.setPipeline(P);
R.setVertexBuffer(0,VB);
R.draw(6); // 2*3 = 2 triangles with 3 vertices each
R.end();
Q.submit([ E.finish() ])
}
C.configure({ device, format })
Q.writeBuffer(VB,0,VB_$);
draw()
</script>
Also, it need to do without using hardcoded colors. Colors and vertex positions must be passed to shader using Vertex Buffer !
PS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - I can't use Uniform or Storage buffers, because browser says: "Unsupported bit-flag set (descriptor range flags 10002).
D3D12 serialize root signature failed with E_INVALIDARG (0x80070057) at CheckHRESULTImpl (....\third_party\dawn\src\dawn\native\d3d\D3DError.cpp:96) at Initialize (....\third_party\dawn\src\dawn\native\d3d12\PipelineLayoutD3D12.cpp:350)"
You can't access vertex buffer data per triangle vs per vertex. You can use storage buffers. If you got an error you must have set your bits set wrong
You can see using storage buffers for vertex data here
Copying that example here and modifying to color per triangle
// WebGPU Storage Buffer vertices
// from https://webgpufundamentals.org/webgpu/webgpu-storage-buffer-vertices.html
// A random number between [min and max)
// With 1 argument it will be [0 to min)
// With no arguments it will be [0 to 1)
const rand = (min, max) => {
if (min === undefined) {
min = 0;
max = 1;
} else if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
};
function createCircleVertices({
radius = 1,
numSubdivisions = 24,
innerRadius = 0,
startAngle = 0,
endAngle = Math.PI * 2,
} = {}) {
// 2 triangles per subdivision, 3 verts per tri, 2 values (xy) each.
const numVertices = numSubdivisions * 3 * 2;
const vertexData = new Float32Array(numSubdivisions * 2 * 3 * 2);
const colorData = new Uint32Array(numSubdivisions * 2)
let offset = 0;
const addVertex = (x, y) => {
vertexData[offset++] = x;
vertexData[offset++] = y;
};
let colorOffset = 0;
const addColor = (r, g, b, a) => {
colorData[colorOffset++] =
((a | 0) << 24) |
((b | 0) << 16) |
((g | 0) << 8) |
((r | 0) << 0) ;
};
const randColor = () => {
return [
rand(256),
rand(256),
rand(256),
255,
];
};
// 2 vertices per subdivision
//
// 0--1 4
// | / /|
// |/ / |
// 2 3--5
for (let i = 0; i < numSubdivisions; ++i) {
const angle1 = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions;
const angle2 = startAngle + (i + 1) * (endAngle - startAngle) / numSubdivisions;
const c1 = Math.cos(angle1);
const s1 = Math.sin(angle1);
const c2 = Math.cos(angle2);
const s2 = Math.sin(angle2);
// first triangle
addVertex(c1 * radius, s1 * radius);
addVertex(c2 * radius, s2 * radius);
addVertex(c1 * innerRadius, s1 * innerRadius);
addColor(...randColor());
// second triangle
addVertex(c1 * innerRadius, s1 * innerRadius);
addVertex(c2 * radius, s2 * radius);
addVertex(c2 * innerRadius, s2 * innerRadius);
addColor(...randColor());
}
return {
vertexData,
colorData,
numVertices,
};
}
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
// Get a WebGPU context from the canvas and configure it
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
});
const module = device.createShaderModule({
code: `
struct PerVertexData {
position: vec2f,
};
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
struct Uniforms {
mat: mat4x4f,
};
@group(0) @binding(0) var<storage, read> perVertData: array<PerVertexData>;
@group(0) @binding(1) var<storage, read> perTriData: array<u32>;
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32,
) -> VSOutput {
let triangleIndex = vertexIndex / 3;
let vert = perVertData[vertexIndex];
let tri = perTriData[triangleIndex];
var vsOut: VSOutput;
vsOut.position = vec4f(vert.position, 0.0, 1.0);
vsOut.color = unpack4x8unorm(tri);
return vsOut;
}
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
return vsOut.color;
}
`,
});
const pipeline = device.createRenderPipeline({
label: 'storage buffer vertices',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format: presentationFormat }],
},
});
const kNumObjects = 100;
const objectInfos = [];
// setup a storage buffer with vertex data
const { vertexData, colorData, numVertices } = createCircleVertices({
radius: 0.5,
innerRadius: 0.25,
});
const vertexStorageBuffer = device.createBuffer({
label: 'storage buffer per vert data',
size: vertexData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexStorageBuffer, 0, vertexData);
const colorStorageBuffer = device.createBuffer({
label: 'storage buffer per color data',
size: colorData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(colorStorageBuffer, 0, colorData);
const bindGroup = device.createBindGroup({
label: 'bind group for objects',
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: vertexStorageBuffer }},
{ binding: 1, resource: { buffer: colorStorageBuffer }},
],
});
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
};
function render() {
// Get the current texture from the canvas context and
// set it as the texture to render to.
renderPassDescriptor.colorAttachments[0].view =
context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(numVertices);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
const width = entry.contentBoxSize[0].inlineSize;
const height = entry.contentBoxSize[0].blockSize;
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
// re-render
render();
}
});
observer.observe(canvas);
}
function fail(msg) {
alert(msg);
}
main();
@import url(https://webgpufundamentals.org/webgpu/resources/webgpu-lesson.css);
html, body {
margin: 0; /* remove the default margin */
height: 100%; /* make the html,body fill the page */
}
canvas {
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
}
<canvas></canvas>
Note that the solution above assumed the goal was to have 1 piece of color data per triangle. If the goal is to have 1 color per triangle ignoring extra data then you can repeat the data. You can also use @interpolate(flat)
in your inter-stage color variable to have it not interpolate the colors.
Also, you can use instancing where some of your vertex data only changes once per instance. That probably doesn't fit your use case but just incase, here's how to instance with vertex buffers, and here's how to instance with storage buffers