diff --git a/src/pipelines/swipe/swipe-pipeline.ts b/src/pipelines/swipe/swipe-pipeline.ts new file mode 100644 index 0000000..5a14406 --- /dev/null +++ b/src/pipelines/swipe/swipe-pipeline.ts @@ -0,0 +1,126 @@ +import { setUpFullScreenQuad } from '../../utils/full-screen-quad'; +import shader from './swipe.wgsl'; + +import { vec2 } from 'gl-matrix'; + +export class SwipePipeline { + private static readonly UNIFORM_COUNT = 8; + + private readonly pipeline: GPURenderPipeline; + private readonly uniforms: GPUBuffer; + private readonly quadVertexBuffer: GPUBuffer; + + private bindGroup?: GPUBindGroup; + private previousTrailMapIn?: GPUTexture; + + public constructor(private readonly device: GPUDevice) { + const { buffer, vertex } = setUpFullScreenQuad(device); + this.quadVertexBuffer = buffer; + + this.pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex, + fragment: { + module: device.createShaderModule({ + code: shader, + }), + entryPoint: 'fragment', + targets: [ + { + format: 'rgba16float', + }, + ], + }, + primitive: { + topology: 'triangle-strip', + }, + }); + + this.uniforms = this.device.createBuffer({ + size: SwipePipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + } + + public setParameters({ + width, + height, + isSwipeActive, + swipe, + swipeRadius, + }: { + width: number; + height: number; + isSwipeActive: boolean; + swipe: vec2; + swipeRadius: number; + }) { + this.device.queue.writeBuffer( + this.uniforms, + 0, + new Float32Array([ + width, + height, + swipe ? swipe[0] : 0, + swipe ? swipe[1] : 0, + swipeRadius, + isSwipeActive ? 1 : 0, + ]) + ); + } + + 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 passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(this.pipeline); + passEncoder.setVertexBuffer(0, this.quadVertexBuffer); + passEncoder.setBindGroup(0, this.bindGroup); + passEncoder.draw(4, 1); + passEncoder.end(); + } + + private ensureBindGroupExists(trailMapIn: GPUTexture) { + if (this.previousTrailMapIn !== trailMapIn) { + this.bindGroup = this.device.createBindGroup({ + layout: this.pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: this.uniforms, + }, + }, + { + binding: 1, + resource: this.device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + }), + }, + { + binding: 2, + resource: trailMapIn.createView(), + }, + ], + }); + + this.previousTrailMapIn = trailMapIn; + } + } +} diff --git a/src/pipelines/swipe/swipe.wgsl b/src/pipelines/swipe/swipe.wgsl new file mode 100644 index 0000000..347bd98 --- /dev/null +++ b/src/pipelines/swipe/swipe.wgsl @@ -0,0 +1,24 @@ +struct Settings { + size : vec2, + swipe : vec2, + swipeRadius : f32, + isSwipeActive : f32, +}; + +@group(0) @binding(0) var settings : Settings; +@group(0) @binding(1) var Sampler: sampler; +@group(0) @binding(2) var trailMap : texture_2d; + +@fragment +fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { + var current = textureSample(trailMap, Sampler, uv); + + if ( + settings.isSwipeActive == 1.0 && + length((uv - settings.swipe) * normalize(settings.size)) < settings.swipeRadius + ) { + current = vec4(1); + } + + return current; +} diff --git a/src/renderer.ts b/src/renderer.ts index 7ca3f49..412015a 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -2,6 +2,7 @@ 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 { SwipePipeline } from './pipelines/swipe/swipe-pipeline'; import { settings } from './settings'; import { randomBetween } from './utils/random-between'; @@ -16,26 +17,49 @@ export default class Renderer { private agentPipeline: AgentPipeline; private renderPipeline: RenderPipeline; private diffusionPipeline: DiffusionPipeline; + private swipePipeline: SwipePipeline; private preferredCanvasFormat: GPUTextureFormat; private trailMapA?: GPUTexture; private trailMapB?: GPUTexture; private previousTime?: DOMHighResTimeStamp = null; + private swipeLocation?: vec2; + private isSwipeActive = false; public constructor(private canvas: HTMLCanvasElement) {} async start() { - await this.initialize(); - requestAnimationFrame(this.render.bind(this)); - } - - private async initialize(): Promise { await this.initializeDevice(); this.resize(); window.addEventListener('resize', this.resize.bind(this)); + window.addEventListener('mousemove', this.onSwipe.bind(this)); + window.addEventListener('mousedown', (_) => (this.isSwipeActive = true)); + window.addEventListener('mouseup', (_) => (this.isSwipeActive = false)); + requestAnimationFrame(this.render.bind(this)); + + this.agentPipeline = new AgentPipeline(this.device, this.spawnAgents()); + this.renderPipeline = new RenderPipeline( + this.context, + this.device, + this.preferredCanvasFormat + ); + this.swipePipeline = new SwipePipeline(this.device); + this.diffusionPipeline = new DiffusionPipeline(this.device); + } + + private onSwipe(event: MouseEvent) { + const position = vec2.fromValues(event.clientX, event.clientY); + this.swipeLocation = vec2.divide( + position, + position, + vec2.fromValues(this.canvas.width, this.canvas.height) + ); + } + + private spawnAgents(): Array { const minSize = Math.min(this.canvas.width, this.canvas.height); const ratio = Math.max(this.canvas.width, this.canvas.height) / minSize; const size = vec2.fromValues( @@ -43,8 +67,7 @@ export default class Renderer { this.canvas.height / minSize ); vec2.normalize(size, size); - console.log(size); - const agents: Array = new Array(settings.agentCount).fill(0).map(() => { + return new Array(settings.agentCount).fill(0).map(() => { const radius = randomBetween(0, settings.startingRadius / ratio); const angle = randomBetween(0, Math.PI * 2); const center = vec2.fromValues(0.5, 0.5); @@ -59,14 +82,6 @@ export default class Renderer { angle: angle + Math.PI, }; }); - - 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() { @@ -125,6 +140,13 @@ export default class Renderer { deltaTime, ...settings, }); + this.swipePipeline.setParameters({ + width: this.canvas.width, + height: this.canvas.height, + isSwipeActive: this.isSwipeActive, + swipe: this.swipeLocation, + ...settings, + }); this.diffusionPipeline.setParameters({ width: this.canvas.width, height: this.canvas.height, @@ -136,8 +158,13 @@ export default class Renderer { for (let i = 0; i < settings.renderSpeed; i++) { this.agentPipeline.execute(commandEncoder, this.trailMapA, this.trailMapB); this.diffusionPipeline.execute(commandEncoder, this.trailMapB, this.trailMapA); - this.renderPipeline.execute(commandEncoder, this.trailMapA); - [this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA]; + if (this.isSwipeActive) { + this.swipePipeline.execute(commandEncoder, this.trailMapA, this.trailMapB); + this.renderPipeline.execute(commandEncoder, this.trailMapB); + } else { + this.renderPipeline.execute(commandEncoder, this.trailMapA); + [this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA]; + } } this.queue.submit([commandEncoder.finish()]); diff --git a/src/settings.ts b/src/settings.ts index 97396d1..e593341 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -9,19 +9,21 @@ interface Settings { turnSpeed: number; sensorAngleDegrees: number; sensorOffsetDst: number; + swipeRadius: number; } export const settings: Settings = { agentCount: 1_000_000, - renderSpeed: 2, + renderSpeed: 1, startingRadius: 0.15, trailWeight: 5, - decayRate: 0.05, - diffusionRate: 0.3, + decayRate: 0.02, + diffusionRate: 0.8, moveSpeed: 0.025, turnSpeed: 6, sensorAngleDegrees: 30, sensorOffsetDst: 0.025, + swipeRadius: 0.005, };