diff --git a/src/index.html b/src/index.html
index 18f119d..10d94f5 100644
--- a/src/index.html
+++ b/src/index.html
@@ -15,7 +15,6 @@
-
) {
+ this.pipeline = device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: device.createShaderModule({
+ code: shader,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ this.uniforms = this.device.createBuffer({
+ size: AgentPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+
+ const serializedAgents = new Float32Array(
+ new ArrayBuffer(agents.length * AGENT_SIZE)
+ );
+ agents.forEach((agent, index) => {
+ serializedAgents[index * 4 + 0] = agent.position[0];
+ serializedAgents[index * 4 + 1] = agent.position[1];
+ serializedAgents[index * 4 + 2] = agent.angle;
+ });
+
+ this.agentsBuffer = device.createBuffer({
+ size: agents.length * AGENT_SIZE,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ device.queue.writeBuffer(this.agentsBuffer, 0, serializedAgents.buffer);
+ }
+
+ public setParameters({
+ width,
+ height,
+ trailWeight,
+ deltaTime,
+ time,
+ moveSpeed,
+ turnSpeed,
+ sensorAngleDegrees,
+ sensorOffsetDst,
+ sensorSize,
+ }: {
+ width: number;
+ height: number;
+ trailWeight: number;
+ deltaTime: number;
+ time: number;
+ moveSpeed: number;
+ turnSpeed: number;
+ sensorAngleDegrees: number;
+ sensorOffsetDst: number;
+ sensorSize: number;
+ }) {
+ this.device.queue.writeBuffer(
+ this.uniforms,
+ 0,
+ new Float32Array([
+ width,
+ height,
+ trailWeight,
+ deltaTime,
+ time,
+ moveSpeed,
+ turnSpeed,
+ sensorAngleDegrees,
+ sensorOffsetDst,
+ sensorSize,
+ ])
+ );
+ }
+
+ public execute(
+ commandEncoder: GPUCommandEncoder,
+ trailMapIn: GPUTexture,
+ trailMapOut: GPUTexture
+ ) {
+ this.ensureBindGroupExists(trailMapIn, trailMapOut);
+
+ const passEncoder = commandEncoder.beginComputePass();
+ passEncoder.setPipeline(this.pipeline);
+ passEncoder.setBindGroup(0, this.bindGroup);
+ passEncoder.dispatchWorkgroups(
+ Math.ceil(this.agentsBuffer.size / AGENT_SIZE / AgentPipeline.WORKGROUP_SIZE)
+ );
+ passEncoder.end();
+ }
+
+ private ensureBindGroupExists(trailMapIn: GPUTexture, trailMapOut: GPUTexture) {
+ if (
+ this.previousTrailMapIn !== trailMapIn ||
+ this.previousTrailMapOut !== trailMapOut
+ ) {
+ this.bindGroup = this.device.createBindGroup({
+ layout: this.pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: this.uniforms,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: this.agentsBuffer,
+ },
+ },
+ {
+ binding: 2,
+ resource: trailMapIn.createView(),
+ },
+ {
+ binding: 3,
+ resource: trailMapOut.createView(),
+ },
+ ],
+ });
+
+ this.previousTrailMapIn = trailMapIn;
+ this.previousTrailMapOut = trailMapOut;
+ }
+ }
+}
diff --git a/src/pipelines/agents/agent.ts b/src/pipelines/agents/agent.ts
new file mode 100644
index 0000000..e204892
--- /dev/null
+++ b/src/pipelines/agents/agent.ts
@@ -0,0 +1,8 @@
+import { vec2 } from 'gl-matrix';
+
+export interface Agent {
+ position: vec2;
+ angle: number;
+}
+
+export const AGENT_SIZE = 4;
diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl
new file mode 100644
index 0000000..822d198
--- /dev/null
+++ b/src/pipelines/agents/agent.wgsl
@@ -0,0 +1,125 @@
+struct Agent {
+ position: vec2,
+ angle: f32,
+}
+
+struct Settings {
+ width : i32,
+ height : i32,
+ trailWeight : f32,
+ deltaTime : f32,
+ time : f32,
+
+ moveSpeed : f32,
+ turnSpeed : f32,
+ sensorAngleDegrees : f32,
+ sensorOffsetDst : f32,
+ sensorSize : f32,
+};
+
+@group(0) @binding(0) var settings : Settings;
+@group(0) @binding(1) var agents: array;
+@group(0) @binding(2) var TrailMapIn : texture_2d;
+@group(0) @binding(3) var TrailMapOut : texture_storage_2d;
+
+
+// Hash function www.cs.ubc.ca/~rbridson/docs/schechter-sca08-turbulence.pdf
+fn hash(state0 : u32) -> u32
+{
+ var state : u32 = state0;
+ state = state ^ 2747636419u;
+ state = state * 2654435769u;
+ state = state ^ (state >> 16u);
+ state = state * 2654435769u;
+ state = state ^ (state >> 16u);
+ state = state * 2654435769u;
+ return state;
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(global_invocation_id) global_id : vec3) {
+ let id = global_id.x;
+
+ if (id >= arrayLength(&agents)) {
+ return;
+ }
+
+ var agent = agents[id];
+
+
+
+ var random = f32(hash(
+ u32(
+ agent.position.y * f32(settings.width) + agent.position.x
+ )
+ + hash(
+ id + u32(settings.time * 100000.)
+ )
+ )) / 4294967295.0;
+
+ // Steer based on sensory data
+ let sensorAngleRad : f32 = settings.sensorAngleDegrees * (3.1415 / 180.);
+ let weightForward : f32 = sense(agent, 0.);
+ let weightLeft : f32 = sense(agent, sensorAngleRad);
+ let weightRight : f32 = sense(agent, -sensorAngleRad);
+
+ let randomSteerStrength : f32 = random;
+ let turnSpeed : f32 = settings.turnSpeed * 2. * 3.1415;
+
+ // choose random direction
+ if (weightForward < weightLeft && weightForward < weightRight) {
+ agent.angle = agent.angle + (randomSteerStrength - 0.5) * 2. * turnSpeed * settings.deltaTime;
+ }
+ // Turn right
+ else if (weightRight > weightLeft) {
+ agent.angle = agent.angle - randomSteerStrength * turnSpeed * settings.deltaTime;
+ }
+ // Turn left
+ else if (weightLeft > weightRight) {
+ agent.angle = agent.angle + randomSteerStrength * turnSpeed * settings.deltaTime;
+ }
+
+ // Update position
+ let direction : vec2 = vec2(cos(agent.angle), sin(agent.angle));
+ var newPos : vec2 = agent.position + direction * settings.deltaTime * settings.moveSpeed;
+
+ // Clamp position to map boundaries, and pick new random move dir if hit boundary
+ if (newPos.x < 0. || newPos.x >= f32(settings.width) || newPos.y < 0. || newPos.y >= f32(settings.height)) {
+ // random = hash(random);
+ let randomAngle : f32 = random * 2. * 3.1415;
+
+ newPos.x = min(f32(settings.width - 1), max(0., newPos.x));
+ newPos.y = min(f32(settings.height - 1), max(0., newPos.y));
+ agent.angle = randomAngle;
+ } else {
+ let offset : i32 = i32() * settings.width * 4 + i32() * 4;
+ textureStore(TrailMapOut, vec2(i32(newPos.x), i32(newPos.y)), vec4(vec3(1.) * settings.trailWeight * settings.deltaTime, 1.));
+ }
+
+ agent.position = newPos;
+ agents[id] = agent;
+}
+
+fn sense(agent : Agent, sensorAngleOffset : f32) -> f32 {
+ let sensorAngle : f32 = agent.angle + sensorAngleOffset;
+ let sensorDir : vec2 = vec2(cos(sensorAngle), sin(sensorAngle));
+
+ let sensorPos : vec2 = agent.position + sensorDir * settings.sensorOffsetDst;
+ let sensorCentreX : i32 = i32(sensorPos.x);
+ let sensorCentreY : i32 = i32(sensorPos.y);
+
+ var sum : f32 = 0.;
+
+ let senseWeight : vec4 = vec4(2, 2, 2, 2) - vec4(1, 1, 1, 1);
+
+ let sensorSize : i32 = i32(settings.sensorSize);
+ for (var offsetX : i32 = -sensorSize; offsetX <= sensorSize; offsetX = offsetX + 1) {
+ for (var offsetY : i32 = -sensorSize; offsetY <= sensorSize; offsetY = offsetY + 1) {
+ let sampleX : i32 = min(settings.width - 1, max(0, sensorCentreX + offsetX));
+ let sampleY : i32 = min(settings.height - 1, max(0, sensorCentreY + offsetY));
+ sum = sum + dot(vec4(senseWeight), textureLoad(TrailMapIn, vec2(sampleX, sampleY), 1));
+ }
+ }
+
+ return sum;
+}
diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl
new file mode 100644
index 0000000..56813bd
--- /dev/null
+++ b/src/pipelines/diffusion/diffuse.wgsl
@@ -0,0 +1,34 @@
+struct VertexOutput {
+ @builtin(position) Position : vec4,
+ @location(0) fragUV : vec2,
+}
+
+@vertex
+fn vertex(@builtin(vertex_index) i : u32) -> VertexOutput {
+ var pos = array, 4>(
+ vec2(-1.0, 1.0),
+ vec2(-1.0, -1.0),
+ vec2(1.0, 1.0),
+ vec2(1.0, -1.0),
+ );
+
+ var output : VertexOutput;
+ output.Position = vec4(pos[i], 0.0, 1.0);
+ output.fragUV = output.Position.xy * 0.5 + 0.5;
+ return output;
+}
+
+
+@group(0) @binding(0) var mySampler: sampler;
+@group(0) @binding(1) var TargetTexture : texture_2d;
+
+
+@fragment
+fn fragment(@location(0) fragUV: vec2) -> @location(0) vec4 {
+ // return vec4(1.0, 0.0, 0.0, 1.0);
+ return mix(
+ vec4(textureSample(TargetTexture, mySampler, fragUV).rgb, 0.1),
+ vec4(1.0, 1.0, 1.0, 1.0),
+ 0.01
+ );
+}
diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts
new file mode 100644
index 0000000..ad4ff73
--- /dev/null
+++ b/src/pipelines/diffusion/diffusion-pipeline.ts
@@ -0,0 +1,81 @@
+import shader from './diffuse.wgsl';
+
+export class DiffusionPipeline {
+ private readonly pipeline: GPURenderPipeline;
+ private bindGroup?: GPUBindGroup;
+ private previousTrailMapIn?: GPUTexture;
+
+ public constructor(private readonly device: GPUDevice) {
+ this.pipeline = device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: device.createShaderModule({
+ code: shader,
+ }),
+ entryPoint: 'vertex',
+ },
+ fragment: {
+ module: device.createShaderModule({
+ code: shader,
+ }),
+ entryPoint: 'fragment',
+ targets: [
+ {
+ format: 'rgba16float',
+ },
+ ],
+ },
+ primitive: {
+ topology: 'triangle-strip',
+ },
+ });
+ }
+
+ public execute(
+ commandEncoder: GPUCommandEncoder,
+ trailMapIn: GPUTexture,
+ trailMapOut: GPUTexture
+ ) {
+ this.ensureBindGroupExists(trailMapIn);
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: trailMapOut.createView(),
+ clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const renderPassEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ renderPassEncoder.setBindGroup(0, this.bindGroup!);
+ renderPassEncoder.setPipeline(this.pipeline);
+ renderPassEncoder.draw(4, 1);
+ renderPassEncoder.end();
+ }
+
+ private ensureBindGroupExists(trailMapIn: GPUTexture) {
+ if (this.previousTrailMapIn !== trailMapIn) {
+ this.bindGroup = this.device.createBindGroup({
+ layout: this.pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: this.device.createSampler({
+ magFilter: 'linear',
+ minFilter: 'linear',
+ }),
+ },
+ {
+ binding: 1,
+ resource: trailMapIn.createView(),
+ },
+ ],
+ });
+
+ this.previousTrailMapIn = trailMapIn;
+ }
+ }
+}
diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts
new file mode 100644
index 0000000..0af5e9c
--- /dev/null
+++ b/src/pipelines/render/render-pipeline.ts
@@ -0,0 +1,80 @@
+import shader from './render.wgsl';
+
+export class RenderPipeline {
+ private readonly pipeline: GPURenderPipeline;
+ private bindGroup?: GPUBindGroup;
+ private previousColorTexture?: GPUTexture;
+
+ public constructor(
+ private readonly context: GPUCanvasContext,
+ private readonly device: GPUDevice,
+ preferredCanvasFormat: GPUTextureFormat
+ ) {
+ this.pipeline = device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: device.createShaderModule({
+ code: shader,
+ }),
+ entryPoint: 'vertex',
+ },
+ fragment: {
+ module: device.createShaderModule({
+ code: shader,
+ }),
+ entryPoint: 'fragment',
+ targets: [
+ {
+ format: preferredCanvasFormat,
+ },
+ ],
+ },
+ primitive: {
+ topology: 'triangle-strip',
+ },
+ });
+ }
+
+ public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) {
+ this.ensureBindGroupExists(colorTexture);
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: this.context.getCurrentTexture().createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+ const renderPassEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ renderPassEncoder.setBindGroup(0, this.bindGroup);
+ renderPassEncoder.setPipeline(this.pipeline);
+ renderPassEncoder.draw(4, 1);
+ renderPassEncoder.end();
+ }
+
+ private ensureBindGroupExists(colorTexture: GPUTexture) {
+ if (this.previousColorTexture !== colorTexture) {
+ this.bindGroup = this.device.createBindGroup({
+ layout: this.pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: this.device.createSampler({
+ magFilter: 'linear',
+ minFilter: 'linear',
+ }),
+ },
+ {
+ binding: 1,
+ resource: colorTexture.createView(),
+ },
+ ],
+ });
+
+ this.previousColorTexture = colorTexture;
+ }
+ }
+}
diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl
new file mode 100644
index 0000000..6c0be6e
--- /dev/null
+++ b/src/pipelines/render/render.wgsl
@@ -0,0 +1,30 @@
+struct VertexOutput {
+ @builtin(position) Position : vec4,
+ @location(0) fragUV : vec2,
+}
+
+@vertex
+fn vertex(@builtin(vertex_index) i : u32) -> VertexOutput {
+ var pos = array, 4>(
+ vec2(-1.0, 1.0),
+ vec2(-1.0, -1.0),
+ vec2(1.0, 1.0),
+ vec2(1.0, -1.0),
+ );
+
+ var output : VertexOutput;
+ output.Position = vec4(pos[i], 0.0, 1.0);
+ output.fragUV = output.Position.xy * 0.5 + 0.5;
+ return output;
+}
+
+
+@group(0) @binding(0) var mySampler: sampler;
+@group(0) @binding(1) var TargetTexture : texture_2d;
+
+
+@fragment
+fn fragment(@location(0) fragUV: vec2) -> @location(0) vec4 {
+// return vec4(1.0, 0.0, 0.0, 1.0);
+ return textureSample(TargetTexture, mySampler, fragUV);
+}
diff --git a/src/renderer.ts b/src/renderer.ts
index 9f8ff25..f6d0fd5 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -1,227 +1,124 @@
-import fragShaderCode from './shaders/triangle.frag.wgsl';
-import vertShaderCode from './shaders/triangle.vert.wgsl';
+import { Agent } from './pipelines/agents/agent';
+import { AgentPipeline } from './pipelines/agents/agent-pipeline';
+import { DiffusionPipeline } from './pipelines/diffusion/diffusion-pipeline';
+import { RenderPipeline } from './pipelines/render/render-pipeline';
+import { settings } from './settings';
+import { randomBetween } from './utils/random-between';
-const positions = new Float32Array([1.0, -1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 1.0, 0.0]);
-const colors = new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0]);
-
-const indices = new Uint16Array([0, 1, 2]);
+import { vec2 } from 'gl-matrix';
export default class Renderer {
- canvas: HTMLCanvasElement;
+ private context: GPUCanvasContext;
+ private adapter: GPUAdapter;
+ private device: GPUDevice;
+ private queue: GPUQueue;
- adapter: GPUAdapter;
- device: GPUDevice;
- queue: GPUQueue;
+ private agentPipeline: AgentPipeline;
+ private renderPipeline: RenderPipeline;
+ private diffusionPipeline: any;
- context: GPUCanvasContext;
- colorTexture: GPUTexture;
- colorTextureView: GPUTextureView;
- depthTexture: GPUTexture;
- depthTextureView: GPUTextureView;
+ private preferredCanvasFormat: GPUTextureFormat;
+ private trailMapA?: GPUTexture;
+ private trailMapB?: GPUTexture;
- positionBuffer: GPUBuffer;
- colorBuffer: GPUBuffer;
- indexBuffer: GPUBuffer;
- vertModule: GPUShaderModule;
- fragModule: GPUShaderModule;
- pipeline: GPURenderPipeline;
-
- commandEncoder: GPUCommandEncoder;
- passEncoder: GPURenderPassEncoder;
-
- constructor(canvas: HTMLCanvasElement) {
- this.canvas = canvas;
- }
+ public constructor(private canvas: HTMLCanvasElement) {}
async start() {
- if (await this.initializeAPI()) {
- this.resizeBackings();
- await this.initializeResources();
- this.render();
- }
+ await this.initialize();
+ requestAnimationFrame(this.render.bind(this));
}
- async initializeAPI(): Promise {
- try {
- const entry: GPU = navigator.gpu;
- if (!entry) {
- return false;
- }
+ private async initialize(): Promise {
+ await this.initializeDevice();
- this.adapter = await entry.requestAdapter();
- this.device = await this.adapter.requestDevice();
- this.queue = this.device.queue;
- } catch (e) {
- console.error(e);
- return false;
+ this.resize();
+ window.addEventListener('resize', this.resize.bind(this));
+
+ const agents: Array = new Array(settings.numAgents).fill(0).map(() => ({
+ position: vec2.fromValues(randomBetween(0, 500), randomBetween(0, 500)),
+ angle: randomBetween(0, Math.PI * 2),
+ }));
+
+ this.agentPipeline = new AgentPipeline(this.device, agents);
+ this.renderPipeline = new RenderPipeline(
+ this.context,
+ this.device,
+ this.preferredCanvasFormat
+ );
+ this.diffusionPipeline = new DiffusionPipeline(this.device);
+ }
+
+ private resize() {
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ this.canvas.width = this.canvas.clientWidth * devicePixelRatio;
+ this.canvas.height = this.canvas.clientHeight * devicePixelRatio;
+
+ this.trailMapA?.destroy();
+ this.trailMapA = this.device.createTexture({
+ size: {
+ width: this.canvas.width,
+ height: this.canvas.height,
+ depthOrArrayLayers: 1,
+ },
+ format: 'rgba16float',
+ usage:
+ GPUTextureUsage.STORAGE_BINDING |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ this.trailMapB?.destroy();
+ this.trailMapB = this.device.createTexture({
+ size: {
+ width: this.canvas.width,
+ height: this.canvas.height,
+ depthOrArrayLayers: 1,
+ },
+ format: 'rgba16float',
+ usage:
+ GPUTextureUsage.STORAGE_BINDING |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ }
+
+ private async initializeDevice(): Promise {
+ const gpu = navigator.gpu;
+ if (!gpu) {
+ throw new Error('WebGPU is not supported');
}
- return true;
+ this.adapter = await gpu.requestAdapter();
+ this.device = await this.adapter.requestDevice(); // could request more resources
+ this.queue = this.device.queue;
+
+ this.context = this.canvas.getContext('webgpu') as any;
+ this.preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat();
+ this.context.configure({
+ device: this.device,
+ format: this.preferredCanvasFormat,
+ alphaMode: 'premultiplied',
+ });
}
- async initializeResources() {
- const createBuffer = (arr: Float32Array | Uint16Array, usage: number) => {
- // 📏 Align to 4 bytes (thanks @chrimsonite)
- const desc = {
- size: (arr.byteLength + 3) & ~3,
- usage,
- mappedAtCreation: true,
- };
- const buffer = this.device.createBuffer(desc);
- const writeArray =
- arr instanceof Uint16Array
- ? new Uint16Array(buffer.getMappedRange())
- : new Float32Array(buffer.getMappedRange());
- writeArray.set(arr);
- buffer.unmap();
- return buffer;
- };
+ private render(time: DOMHighResTimeStamp) {
+ this.agentPipeline.setParameters({
+ width: this.canvas.width,
+ height: this.canvas.height,
+ time,
+ deltaTime: 0.016,
+ sensorAngleDegrees: 45,
+ ...settings,
+ });
+ const commandEncoder = this.device.createCommandEncoder();
- this.positionBuffer = createBuffer(positions, GPUBufferUsage.VERTEX);
- this.colorBuffer = createBuffer(colors, GPUBufferUsage.VERTEX);
- this.indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
+ this.agentPipeline.execute(commandEncoder, this.trailMapA, this.trailMapB);
+ this.diffusionPipeline.execute(commandEncoder, this.trailMapB, this.trailMapA);
+ this.renderPipeline.execute(commandEncoder, this.trailMapB);
+ [this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA];
- const vsmDesc = {
- code: vertShaderCode,
- };
- this.vertModule = this.device.createShaderModule(vsmDesc);
+ this.queue.submit([commandEncoder.finish()]);
- const fsmDesc = {
- code: fragShaderCode,
- };
- this.fragModule = this.device.createShaderModule(fsmDesc);
-
- const positionAttribDesc: GPUVertexAttribute = {
- shaderLocation: 0, // [[location(0)]]
- offset: 0,
- format: 'float32x3',
- };
- const colorAttribDesc: GPUVertexAttribute = {
- shaderLocation: 1, // [[location(1)]]
- offset: 0,
- format: 'float32x3',
- };
- const positionBufferDesc: GPUVertexBufferLayout = {
- attributes: [positionAttribDesc],
- arrayStride: 4 * 3, // sizeof(float) * 3
- stepMode: 'vertex',
- };
- const colorBufferDesc: GPUVertexBufferLayout = {
- attributes: [colorAttribDesc],
- arrayStride: 4 * 3, // sizeof(float) * 3
- stepMode: 'vertex',
- };
-
- const depthStencil: GPUDepthStencilState = {
- depthWriteEnabled: true,
- depthCompare: 'less',
- format: 'depth24plus-stencil8',
- };
-
- const pipelineLayoutDesc = { bindGroupLayouts: [] };
- const layout = this.device.createPipelineLayout(pipelineLayoutDesc);
-
- const vertex: GPUVertexState = {
- module: this.vertModule,
- entryPoint: 'main',
- buffers: [positionBufferDesc, colorBufferDesc],
- };
-
- const colorState: GPUColorTargetState = {
- format: 'bgra8unorm',
- };
-
- const fragment: GPUFragmentState = {
- module: this.fragModule,
- entryPoint: 'main',
- targets: [colorState],
- };
-
- const primitive: GPUPrimitiveState = {
- frontFace: 'cw',
- cullMode: 'none',
- topology: 'triangle-list',
- };
-
- const pipelineDesc: GPURenderPipelineDescriptor = {
- layout,
-
- vertex,
- fragment,
-
- primitive,
- depthStencil,
- };
- this.pipeline = this.device.createRenderPipeline(pipelineDesc);
+ requestAnimationFrame(this.render.bind(this));
}
-
- resizeBackings() {
- if (!this.context) {
- this.context = this.canvas.getContext('webgpu') as any;
- const canvasConfig: GPUCanvasConfiguration = {
- device: this.device,
- format: 'bgra8unorm',
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
- alphaMode: 'opaque',
- };
- this.context.configure(canvasConfig);
- }
-
- const depthTextureDesc: GPUTextureDescriptor = {
- size: [this.canvas.width, this.canvas.height, 1],
- dimension: '2d',
- format: 'depth24plus-stencil8',
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
- };
-
- this.depthTexture = this.device.createTexture(depthTextureDesc);
- this.depthTextureView = this.depthTexture.createView();
- }
-
- encodeCommands() {
- const colorAttachment: GPURenderPassColorAttachment = {
- view: this.colorTextureView,
- clearValue: { r: 0, g: 0, b: 0, a: 1 },
- loadOp: 'clear',
- storeOp: 'store',
- };
-
- const depthAttachment: GPURenderPassDepthStencilAttachment = {
- view: this.depthTextureView,
- depthClearValue: 1,
- depthLoadOp: 'clear',
- depthStoreOp: 'store',
- stencilClearValue: 0,
- stencilLoadOp: 'clear',
- stencilStoreOp: 'store',
- };
-
- const renderPassDesc: GPURenderPassDescriptor = {
- colorAttachments: [colorAttachment],
- depthStencilAttachment: depthAttachment,
- };
-
- this.commandEncoder = this.device.createCommandEncoder();
-
- this.passEncoder = this.commandEncoder.beginRenderPass(renderPassDesc);
- this.passEncoder.setPipeline(this.pipeline);
- this.passEncoder.setViewport(0, 0, this.canvas.width, this.canvas.height, 0, 1);
- this.passEncoder.setScissorRect(0, 0, this.canvas.width, this.canvas.height);
- this.passEncoder.setVertexBuffer(0, this.positionBuffer);
- this.passEncoder.setVertexBuffer(1, this.colorBuffer);
- this.passEncoder.setIndexBuffer(this.indexBuffer, 'uint16');
- this.passEncoder.drawIndexed(3, 1);
- this.passEncoder.end();
-
- this.queue.submit([this.commandEncoder.finish()]);
- }
-
- render = () => {
- this.colorTexture = this.context.getCurrentTexture();
- this.colorTextureView = this.colorTexture.createView();
-
- this.encodeCommands();
-
- requestAnimationFrame(this.render);
- };
}
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..cbe1eff
--- /dev/null
+++ b/src/settings.ts
@@ -0,0 +1,30 @@
+const SpawnMode = { Random: 0, Point: 1, InwardCircle: 2, RandomCircle: 3 };
+
+interface Settings {
+ stepsPerFrame: number;
+ numAgents: number;
+ spawnMode: number;
+ trailWeight: number;
+ decayRate: number;
+ diffuseRate: number;
+ moveSpeed: number;
+ turnSpeed: number;
+ sensorAngleSpacing: number;
+ sensorOffsetDst: number;
+ sensorSize: number;
+}
+
+export const settings: Settings = {
+ stepsPerFrame: 2,
+ numAgents: 250000,
+ spawnMode: SpawnMode.InwardCircle,
+ trailWeight: 5,
+ decayRate: 0.2,
+ diffuseRate: 3,
+
+ moveSpeed: 20,
+ turnSpeed: 2,
+ sensorAngleSpacing: 30,
+ sensorOffsetDst: 35,
+ sensorSize: 1,
+};
diff --git a/src/shaders/triangle.frag.wgsl b/src/shaders/triangle.frag.wgsl
deleted file mode 100644
index d975e3b..0000000
--- a/src/shaders/triangle.frag.wgsl
+++ /dev/null
@@ -1,4 +0,0 @@
-@fragment
-fn main(@location(0) inColor: vec3) -> @location(0) vec4 {
- return vec4(inColor, 1.0);
-}
\ No newline at end of file
diff --git a/src/shaders/triangle.vert.wgsl b/src/shaders/triangle.vert.wgsl
deleted file mode 100644
index c899b01..0000000
--- a/src/shaders/triangle.vert.wgsl
+++ /dev/null
@@ -1,13 +0,0 @@
-struct VSOut {
- @builtin(position) Position: vec4,
- @location(0) color: vec3,
- };
-
-@vertex
-fn main(@location(0) inPos: vec3,
- @location(1) inColor: vec3) -> VSOut {
- var vsOut: VSOut;
- vsOut.Position = vec4(inPos, 1.0);
- vsOut.color = inColor;
- return vsOut;
-}
\ No newline at end of file
diff --git a/src/utils/mulberry32.ts b/src/utils/mulberry32.ts
new file mode 100644
index 0000000..04d054d
--- /dev/null
+++ b/src/utils/mulberry32.ts
@@ -0,0 +1,8 @@
+export const mulberry32 = (seed) => () => {
+ let t = (seed += 0x6d2b79f5);
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+};
+
+Math.random = mulberry32(123);
diff --git a/src/utils/random-between.ts b/src/utils/random-between.ts
new file mode 100644
index 0000000..dd547a5
--- /dev/null
+++ b/src/utils/random-between.ts
@@ -0,0 +1,3 @@
+export const randomBetween = (min: number, max: number) => {
+ return Math.random() * (max - min) + min;
+};
diff --git a/webpack.config.js b/webpack.config.js
index 594436b..55462d4 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -42,7 +42,7 @@ module.exports = (env, argv) => ({
use: 'svg-inline-loader',
},
{
- test: /\.wgsl/,
+ test: /\.wgsl$/i,
type: 'asset/source',
generator: {
filename: '[name][ext]',