Public
Edited
Jun 2, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof interval = Inputs.range([0.1, 1], { value: 1, label: 'Frame length (s)', step: 0.05})
Insert cell
currentStep = {
let step = 0
while (true) {
// In updateGrid()
// Move the encoder creation to the top of the function.
const encoder = gpu.device.createCommandEncoder();
const computePass = encoder.beginComputePass();
// New lines
computePass.setPipeline(pipeline.simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);
// New lines
const workgroupCount = Math.ceil(settings.GRID_SIZE / settings.WORKGROUP_SIZE);
computePass.dispatchWorkgroups(workgroupCount, workgroupCount);
computePass.end();

step = (step + 1) % 2
// Start a render pass
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: canvas.context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: backgroundSetting.color,
storeOp: "store",
}]
});

// Draw the grid.
pass.setPipeline(pipeline.cellPipeline);
pass.setBindGroup(0, bindGroups[step % 2]); // Updated!
pass.setVertexBuffer(0, geodata.vertexBuffer);
pass.draw(geodata.vertexBuffer.size /4 /2, settings.GRID_SIZE * settings.GRID_SIZE);

// End the render pass and submit the command buffer
pass.end();
gpu.device.queue.submit([encoder.finish()]);
yield Promises.tick(interval*1000, step)
}
}
Insert cell
bindGroupLayout = {
// Create the bind group layout and pipeline layout.
const bindGroupLayout = gpu.device.createBindGroupLayout({
label: "Cell Bind Group Layout",
entries: [{
binding: 0,
// Add GPUShaderStage.FRAGMENT here if you are using the `grid` uniform in the fragment shader.
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
buffer: {} // Grid uniform buffer
}, {
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "read-only-storage"} // Cell state input buffer
}, {
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage"} // Cell state output buffer
}]
});
return bindGroupLayout
}
Insert cell
bindGroups = {
// Create a uniform buffer that describes the grid.
const uniformArray = new Float32Array([settings.GRID_SIZE, settings.GRID_SIZE]);
const uniformBuffer = gpu.device.createBuffer({
label: "Grid Uniforms",
size: uniformArray.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
gpu.device.queue.writeBuffer(uniformBuffer, 0, uniformArray);
const bindGroups = [
gpu.device.createBindGroup({
label: "Cell renderer bind group A",
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: state.cellStateStorage[0] }
}, {
binding: 2, // New Entry
resource: { buffer: state.cellStateStorage[1] }
}],
}),
gpu.device.createBindGroup({
label: "Cell renderer bind group B",
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: state.cellStateStorage[1] }
}, {
binding: 2, // New Entry
resource: { buffer: state.cellStateStorage[0] }
}],
})
];
return bindGroups
}
Insert cell
settings = {
return {
GRID_SIZE: 32,
WORKGROUP_SIZE: 8,
}
}
Insert cell
geodata = {
const square = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
]);
const vertexBuffer = gpu.device.createBuffer({
label: "Cell vertices",
size: square.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
gpu.device.queue.writeBuffer(vertexBuffer, /*bufferOffset=*/0, square);
const vertexBufferLayout = {
arrayStride: 8,
attributes: [{
format: "float32x2",
offset: 0,
shaderLocation: 0, // Position, see vertex shader
}],
};
return {
vertexBuffer,
vertexBufferLayout,
}
}
Insert cell
shaderModule = {
const cellShaderModule = gpu.device.createShaderModule({
label: "Cell shader",
code: `
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};

struct VertexOutput {
@builtin(position) pos: vec4f,
@location(0) cell: vec2f, // New line!
};

@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;

@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let state = f32(cellStateIn[input.instance]); // New line!

let cellOffset = cell / grid * 2;
let gridPos = (input.pos*state+1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell; // New line!
return output;
}

@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
let c = input.cell/grid;
return vec4f(c.x+(1-c.x)*0.5, c.y+(1-c.x)*0.5, 1.0-(c.x+c.y)*0.5, 1);
}

// New function
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
fn cellActive(x: u32, y: u32) -> u32 {
return cellStateIn[cellIndex(vec2(x, y))];
}
@compute
@workgroup_size(${settings.WORKGROUP_SIZE}, ${settings.WORKGROUP_SIZE}) // New line
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
let activeNeighbors = cellActive(cell.x+1, cell.y+1) +
cellActive(cell.x+1, cell.y) +
cellActive(cell.x+1, cell.y-1) +
cellActive(cell.x, cell.y-1) +
cellActive(cell.x-1, cell.y-1) +
cellActive(cell.x-1, cell.y) +
cellActive(cell.x-1, cell.y+1) +
cellActive(cell.x, cell.y+1);

let i = cellIndex(cell.xy);
// Conway's game of life rules:
switch activeNeighbors {
case 2: { // Active cells with 2 neighbors stay active.
cellStateOut[i] = cellStateIn[i];
}
case 3: { // Cells with 3 neighbors become or stay active.
cellStateOut[i] = 1;
}
default: { // Cells with < 2 or > 3 neighbors become inactive.
cellStateOut[i] = 0;
}
}
}
`
});
return cellShaderModule
}
Insert cell
pipeline = {
const pipelineLayout = gpu.device.createPipelineLayout({
label: "Cell Pipeline Layout",
bindGroupLayouts: [ bindGroupLayout ],
});
const cellPipeline = gpu.device.createRenderPipeline({
label: "Cell pipeline",
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: "vertexMain",
buffers: [geodata.vertexBufferLayout]
},
fragment: {
module: shaderModule,
entryPoint: "fragmentMain",
targets: [{
format: navigator.gpu.getPreferredCanvasFormat()
}]
}
});
// Create a compute pipeline that updates the game state.
const simulationPipeline = gpu.device.createComputePipeline({
label: "Simulation pipeline",
layout: pipelineLayout,
compute: {
module: shaderModule,
entryPoint: "computeMain",
}
});
return {
cellPipeline,
simulationPipeline,
}
}
Insert cell
state = {
button
// Create an array representing the active state of each cell.
const cellStateArray = new Uint32Array(settings.GRID_SIZE * settings.GRID_SIZE);

// Create a storage buffer to hold the cell state.
const cellStateStorage = [
gpu.device.createBuffer({
label: "Cell State A",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
}),
gpu.device.createBuffer({
label: "Cell State B",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
})
];
// Mark every third cell of the first grid as active.
for (let i = 0; i < cellStateArray.length; i+=3) {
cellStateArray[i] = Math.random() > 0.3 ? 1 : 0;
}
gpu.device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
// Mark every other cell of the second grid as active.
// for (let i = 0; i < cellStateArray.length; i++) {
// cellStateArray[i] = i % 2;
// }
// gpu.device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray);
return {
cellStateStorage,
}
}
Insert cell
gpu = {
if (!navigator.gpu) {
return new Error('WebGPU is not supported by your browser.')
}
const adapter = await navigator.gpu.requestAdapter()
if (!adapter) {
throw new Error("No appropriate GPUAdapter found.")
}
const device = await adapter.requestDevice();
return {
device
}
}
Insert cell
canvas = {
const canvas = DOM.canvas(320, 320)
const context = canvas.getContext("webgpu")
const format = navigator.gpu.getPreferredCanvasFormat()
context.configure({
alphaMode: backgroundSetting.alphaMode,
device: gpu.device,
format,
})
return {
canvas,
context,
format,
}
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more