export const WARMUP = { framesBeforeReady: 360 };
export const PARAMS = {
count: { value: 2000, min: 200, max: 5000, step: 100, label: "Particles", folder: "Structure", rebuildOnChange: true },
species: { value: 4, min: 2, max: 6, step: 1, label: "Species", folder: "Structure", rebuildOnChange: true },
rmax: { value: 0.15, min: 0.05, max: 0.4, step: 0.005, label: "Interaction Radius", folder: "Behavior" },
friction:{ value: 0.5, min: 0.1, max: 0.99, step: 0.01, label: "Friction", folder: "Behavior" },
force: { value: 50, min: 5, max: 200, step: 1, label: "Force Strength", folder: "Behavior" },
};
export const SHARE = { bookmarked: ["count","species","rmax","friction","force"] };
const COMPUTE_WGSL = /* wgsl */ `
struct Uniforms {
time: f32,
res: vec2<f32>,
seed: f32,
count: u32,
dt: f32,
};
struct Particle {
pos: vec2<f32>,
vel: vec2<f32>,
sp: u32,
_pad0: u32,
_pad1: u32,
_pad2: u32,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage, read> srcParticles: array<Particle>;
@group(0) @binding(2) var<storage, read_write> dstParticles: array<Particle>;
@group(0) @binding(3) var<storage, read> forceMatrix: array<f32>;
@group(0) @binding(4) var<uniform> sim: vec4<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x;
if (i >= u.count) { return; }
let me = srcParticles[i];
let rmax = sim.x;
let fric = sim.y;
let strength = sim.z;
let species = u32(sim.w);
var force = vec2<f32>(0.0);
for (var j: u32 = 0u; j < u.count; j = j + 1u) {
if (j == i) { continue; }
let other = srcParticles[j];
var diff = other.pos - me.pos;
if (diff.x > 0.5) { diff.x = diff.x - 1.0; } else if (diff.x < -0.5) { diff.x = diff.x + 1.0; }
if (diff.y > 0.5) { diff.y = diff.y - 1.0; } else if (diff.y < -0.5) { diff.y = diff.y + 1.0; }
let r = length(diff);
if (r > 0.0 && r < rmax) {
let a = forceMatrix[me.sp * species + other.sp];
var f = 0.0;
if (r < 0.3 * rmax) {
f = (r / (0.3 * rmax)) - 1.0;
} else {
let t = (r - 0.3 * rmax) / (rmax - 0.3 * rmax);
f = a * (1.0 - abs(2.0 * t - 1.0));
}
force = force + (diff / r) * f;
}
}
var newVel = me.vel + force * strength * u.dt;
newVel = newVel * pow(fric, u.dt * 60.0);
var newPos = me.pos + newVel * u.dt;
newPos.x = fract(newPos.x + 1.0);
newPos.y = fract(newPos.y + 1.0);
dstParticles[i] = Particle(newPos, newVel, me.sp, 0u, 0u, 0u);
}
`;
const RENDER_WGSL = /* wgsl */ `
struct Uniforms {
time: f32,
res: vec2<f32>,
seed: f32,
count: u32,
dt: f32,
};
struct Particle {
pos: vec2<f32>,
vel: vec2<f32>,
sp: u32,
_pad0: u32,
_pad1: u32,
_pad2: u32,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage, read> particles: array<Particle>;
struct VOut { @builtin(position) pos: vec4<f32>, @location(0) col: vec3<f32>, @location(1) uv: vec2<f32> };
@vertex
fn vmain(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
var quad = array<vec2<f32>, 6>(
vec2(-1.0,-1.0), vec2(1.0,-1.0), vec2(1.0,1.0),
vec2(-1.0,-1.0), vec2(1.0,1.0), vec2(-1.0,1.0)
);
let p = particles[ii];
let aspect = u.res.x / u.res.y;
let size = 0.005;
let q = quad[vi];
let center = (p.pos - vec2(0.5, 0.5)) * 2.0;
let pos = vec2(center.x + q.x * size, center.y + q.y * size * aspect);
let palette = array<vec3<f32>, 6>(
vec3(0.95,0.30,0.30), vec3(0.30,0.70,0.95),
vec3(0.95,0.85,0.30), vec3(0.30,0.95,0.55),
vec3(0.85,0.30,0.95), vec3(0.95,0.55,0.20),
);
var out: VOut;
out.pos = vec4(pos, 0.0, 1.0);
out.col = palette[p.sp % 6u];
out.uv = q;
return out;
}
@fragment
fn fmain(in: VOut) -> @location(0) vec4<f32> {
let r = length(in.uv);
let alpha = smoothstep(1.0, 0.6, r);
return vec4(in.col * alpha, alpha);
}
`;
export function sketchSetup(device, ctx, format, params, seed, uniformBuffer) {
const N = params.count;
const S = params.species;
const PARTICLE_BYTES = 32;
const particleData = new ArrayBuffer(N * PARTICLE_BYTES);
const f32 = new Float32Array(particleData);
const u32 = new Uint32Array(particleData);
let s = (seed | 0) || 1;
function rng() { s = (s * 1664525 + 1013904223) | 0; return ((s >>> 0) / 4294967296); }
for (let i = 0; i < N; i++) {
f32[i * 8 + 0] = rng();
f32[i * 8 + 1] = rng();
f32[i * 8 + 2] = (rng() - 0.5) * 0.1;
f32[i * 8 + 3] = (rng() - 0.5) * 0.1;
u32[i * 8 + 4] = Math.floor(rng() * S);
}
const bufA = device.createBuffer({
size: particleData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Uint8Array(bufA.getMappedRange()).set(new Uint8Array(particleData));
bufA.unmap();
const bufB = device.createBuffer({
size: particleData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
});
const matrix = new Float32Array(S * S);
for (let i = 0; i < S * S; i++) matrix[i] = (rng() * 2 - 1) * 0.8;
const matBuf = device.createBuffer({
size: matrix.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(matBuf.getMappedRange()).set(matrix);
matBuf.unmap();
const simBuf = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const computeModule = device.createShaderModule({ code: COMPUTE_WGSL });
const renderModule = device.createShaderModule({ code: RENDER_WGSL });
const computeLayout = device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
],
});
const computePipeline = device.createComputePipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [computeLayout] }),
compute: { module: computeModule, entryPoint: 'main' },
});
const renderLayout = device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
],
});
const renderPipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [renderLayout] }),
vertex: { module: renderModule, entryPoint: 'vmain' },
fragment: {
module: renderModule, entryPoint: 'fmain',
targets: [{
format,
blend: {
color: { srcFactor: 'src-alpha', dstFactor: 'one' },
alpha: { srcFactor: 'one', dstFactor: 'one' },
},
}],
},
primitive: { topology: 'triangle-list' },
});
function makeBindGroups(srcBuf, dstBuf) {
return {
compute: device.createBindGroup({
layout: computeLayout,
entries: [
{ binding: 0, resource: { buffer: uniformBuffer } },
{ binding: 1, resource: { buffer: srcBuf } },
{ binding: 2, resource: { buffer: dstBuf } },
{ binding: 3, resource: { buffer: matBuf } },
{ binding: 4, resource: { buffer: simBuf } },
],
}),
render: device.createBindGroup({
layout: renderLayout,
entries: [
{ binding: 0, resource: { buffer: uniformBuffer } },
{ binding: 1, resource: { buffer: srcBuf } },
],
}),
};
}
return {
bufA, bufB, simBuf, matBuf,
computePipeline, renderPipeline,
bindings: [makeBindGroups(bufA, bufB), makeBindGroups(bufB, bufA)],
pingpong: 0,
count: N,
species: S,
};
}
export function computePass(device, encoder, state, params, time, delta) {
const sim = new Float32Array([params.rmax, params.friction, params.force, params.species]);
device.queue.writeBuffer(state.simBuf, 0, sim);
const cp = encoder.beginComputePass();
cp.setPipeline(state.computePipeline);
cp.setBindGroup(0, state.bindings[state.pingpong].compute);
cp.dispatchWorkgroups(Math.ceil(state.count / 64));
cp.end();
state.pingpong = 1 - state.pingpong;
}
export function renderPass(device, encoder, view, state) {
const rp = encoder.beginRenderPass({
colorAttachments: [{
view, loadOp: 'clear', storeOp: 'store',
clearValue: { r: 0.02, g: 0.02, b: 0.04, a: 1 },
}],
});
rp.setPipeline(state.renderPipeline);
rp.setBindGroup(0, state.bindings[state.pingpong].render);
rp.draw(6, state.count);
rp.end();
}