From de7fcc15d0a3a7ce5a168d0a4675b72441690556 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 27 Apr 2023 20:55:48 +0100 Subject: [PATCH] Improve drawing --- src/pipelines/brush/brush-pipeline.ts | 143 ++++++++++++++++++ src/pipelines/brush/brush.wgsl | 23 +++ src/pipelines/diffusion/diffuse.wgsl | 15 -- src/pipelines/diffusion/diffusion-pipeline.ts | 18 +-- src/renderer.ts | 53 ++++--- src/utils/delta-time-calculator.ts | 25 +++ 6 files changed, 221 insertions(+), 56 deletions(-) create mode 100644 src/pipelines/brush/brush-pipeline.ts create mode 100644 src/pipelines/brush/brush.wgsl create mode 100644 src/utils/delta-time-calculator.ts diff --git a/src/pipelines/brush/brush-pipeline.ts b/src/pipelines/brush/brush-pipeline.ts new file mode 100644 index 0000000..826d101 --- /dev/null +++ b/src/pipelines/brush/brush-pipeline.ts @@ -0,0 +1,143 @@ +import shader from './brush.wgsl'; + +import { vec2 } from 'gl-matrix'; + +export class BrushPipeline { + private static readonly UNIFORM_COUNT = 2; + private static readonly MAX_LINE_COUNT = 100; + private static readonly VERTICES_PER_LINE_SEGMENT = 6; + + private readonly pipeline: GPURenderPipeline; + private readonly uniforms: GPUBuffer; + private readonly vertexBuffer: GPUBuffer; + private readonly linePoints: Array = []; + private bindGroup: GPUBindGroup; + + public constructor(private readonly device: GPUDevice) { + this.vertexBuffer = device.createBuffer({ + size: + BrushPipeline.MAX_LINE_COUNT * + BrushPipeline.VERTICES_PER_LINE_SEGMENT * + 2 * + Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }); + + this.pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: shader, + }), + entryPoint: 'vertex', + buffers: [ + { + arrayStride: Float32Array.BYTES_PER_ELEMENT * 2, + attributes: [ + { + shaderLocation: 0, + format: 'float32x2', + offset: 0, + }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: shader, + }), + entryPoint: 'fragment', + targets: [ + { + format: 'rgba16float', + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + }); + + this.uniforms = this.device.createBuffer({ + size: BrushPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + this.bindGroup = this.device.createBindGroup({ + layout: this.pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: this.uniforms, + }, + }, + ], + }); + } + + public addSwipe(position: vec2) { + this.linePoints.push(position); + } + + public clearSwipes() { + this.linePoints.length = 0; + } + + public setParameters({ width, height }: { width: number; height: number }) { + this.device.queue.writeBuffer(this.uniforms, 0, new Float32Array([width, height])); + + this.device.queue.writeBuffer( + this.vertexBuffer, + 0, + new Float32Array( + new Array(this.lineCount).fill(0).flatMap((_, i) => { + const from = this.linePoints[i]; + const to = this.linePoints[i + 1]; + const [a, b, c, d] = this.lineToRectangle(from, to, 0.01); + return [...a, ...b, ...c, ...b, ...c, ...d]; + }) + ) + ); + } + + private get lineCount() { + return Math.max(0, this.linePoints.length - 1); + } + + private lineToRectangle(from: vec2, to: vec2, width: number): [vec2, vec2, vec2, vec2] { + const dir = vec2.sub(vec2.create(), to, from); + const perp = vec2.fromValues(dir[1], -dir[0]); + vec2.normalize(perp, perp); + vec2.scale(perp, perp, width / 2); + return [ + vec2.add(vec2.create(), from, perp), + vec2.sub(vec2.create(), from, perp), + vec2.add(vec2.create(), to, perp), + vec2.sub(vec2.create(), to, perp), + ]; + } + + public execute(commandEncoder: GPUCommandEncoder, trailMapOut: GPUTexture) { + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: trailMapOut.createView(), + clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, + loadOp: 'load', + storeOp: 'store', + }, + ], + }; + + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(this.pipeline); + passEncoder.setBindGroup(0, this.bindGroup); + passEncoder.setVertexBuffer(0, this.vertexBuffer); + passEncoder.draw(this.lineCount * BrushPipeline.VERTICES_PER_LINE_SEGMENT, 1); + passEncoder.end(); + + this.linePoints.splice(0, this.linePoints.length - 1); // clear the array + } +} diff --git a/src/pipelines/brush/brush.wgsl b/src/pipelines/brush/brush.wgsl new file mode 100644 index 0000000..4bc9437 --- /dev/null +++ b/src/pipelines/brush/brush.wgsl @@ -0,0 +1,23 @@ +struct VertexOutput { + @builtin(position) position : vec4, + @location(0) uv : vec2 +} + +@vertex +fn vertex( + @location(0) uv : vec2 +) -> VertexOutput { + let position = uv * 2.0 - 1.0; + return VertexOutput(vec4(position, 0.0, 1.0), uv); +} + +struct Settings { + size : vec2 +}; + +@group(0) @binding(0) var settings : Settings; + +@fragment +fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(1, settings.size.x * 0, 0, 1); +} diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 334472d..e010435 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -1,14 +1,11 @@ struct Settings { size : vec2, - swipePrevious : vec2, - swipeCurrent : vec2, diffusionRate : f32, decayRate : f32, deltaTime : f32, time : f32, swipeRadius : f32, swipeBlur : f32, - isSwipeActive : f32 }; @group(0) @binding(0) var settings : Settings; @@ -26,18 +23,6 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { + textureSample(trailMap, Sampler, uv + vec2(1, 0) / settings.size) ); - if (settings.isSwipeActive == 1.0) { - let pa = (uv - settings.swipePrevious) * normalize(settings.size); - let direction = (settings.swipeCurrent - settings.swipePrevious) * normalize(settings.size); - let q = clamp(dot(pa, direction) / dot(direction, direction), 0, 1); - let distance = length(pa - direction * q) - settings.swipeRadius; - - if(distance < 0) { - let opacity = -distance / settings.swipeBlur; - return clamp(vec4(1), current, vec4(1)); - } - } - return mix( current, neighbours / 4.0, diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index a491bea..eb26a76 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -1,19 +1,13 @@ import { setUpFullScreenQuad } from '../../utils/full-screen-quad'; import shader from './diffuse.wgsl'; -import { vec2 } from 'gl-matrix'; - export class DiffusionPipeline { - private static readonly UNIFORM_COUNT = 14; + private static readonly UNIFORM_COUNT = 16; private readonly pipeline: GPURenderPipeline; private readonly uniforms: GPUBuffer; private readonly quadVertexBuffer: GPUBuffer; - private swipes: Array = [ - vec2.fromValues(Number.NaN, Number.NaN), - vec2.fromValues(Number.NaN, Number.NaN), - ]; private bindGroup?: GPUBindGroup; private previousTrailMapIn?: GPUTexture; @@ -53,40 +47,30 @@ export class DiffusionPipeline { decayRate, deltaTime, time, - swipe, swipeRadius, swipeBlur, - isSwipeActive, }: { width: number; height: number; - swipe: vec2; diffusionRate: number; decayRate: number; deltaTime: number; time: number; swipeRadius: number; swipeBlur: number; - isSwipeActive: boolean; }) { - if (swipe) { - this.swipes = [...this.swipes.slice(-1), swipe]; - } - this.device.queue.writeBuffer( this.uniforms, 0, new Float32Array([ width, height, - ...this.swipes.flatMap((s) => [s[0], s[1]]), diffusionRate, decayRate, deltaTime, time, swipeRadius, swipeBlur, - isSwipeActive ? 1.0 : 0.0, ]) ); } diff --git a/src/renderer.ts b/src/renderer.ts index 4d3b523..c0ee7ce 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,8 +1,10 @@ import { Agent } from './pipelines/agents/agent'; import { AgentPipeline } from './pipelines/agents/agent-pipeline'; +import { BrushPipeline } from './pipelines/brush/brush-pipeline'; import { DiffusionPipeline } from './pipelines/diffusion/diffusion-pipeline'; import { RenderPipeline } from './pipelines/render/render-pipeline'; import { settings } from './settings'; +import { DeltaTimeCalculator } from './utils/delta-time-calculator'; import { randomBetween } from './utils/random-between'; import { sleep } from './utils/sleep'; @@ -16,15 +18,15 @@ export default class Renderer { private agentPipeline: AgentPipeline; private renderPipeline: RenderPipeline; + private brushPipeline: BrushPipeline; private diffusionPipeline: DiffusionPipeline; private preferredCanvasFormat: GPUTextureFormat; private trailMapA?: GPUTexture; private trailMapB?: GPUTexture; - private previousTime?: DOMHighResTimeStamp = null; - private swipeLocation?: vec2; private isSwipeActive = false; + private readonly deltaTimeCalculator = new DeltaTimeCalculator(); public constructor(private canvas: HTMLCanvasElement) {} @@ -32,10 +34,6 @@ export default class Renderer { 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)); this.agentPipeline = new AgentPipeline(this.device, this.spawnAgents()); this.renderPipeline = new RenderPipeline( @@ -43,18 +41,31 @@ export default class Renderer { this.device, this.preferredCanvasFormat ); + this.brushPipeline = new BrushPipeline(this.device); this.diffusionPipeline = new DiffusionPipeline(this.device); + 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; + this.brushPipeline.clearSwipes(); + }); + requestAnimationFrame(this.render.bind(this)); } 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) + if (!this.isSwipeActive) { + return; + } + + const uv = vec2.fromValues( + event.clientX / this.canvas.width, + 1 - event.clientY / this.canvas.height ); + + this.brushPipeline.addSwipe(uv); } private spawnAgents(): Array { @@ -129,7 +140,7 @@ export default class Renderer { } private async render(time: DOMHighResTimeStamp) { - const deltaTime = this.calculateDeltaTime(time); + const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time); this.agentPipeline.setParameters({ ...settings, @@ -138,19 +149,22 @@ export default class Renderer { time, deltaTime, }); + this.brushPipeline.setParameters({ + width: this.canvas.width, + height: this.canvas.height, + }); this.diffusionPipeline.setParameters({ ...settings, width: this.canvas.width, height: this.canvas.height, deltaTime, time, - isSwipeActive: this.isSwipeActive, - swipe: this.swipeLocation, }); const commandEncoder = this.device.createCommandEncoder(); for (let i = 0; i < settings.renderSpeed; i++) { this.agentPipeline.execute(commandEncoder, this.trailMapA, this.trailMapB); + this.brushPipeline.execute(commandEncoder, this.trailMapB); this.diffusionPipeline.execute(commandEncoder, this.trailMapB, this.trailMapA); this.renderPipeline.execute(commandEncoder, this.trailMapA); [this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA]; @@ -158,16 +172,7 @@ export default class Renderer { this.queue.submit([commandEncoder.finish()]); - // await sleep(1000); + await sleep(200); requestAnimationFrame(this.render.bind(this)); } - - private calculateDeltaTime(time: DOMHighResTimeStamp): number { - if (this.previousTime === null) { - this.previousTime = time; - } - const deltaTime = time - this.previousTime; - this.previousTime = time; - return deltaTime / 1000; - } } diff --git a/src/utils/delta-time-calculator.ts b/src/utils/delta-time-calculator.ts new file mode 100644 index 0000000..9feb969 --- /dev/null +++ b/src/utils/delta-time-calculator.ts @@ -0,0 +1,25 @@ +export class DeltaTimeCalculator { + private previousTime: DOMHighResTimeStamp | null = null; + + constructor() { + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); + } + + public calculateDeltaTimeInSeconds( + currentTime: DOMHighResTimeStamp + ): DOMHighResTimeStamp { + if (this.previousTime === null) { + this.previousTime = currentTime; + } + + const delta = currentTime - this.previousTime; + this.previousTime = currentTime; + return delta / 1000; + } + + private handleVisibilityChange() { + if (!document.hidden) { + this.previousTime = null; + } + } +}