Skip to content

Instantly share code, notes, and snippets.

@ayushmi
Created November 6, 2025 21:42
Show Gist options
  • Select an option

  • Save ayushmi/634dfe4b402d743aaf6f62bc1eea1245 to your computer and use it in GitHub Desktop.

Select an option

Save ayushmi/634dfe4b402d743aaf6f62bc1eea1245 to your computer and use it in GitHub Desktop.
10,000 robots with collision avoidance in WebGPU in HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>swarmbots.wgpu — 10k robots</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<style>
:root { color-scheme: dark; }
body { margin:0; font:14px/1.3 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:#0b0e14; color:#e6e9ef; }
#wrap { display:grid; grid-template-columns: 320px 1fr; min-height:100vh; }
aside { padding:16px; border-right:1px solid #151a24; background:#0f1320; }
h1 { font-size:16px; margin:0 0 8px 0; }
.row { display:flex; align-items:center; gap:8px; margin:8px 0; }
.row label { width:140px; color:#aab3c5; }
.row input[type=range] { flex:1; }
.val { min-width:52px; text-align:right; color:#9fc2ff; font-variant-numeric: tabular-nums; }
.note { color:#8a94a7; font-size:12px; margin:8px 0 0; }
.split { height:1px; background:#151a24; margin:12px 0; }
#hud { position:fixed; right:12px; top:10px; font-size:12px; padding:6px 8px; background:#0e1220c7; border:1px solid #1a2130; border-radius:8px; backdrop-filter: blur(4px); }
#stageWrap { position:relative; background:#07090e; }
#stage { width:100%; height:100vh; display:block; }
#hideUI:checked ~ aside, #hideUI:checked ~ #hud { display:none; }
.btn { background:#1b2a4d; border:1px solid #24345e; color:#cfe0ff; padding:6px 10px; border-radius:8px; cursor:pointer; }
.btn:active { transform: translateY(1px); }
.mini { color:#9aa7bf; font-size:12px; margin-top:2px; }
</style>
</head>
<body>
<input id="hideUI" type="checkbox" hidden>
<div id="wrap">
<aside>
<h1>swarmbots.wgpu</h1>
<div class="row"><label>DPR cap</label>
<input id="dprCap" type="range" min="0.5" max="2" step="0.1" value="1">
<div class="val" id="dprCapVal">1.0</div>
</div>
<div class="row"><label>Render scale</label>
<input id="resScale" type="range" min="0.5" max="1.0" step="0.05" value="1">
<div class="val" id="resScaleVal">1.00</div>
</div>
<div class="mini">Internal resolution = CSS size × min(devicePixelRatio, DPR cap) × render scale</div>
<div class="split"></div>
<div class="row"><label>Agents</label>
<input id="agentCount" type="range" min="100" max="20000" step="100" value="10000">
<div class="val" id="agentCountVal">10000</div>
</div>
<div class="row"><label>cellSize</label>
<input id="cellSize" type="range" min="8" max="48" step="1" value="20">
<div class="val" id="cellSizeVal">20</div>
</div>
<div class="row"><label>maxPerCell</label>
<input id="maxPerCell" type="range" min="8" max="64" step="1" value="28">
<div class="val" id="maxPerCellVal">28</div>
</div>
<div class="split"></div>
<div class="row"><label>Physics rate</label>
<input id="physEvery" type="range" min="1" max="4" step="1" value="2">
<div class="val" id="physEveryVal">2</div>
</div>
<div class="mini">1 = update every frame (~60 Hz), 2 ≈ 30 Hz, 3 ≈ 20 Hz…</div>
<div class="row"><label>Workgroup size</label>
<select id="wgSize">
<option>64</option><option selected>128</option><option>256</option>
</select>
<button class="btn" id="applyWg">Apply</button>
</div>
<div class="split"></div>
<div class="row"><label>Triangle size</label>
<input id="triSize" type="range" min="2" max="12" step="0.5" value="6">
<div class="val" id="triSizeVal">6.0</div>
</div>
<div class="row"><label>Vel color strength</label>
<input id="velColor" type="range" min="0" max="2" step="0.05" value="1.0">
<div class="val" id="velColorVal">1.00</div>
</div>
<div class="split"></div>
<div class="row"><label>Overlays</label>
<button class="btn" id="toggleUI">Hide / Show</button>
</div>
<div class="note">macOS tip: disable “Low Power Mode” for higher sustained FPS.</div>
</aside>
<div id="stageWrap">
<canvas id="stage"></canvas>
</div>
</div>
<div id="hud">FPS: <span id="fps">0</span> · physics: <span id="hz">~30</span> · cells: <span id="cells">0</span></div>
<script>
(async function(){
if (!('gpu' in navigator)) { alert('WebGPU not available'); return; }
// ---------- DOM ----------
const qs = id => document.getElementById(id);
const canvas = qs('stage');
const fpsEl = qs('fps'), hzEl = qs('hz'), cellsEl = qs('cells');
const ui = {
dprCap: qs('dprCap'), dprCapVal: qs('dprCapVal'),
resScale: qs('resScale'), resScaleVal: qs('resScaleVal'),
agentCount: qs('agentCount'), agentCountVal: qs('agentCountVal'),
cellSize: qs('cellSize'), cellSizeVal: qs('cellSizeVal'),
maxPerCell: qs('maxPerCell'), maxPerCellVal: qs('maxPerCellVal'),
physEvery: qs('physEvery'), physEveryVal: qs('physEveryVal'),
wgSize: qs('wgSize'), applyWg: qs('applyWg'),
triSize: qs('triSize'), triSizeVal: qs('triSizeVal'),
velColor: qs('velColor'), velColorVal: qs('velColorVal'),
toggleUI: qs('toggleUI'), hideUI: qs('hideUI')
};
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
// ---------- State ----------
let state = {
cssW: 0, cssH: 0, dpr: 1, resScale: 1,
agentCount: parseInt(ui.agentCount.value,10),
cellSize: parseInt(ui.cellSize.value,10),
maxPerCell: parseInt(ui.maxPerCell.value,10),
physEvery: parseInt(ui.physEvery.value,10),
wgSize: parseInt(ui.wgSize.value,10),
triSize: parseFloat(ui.triSize.value),
velColor: parseFloat(ui.velColor.value),
};
// ---------- Buffers ----------
const U_PAD = 64; // uniform padding (16B aligned)
const paramsBuf = device.createBuffer({ size: U_PAD, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
const screenBuf = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
let agentsA, agentsB, gridCounts, gridIdx;
let gridCols=0, gridRows=0, gridCells=0;
function allocAgents(n){
const bytes = n * 16; // vec2 pos + vec2 vel (f32)
agentsA = device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
agentsB = device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX });
// init positions/velocities
const tmp = new Float32Array(n*4);
for (let i=0;i<n;i++){
tmp[i*4+0] = Math.random()*state.cssW;
tmp[i*4+1] = Math.random()*state.cssH;
const ang = Math.random()*Math.PI*2;
tmp[i*4+2] = Math.cos(ang)*20;
tmp[i*4+3] = Math.sin(ang)*20;
}
device.queue.writeBuffer(agentsA, 0, tmp.buffer);
}
function allocGrid(){
gridCols = Math.max(1, Math.floor(state.cssW / state.cellSize));
gridRows = Math.max(1, Math.floor(state.cssH / state.cellSize));
gridCells = gridCols * gridRows;
const countsBytes = gridCells * 4; // atomic<u32>
const idxBytes = gridCells * state.maxPerCell * 4; // u32 agent indices
gridCounts = device.createBuffer({ size: countsBytes, usage: GPUBufferUsage.STORAGE });
gridIdx = device.createBuffer({ size: idxBytes, usage: GPUBufferUsage.STORAGE });
cellsEl.textContent = gridCells.toString();
}
// ---------- Shaders (with dynamic workgroup size & params) ----------
function commonWGSL(){ return /*wgsl*/`
struct Agent { pos: vec2<f32>, vel: vec2<f32> };
struct Params {
dt: f32, maxSpeed: f32, neighborR: f32, sepR: f32,
alignW: f32, cohW: f32, sepW: f32, turnW: f32,
cellSize: f32, gridCols: f32, gridRows: f32, agentCount: f32,
triSize: f32, velColor: f32, _pad0: f32, _pad1: f32,
};
@group(0) @binding(2) var<uniform> params: Params;
`;}
function clearWGSL(){ return /*wgsl*/`
${commonWGSL()}
@group(0) @binding(3) var<storage, read_write> gridCounts : array<atomic<u32>>;
@compute @workgroup_size(${state.wgSize})
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let idx = gid.x;
if (idx < u32(params.gridCols * params.gridRows)) {
atomicStore(&gridCounts[idx], 0u);
}
}
`;}
function binWGSL(){ return /*wgsl*/`
${commonWGSL()}
@group(0) @binding(0) var<storage, read> agents : array<Agent>;
@group(0) @binding(3) var<storage, read_write> gridCounts: array<atomic<u32>>;
@group(0) @binding(4) var<storage, read_write> gridIdx : array<u32>;
fn wrap(p: vec2<f32>, w: f32, h: f32) -> vec2<f32> {
var x = p.x; var y = p.y;
if (x < 0.0) { x += w; } else if (x >= w) { x -= w; }
if (y < 0.0) { y += h; } else if (y >= h) { y -= h; }
return vec2<f32>(x,y);
}
@compute @workgroup_size(${state.wgSize})
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x;
if (i >= u32(params.agentCount)) { return; }
let a = agents[i];
let col = clamp(i32(a.pos.x / params.cellSize), 0, i32(params.gridCols)-1);
let row = clamp(i32(a.pos.y / params.cellSize), 0, i32(params.gridRows)-1);
let cell = u32(row) * u32(params.gridCols) + u32(col);
let slot = atomicAdd(&gridCounts[cell], 1u);
if (slot < u32(${state.maxPerCell})) {
gridIdx[cell * u32(${state.maxPerCell}) + slot] = i;
}
}
`;}
function updateWGSL(){ return /*wgsl*/`
${commonWGSL()}
@group(0) @binding(0) var<storage, read> agentsIn : array<Agent>;
@group(0) @binding(1) var<storage, read_write> agentsOut : array<Agent>;
@group(0) @binding(3) var<storage, read_write> gridCounts: array<atomic<u32>>;
@group(0) @binding(4) var<storage, read_write> gridIdx : array<u32>;
@group(0) @binding(5) var<uniform> screen : vec2<f32>;
fn shortest_delta(a: f32, b: f32, dim: f32) -> f32 {
var d = b - a;
if (d > dim*0.5) { d -= dim; }
if (d < -dim*0.5) { d += dim; }
return d;
}
@compute @workgroup_size(${state.wgSize})
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x;
if (i >= u32(params.agentCount)) { return; }
var me = agentsIn[i];
let w = screen.x; let h = screen.y;
let cols = i32(params.gridCols); let rows = i32(params.gridRows);
let c = clamp(i32(me.pos.x / params.cellSize), 0, cols-1);
let r = clamp(i32(me.pos.y / params.cellSize), 0, rows-1);
var align = vec2<f32>(0.0,0.0);
var coh = vec2<f32>(0.0,0.0);
var sep = vec2<f32>(0.0,0.0);
var cntAlign:f32 = 0.0; var cntCoh:f32 = 0.0; var cntSep:f32 = 0.0;
// 3x3 neighbors
var dy:i32 = -1;
loop {
if (dy > 1) { break; }
var dx:i32 = -1;
loop {
if (dx > 1) { break; }
let cc = ( ( ( ( (r+dy + rows) % rows) * cols) + ((c+dx + cols) % cols) ) );
let cell = u32(cc);
let n = atomicLoad(&gridCounts[cell]);
let base = cell * u32(${state.maxPerCell});
var k:u32 = 0u;
loop {
if (k >= n || k >= u32(${state.maxPerCell})) { break; }
let j = gridIdx[base + k];
if (j != i) {
let other = agentsIn[j];
let dxw = shortest_delta(me.pos.x, other.pos.x, w);
let dyw = shortest_delta(me.pos.y, other.pos.y, h);
let d2 = dxw*dxw + dyw*dyw;
// align
if (d2 < params.neighborR*params.neighborR) {
align += other.vel; cntAlign += 1.0;
}
// cohesion
if (d2 < params.neighborR*params.neighborR) {
coh += vec2<f32>(other.pos.x, other.pos.y); cntCoh += 1.0;
}
// separation
if (d2 < params.sepR*params.sepR && d2 > 1.0) {
let inv = inverseSqrt(d2);
sep -= vec2<f32>(dxw, dyw) * inv;
cntSep += 1.0;
}
}
k = k + 1u;
}
dx = dx + 1;
}
dy = dy + 1;
}
var acc = vec2<f32>(0.0,0.0);
if (cntAlign > 0.0) {
let desired = normalize(align / cntAlign) * params.maxSpeed;
acc += (desired - me.vel) * params.alignW;
}
if (cntCoh > 0.0) {
let center = coh / cntCoh;
let dir = normalize(center - me.pos);
acc += dir * params.cohW;
}
if (cntSep > 0.0) {
acc += normalize(sep / cntSep) * params.sepW;
}
// small turn toward center to reduce drift
let center = vec2<f32>(w*0.5, h*0.5);
acc += normalize(center - me.pos) * params.turnW;
var v = me.vel + acc * params.dt;
let speed = max(length(v), 1e-3);
if (speed > params.maxSpeed) { v = v * (params.maxSpeed / speed); }
var p = me.pos + v * params.dt;
// wrap
if (p.x < 0.0) { p.x += w; } else if (p.x >= w) { p.x -= w; }
if (p.y < 0.0) { p.y += h; } else if (p.y >= h) { p.y -= h; }
agentsOut[i].pos = p;
agentsOut[i].vel = v;
}
`;}
function vertexWGSL(){ return /*wgsl*/`
${commonWGSL()}
@group(0) @binding(0) var<storage, read> agents : array<Agent>;
@group(0) @binding(5) var<uniform> screen : vec2<f32>;
struct VSOut { @builtin(position) pos: vec4<f32>, @location(0) col: vec3<f32> };
@vertex
fn main(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> VSOut {
let a = agents[iid];
// small triangle in agent-local space
let s = params.triSize;
var v = vec2<f32>(0.0,0.0);
if (vid == 0u) { v = vec2<f32>( 0.0, -1.0)*s; }
if (vid == 1u) { v = vec2<f32>(-0.6, 0.8)*s; }
if (vid == 2u) { v = vec2<f32>( 0.6, 0.8)*s; }
// face along velocity
let dir = normalize(a.vel + vec2<f32>(1e-4,0.0));
let ang = atan2(dir.y, dir.x);
let c = cos(ang); let sn = sin(ang);
let rot = mat2x2<f32>(c,-sn,sn,c);
let world = a.pos + (rot * v);
let ndc = vec2<f32>(
world.x / screen.x * 2.0 - 1.0,
1.0 - world.y / screen.y * 2.0
);
// simple vel-based color
let speed = length(a.vel);
let t = clamp(speed / params.maxSpeed, 0.0, 1.0) * params.velColor;
let col = mix(vec3<f32>(0.3,0.6,1.0), vec3<f32>(1.0,0.6,0.2), t);
var o: VSOut;
o.pos = vec4<f32>(ndc, 0.0, 1.0);
o.col = col;
return o;
}
`;}
const fragWGSL = /*wgsl*/`
@fragment
fn main(@location(0) col: vec3<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(col, 1.0);
}
`;
// ---------- Pipelines ----------
let clearPipe, binPipe, updatePipe, renderPipe;
let bindClear, bindBin, bindUpdate, bindRender;
function makePipelines(){
const clearMod = device.createShaderModule({ code: clearWGSL() });
const binMod = device.createShaderModule({ code: binWGSL() });
const updMod = device.createShaderModule({ code: updateWGSL() });
const vsMod = device.createShaderModule({ code: vertexWGSL() });
const fsMod = device.createShaderModule({ code: fragWGSL });
clearPipe = device.createComputePipeline({
layout: 'auto',
compute: { module: clearMod, entryPoint: 'main' }
});
binPipe = device.createComputePipeline({
layout: 'auto',
compute: { module: binMod, entryPoint: 'main' }
});
updatePipe = device.createComputePipeline({
layout: 'auto',
compute: { module: updMod, entryPoint: 'main' }
});
renderPipe = device.createRenderPipeline({
layout: 'auto',
vertex: { module: vsMod, entryPoint:'main' },
fragment: { module: fsMod, entryPoint:'main', targets:[{ format }] },
primitive: { topology:'triangle-list' }
});
// bind groups (match shader layouts)
bindClear = device.createBindGroup({
layout: clearPipe.getBindGroupLayout(0),
entries: [
{ binding:2, resource:{ buffer: paramsBuf } },
{ binding:3, resource:{ buffer: gridCounts } },
],
});
bindBin = device.createBindGroup({
layout: binPipe.getBindGroupLayout(0),
entries: [
{ binding:0, resource:{ buffer: agentsA } },
{ binding:2, resource:{ buffer: paramsBuf } },
{ binding:3, resource:{ buffer: gridCounts } },
{ binding:4, resource:{ buffer: gridIdx } },
],
});
bindUpdate = device.createBindGroup({
layout: updatePipe.getBindGroupLayout(0),
entries: [
{ binding:0, resource:{ buffer: agentsA } },
{ binding:1, resource:{ buffer: agentsB } },
{ binding:2, resource:{ buffer: paramsBuf } },
{ binding:3, resource:{ buffer: gridCounts } },
{ binding:4, resource:{ buffer: gridIdx } },
{ binding:5, resource:{ buffer: screenBuf } },
],
});
bindRender = device.createBindGroup({
layout: renderPipe.getBindGroupLayout(0),
entries: [
{ binding:0, resource:{ buffer: agentsB } },
{ binding:2, resource:{ buffer: paramsBuf } },
{ binding:5, resource:{ buffer: screenBuf } },
],
});
}
// ---------- Resize / DPR ----------
function resize(){
state.cssW = canvas.clientWidth || window.innerWidth;
state.cssH = canvas.clientHeight || window.innerHeight;
const dprCap = parseFloat(ui.dprCap.value);
state.resScale = parseFloat(ui.resScale.value);
state.dpr = Math.min(window.devicePixelRatio || 1, dprCap) * state.resScale;
const w = Math.max(2, Math.floor(state.cssW * state.dpr));
const h = Math.max(2, Math.floor(state.cssH * state.dpr));
canvas.width = w; canvas.height = h;
context.configure({ device, format, alphaMode:'opaque' });
device.queue.writeBuffer(screenBuf, 0, new Float32Array([state.cssW, state.cssH]).buffer);
allocGrid(); if (agentsA && agentsB) makePipelines(); // call only when agent buffers exist// grid dims changed → rebuild
}
window.addEventListener('resize', ()=>{ resize(); });
// ---------- Uniforms ----------
function writeParams(){
// pack 16 floats (64 bytes)
const dt = 1/60;
const u = new Float32Array(16);
u[0]=dt; u[1]=80; u[2]=22; u[3]=12; // dt, maxSpeed, neighborR, sepR
u[4]=0.06; u[5]=0.03; u[6]=0.2; u[7]=0.005; // alignW, cohW, sepW, turnW
u[8]=state.cellSize; u[9]=gridCols; u[10]=gridRows; u[11]=state.agentCount;
u[12]=state.triSize; u[13]=state.velColor; u[14]=0; u[15]=0;
device.queue.writeBuffer(paramsBuf, 0, u.buffer);
}
// ---------- Frame / passes ----------
function runCompute(encoder){
// 1) clear gridCounts
{
const pass = encoder.beginComputePass();
pass.setPipeline(clearPipe);
pass.setBindGroup(0, bindClear);
pass.dispatchWorkgroups(Math.ceil(gridCells / state.wgSize));
pass.end();
}
// 2) bin agentsA into grid
{
const pass = encoder.beginComputePass();
pass.setPipeline(binPipe);
pass.setBindGroup(0, bindBin);
pass.dispatchWorkgroups(Math.ceil(state.agentCount / state.wgSize));
pass.end();
}
// 3) update into agentsB
{
const pass = encoder.beginComputePass();
pass.setPipeline(updatePipe);
pass.setBindGroup(0, bindUpdate);
pass.dispatchWorkgroups(Math.ceil(state.agentCount / state.wgSize));
pass.end();
}
}
function render(encoder, view){
const pass = encoder.beginRenderPass({
colorAttachments:[{ view, loadOp:'clear', storeOp:'store', clearValue:{r:0.03,g:0.04,b:0.07,a:1} }]
});
pass.setPipeline(renderPipe);
pass.setBindGroup(0, bindRender);
pass.draw(3, state.agentCount, 0, 0);
pass.end();
}
// ---------- UI wiring ----------
function syncLabels(){
ui.dprCapVal.textContent = parseFloat(ui.dprCap.value).toFixed(1);
ui.resScaleVal.textContent = parseFloat(ui.resScale.value).toFixed(2);
ui.agentCountVal.textContent = state.agentCount.toString();
ui.cellSizeVal.textContent = state.cellSize.toString();
ui.maxPerCellVal.textContent = state.maxPerCell.toString();
ui.physEveryVal.textContent = state.physEvery.toString();
ui.triSizeVal.textContent = state.triSize.toFixed(1);
ui.velColorVal.textContent = state.velColor.toFixed(2);
}
ui.dprCap.oninput = ()=>{ syncLabels(); resize(); };
ui.resScale.oninput = ()=>{ syncLabels(); resize(); };
ui.agentCount.oninput = ()=>{ state.agentCount = parseInt(ui.agentCount.value,10); syncLabels(); allocAgents(state.agentCount); makePipelines(); };
ui.cellSize.oninput = ()=>{ state.cellSize = parseInt(ui.cellSize.value,10); syncLabels(); resize(); };
ui.maxPerCell.oninput = ()=>{ state.maxPerCell = parseInt(ui.maxPerCell.value,10); syncLabels(); allocGrid(); makePipelines(); };
ui.physEvery.oninput = ()=>{ state.physEvery = parseInt(ui.physEvery.value,10); syncLabels(); };
ui.applyWg.onclick = ()=>{ state.wgSize = parseInt(ui.wgSize.value,10); makePipelines(); };
ui.triSize.oninput = ()=>{ state.triSize = parseFloat(ui.triSize.value); syncLabels(); writeParams(); };
ui.velColor.oninput = ()=>{ state.velColor = parseFloat(ui.velColor.value); syncLabels(); writeParams(); };
ui.toggleUI.onclick = ()=>{ ui.hideUI.checked = !ui.hideUI.checked; };
// ---------- Boot ----------
// initial sizes
resize();
allocAgents(state.agentCount);
makePipelines();
writeParams();
let tick=0, last=performance.now(), frames=0, acc=0;
function frame(){
const t = performance.now();
const dt = t - last; last = t; frames++; acc += dt;
if (acc >= 500){ fpsEl.textContent = ((frames*1000)/acc).toFixed(0); frames=0; acc=0; }
hzEl.textContent = state.physEvery===1 ? '~60' : (state.physEvery===2 ? '~30' : state.physEvery===3 ? '~20' : '~15');
writeParams();
const encoder = device.createCommandEncoder();
if ((tick++ % state.physEvery) === 0) {
runCompute(encoder);
// swap A/B
[agentsA, agentsB] = [agentsB, agentsA];
// rebind after swap
bindBin = device.createBindGroup({
layout: binPipe.getBindGroupLayout(0),
entries: [
{ binding:0, resource:{ buffer: agentsA } },
{ binding:2, resource:{ buffer: paramsBuf } },
{ binding:3, resource:{ buffer: gridCounts } },
{ binding:4, resource:{ buffer: gridIdx } },
],
});
bindUpdate = device.createBindGroup({
layout: updatePipe.getBindGroupLayout(0),
entries: [
{ binding:0, resource:{ buffer: agentsA } },
{ binding:1, resource:{ buffer: agentsB } },
{ binding:2, resource:{ buffer: paramsBuf } },
{ binding:3, resource:{ buffer: gridCounts } },
{ binding:4, resource:{ buffer: gridIdx } },
{ binding:5, resource:{ buffer: screenBuf } },
],
});
bindRender = device.createBindGroup({
layout: renderPipe.getBindGroupLayout(0),
entries: [
{ binding:0, resource:{ buffer: agentsB } },
{ binding:2, resource:{ buffer: paramsBuf } },
{ binding:5, resource:{ buffer: screenBuf } },
],
});
}
const view = context.getCurrentTexture().createView();
render(encoder, view);
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment