From 5cc94805f1a2def62de7790fae54f5b0af1d7457 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 15 Apr 2023 21:57:33 +0100 Subject: [PATCH] Copy from https://playground.babylonjs.com/#GXJ3FZ#51 --- src/index.html | 1 - src/index.scss | 36 +- src/index.ts | 1 + src/pipelines/agents/agent-pipeline.ts | 140 ++++++++ src/pipelines/agents/agent.ts | 8 + src/pipelines/agents/agent.wgsl | 125 +++++++ src/pipelines/diffusion/diffuse.wgsl | 34 ++ src/pipelines/diffusion/diffusion-pipeline.ts | 81 +++++ src/pipelines/render/render-pipeline.ts | 80 +++++ src/pipelines/render/render.wgsl | 30 ++ src/renderer.ts | 311 ++++++------------ src/settings.ts | 30 ++ src/shaders/triangle.frag.wgsl | 4 - src/shaders/triangle.vert.wgsl | 13 - src/utils/mulberry32.ts | 8 + src/utils/random-between.ts | 3 + webpack.config.js | 2 +- 17 files changed, 670 insertions(+), 237 deletions(-) create mode 100644 src/pipelines/agents/agent-pipeline.ts create mode 100644 src/pipelines/agents/agent.ts create mode 100644 src/pipelines/agents/agent.wgsl create mode 100644 src/pipelines/diffusion/diffuse.wgsl create mode 100644 src/pipelines/diffusion/diffusion-pipeline.ts create mode 100644 src/pipelines/render/render-pipeline.ts create mode 100644 src/pipelines/render/render.wgsl create mode 100644 src/settings.ts delete mode 100644 src/shaders/triangle.frag.wgsl delete mode 100644 src/shaders/triangle.vert.wgsl create mode 100644 src/utils/mulberry32.ts create mode 100644 src/utils/random-between.ts 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]',