{
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = util.ctx(512, 512);
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({
code: `
struct StaticData {
color: vec4f,
offset: vec2f,
};
struct DynamicData {
scale: vec2f,
};
struct Vertex {
position: vec2f,
};
struct VertexOut {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
@group(0) @binding(0) var<storage, read> staticData: array<StaticData>;
@group(0) @binding(1) var<storage, read> dynamicData: array<DynamicData>;
@group(0) @binding(2) var<storage, read> pos: array<Vertex>;
@vertex
fn vs(
@builtin(vertex_index) vertexIndex : u32,
@builtin(instance_index) instanceIndex: u32
) -> VertexOut {
let dynamicDatum = dynamicData[instanceIndex];
let staticDatum = staticData[instanceIndex];
var vsOut: VertexOut;
vsOut.position = vec4f(
pos[vertexIndex].position * dynamicDatum.scale + staticDatum.offset, 0.0, 1.0
);
vsOut.color = staticDatum.color;
return vsOut;
}
@fragment
fn fs(vsOut: VertexOut) -> @location(0) vec4f {
return vsOut.color;
}
`
})
//
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format }]
}
})
const kNumObjects = 100;
const data = [];
// create 2 storage buffers
const staticUnitSize =
4 * 4 + // color is 4 32bit floats (4bytes each)
2 * 4 + // offset is 2 32bit floats (4bytes each)
2 * 4; // padding
const dynamicUnitSize =
2 * 4; // scale is 2 32bit floats (4bytes each)
const staticStorageBufferSize = staticUnitSize * kNumObjects;
const dynamicStorageBufferSize = dynamicUnitSize * kNumObjects;
const staticStorageBuffer = device.createBuffer({
size: staticStorageBufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
const dynamicStorageBuffer = device.createBuffer({
size: dynamicStorageBufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
// offsets to the various uniform values in float32 indices
const kColorOffset = 0;
const kOffsetOffset = 4;
//
const kScaleOffset = 0;
const staticStorageValues = new Float32Array(staticStorageBufferSize / 4);
for (let i = 0; i < kNumObjects; i++) {
const staticOffset = i * (staticUnitSize / 4);
// these are only set once so set them now
// see note [1] below
const rbg = util.arr(3, () => util.rand())
staticStorageValues.set([...rbg, 1], staticOffset + kColorOffset); // set color
const pos = util.arr(3, () => util.rand(-0.9, 0.9))
staticStorageValues.set(pos, staticOffset + kOffsetOffset); // set offset
data.push({ scale: util.rand(0.2, 0.5) });
}
// write to gpu
device.queue.writeBuffer(staticStorageBuffer, 0, staticStorageValues);
// used to update dynamicStorageBuffer
const storageValues = new Float32Array(dynamicStorageBufferSize / 4);
// setup a storage buffer with vertex data
// [1] all circles will share the same (inner) radius, but their individual scales,
// positions, and colors are determined above
const { vertices, count: vertexCount } = util.geom.circle({ r: 0.5, innerR: 0 });
const vertexStorageBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexStorageBuffer, 0, vertices);
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: staticStorageBuffer }},
{ binding: 1, resource: { buffer: dynamicStorageBuffer }},
{ binding: 2, resource: { buffer: vertexStorageBuffer }},
]
});
const renderPassDescriptor = {
colorAttachments: [
{
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
view: undefined, // to be filled out when we render
},
],
};
function render() {
renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
// Set the uniform values in our JavaScript side Float32Array
// can cache this w a flag, esp since getting canvas dims is expensive
const aspect = context.canvas.width / context.canvas.height;
// set scales for each object
data.forEach(({ scale }, i) => {
const offset = i * (dynamicUnitSize / 4);
storageValues.set([scale / aspect, scale], offset + kScaleOffset); // set scale
});
// upload all scales at once
device.queue.writeBuffer(dynamicStorageBuffer, 0, storageValues);
pass.setBindGroup(0, bindGroup);
pass.draw(vertexCount, kNumObjects);
pass.end();
device.queue.submit([encoder.finish()]);
}
render()
return context.canvas;
}