From 9e582110ea4969b82b4a4673de137dda723516a8 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 29 Apr 2023 11:58:14 +0100 Subject: [PATCH] Refactor --- README.md | 19 +------ src/game-loop/game-loop-settings.ts | 5 ++ src/{renderer.ts => game-loop/game-loop.ts} | 49 +++++++++---------- src/index.ts | 5 +- src/pipelines/agents/agent-pipeline.ts | 26 ++++------ src/pipelines/agents/agent-settings.ts | 7 +++ src/pipelines/agents/agent.wgsl | 4 +- src/pipelines/brush/brush-pipeline.ts | 16 ++++-- src/pipelines/brush/brush-settings.ts | 1 + src/pipelines/brush/brush.wgsl | 4 +- src/pipelines/common-parameters.ts | 7 +++ src/pipelines/diffusion/diffuse.wgsl | 5 +- src/pipelines/diffusion/diffusion-pipeline.ts | 28 ++++------- src/pipelines/diffusion/diffusion-settings.ts | 6 +++ src/pipelines/render/render-pipeline.ts | 30 +++++++++++- src/pipelines/render/render-settings.ts | 1 + src/pipelines/render/render.wgsl | 12 +++-- src/settings.ts | 31 +++++------- src/{ => styles}/index.scss | 0 src/styles/mixins.scss | 25 ++++++++++ src/utils/clamp.ts | 4 ++ src/utils/handle-full-screen.ts | 48 ++++++++++++++++++ src/utils/last.ts | 3 ++ src/utils/mulberry32.ts | 8 --- src/utils/pretty-print.ts | 4 ++ src/utils/random-between.ts | 3 -- src/utils/random.ts | 20 ++++++++ 27 files changed, 248 insertions(+), 123 deletions(-) create mode 100644 src/game-loop/game-loop-settings.ts rename src/{renderer.ts => game-loop/game-loop.ts} (81%) create mode 100644 src/pipelines/agents/agent-settings.ts create mode 100644 src/pipelines/brush/brush-settings.ts create mode 100644 src/pipelines/common-parameters.ts create mode 100644 src/pipelines/diffusion/diffusion-settings.ts create mode 100644 src/pipelines/render/render-settings.ts rename src/{ => styles}/index.scss (100%) create mode 100644 src/styles/mixins.scss create mode 100644 src/utils/clamp.ts create mode 100644 src/utils/handle-full-screen.ts create mode 100644 src/utils/last.ts delete mode 100644 src/utils/mulberry32.ts create mode 100644 src/utils/pretty-print.ts delete mode 100644 src/utils/random-between.ts create mode 100644 src/utils/random.ts diff --git a/README.md b/README.md index a315777..59ba64c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,3 @@ -# 🔺 WebGPU Seed +## todo -[![License][license-img]][license-url] - -A WebGPU repo you can use to get started with your own renderer. - -- [🔳 Codepen Example](https://codepen.io/alaingalvan/pen/GRgvLGw) - -- [💬 Blog Post](https://alain.xyz/blog/raw-webgpu) - -## Setup - -> Refer to [this blog post on designing web libraries and apps](https://alain.xyz/blog/designing-a-web-app) for more details on Node.js, packages, etc. - -As your project becomes more complex, you'll want to separate files and organize your application to something more akin to a game or renderer, check out this post on [game engine architecture](https://alain.xyz/blog/game-engine-architecture) and this one on [real time renderer architecture](https://alain.xyz/blog/realtime-renderer-architectures) for more details. - -[license-img]: https://img.shields.io/:license-unlicense-blue.svg?style=flat-square -[license-url]: https://unlicense.org/ +- diff --git a/src/game-loop/game-loop-settings.ts b/src/game-loop/game-loop-settings.ts new file mode 100644 index 0000000..e002067 --- /dev/null +++ b/src/game-loop/game-loop-settings.ts @@ -0,0 +1,5 @@ +export interface GameLoopSettings { + agentCount: number; + renderSpeed: number; + startingRadius: number; +} diff --git a/src/renderer.ts b/src/game-loop/game-loop.ts similarity index 81% rename from src/renderer.ts rename to src/game-loop/game-loop.ts index c0ee7ce..93c40e2 100644 --- a/src/renderer.ts +++ b/src/game-loop/game-loop.ts @@ -1,12 +1,11 @@ -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'; +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 { Random } from '../utils/random'; import { vec2 } from 'gl-matrix'; @@ -77,8 +76,8 @@ export default class Renderer { ); vec2.normalize(size, size); return new Array(settings.agentCount).fill(0).map(() => { - const radius = randomBetween(0, settings.startingRadius / ratio); - const angle = randomBetween(0, Math.PI * 2); + const radius = Random.randomBetween(0, settings.startingRadius / ratio); + const angle = Random.randomBetween(0, Math.PI * 2); const center = vec2.fromValues(0.5, 0.5); const delta = vec2.fromValues(Math.cos(angle) * radius, Math.sin(angle) * radius); @@ -142,24 +141,20 @@ export default class Renderer { private async render(time: DOMHighResTimeStamp) { const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time); - this.agentPipeline.setParameters({ - ...settings, - width: this.canvas.width, - height: this.canvas.height, + const params = { + canvasSize: vec2.fromValues(this.canvas.width, this.canvas.height), 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, - }); + }; + + [ + this.agentPipeline, + this.brushPipeline, + this.diffusionPipeline, + this.renderPipeline, + ].forEach((pipeline) => pipeline.setParameters(params)); + const commandEncoder = this.device.createCommandEncoder(); for (let i = 0; i < settings.renderSpeed; i++) { @@ -172,7 +167,7 @@ export default class Renderer { this.queue.submit([commandEncoder.finish()]); - await sleep(200); + // await sleep(200); requestAnimationFrame(this.render.bind(this)); } } diff --git a/src/index.ts b/src/index.ts index 97d4c27..86ebfb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ -import './index.scss'; -import Renderer from './renderer'; -import './utils/mulberry32'; +import Renderer from './game-loop/game-loop'; +import './styles/index.scss'; const main = () => { const canvas = document.querySelector('canvas'); diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index 525d170..de863ce 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -1,4 +1,6 @@ +import { CommonParameters } from '../common-parameters'; import { AGENT_SIZE_IN_BYTES, Agent } from './agent'; +import { AgentSettings } from './agent-settings'; import shader from './agent.wgsl'; export class AgentPipeline { @@ -54,34 +56,24 @@ export class AgentPipeline { } public setParameters({ - width, - height, - trailWeight, + canvasSize, deltaTime, time, + trailWeight, moveSpeed, turnSpeed, sensorAngleDegrees, sensorOffsetDst, - }: { - width: number; - height: number; - trailWeight: number; - deltaTime: number; - time: number; - moveSpeed: number; - turnSpeed: number; - sensorAngleDegrees: number; - sensorOffsetDst: number; - }) { + }: CommonParameters & AgentSettings) { this.device.queue.writeBuffer( this.uniforms, 0, new Float32Array([ - width, - height, - trailWeight, + canvasSize[0], + canvasSize[1], + deltaTime, time, + trailWeight, moveSpeed * deltaTime, turnSpeed * deltaTime, (sensorAngleDegrees * Math.PI) / 180, diff --git a/src/pipelines/agents/agent-settings.ts b/src/pipelines/agents/agent-settings.ts new file mode 100644 index 0000000..c60587e --- /dev/null +++ b/src/pipelines/agents/agent-settings.ts @@ -0,0 +1,7 @@ +export interface AgentSettings { + trailWeight: number; + moveSpeed: number; + turnSpeed: number; + sensorAngleDegrees: number; + sensorOffsetDst: number; +} diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 6c136d3..e547a67 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -5,12 +5,12 @@ struct Agent { struct Settings { size: vec2, - trailWeight : f32, + deltaTime : f32, time : f32, + trailWeight : f32, moveRate : f32, turnRate : f32, - sensorAngle : f32, sensorOffsetDst : f32, }; diff --git a/src/pipelines/brush/brush-pipeline.ts b/src/pipelines/brush/brush-pipeline.ts index 826d101..1fe9105 100644 --- a/src/pipelines/brush/brush-pipeline.ts +++ b/src/pipelines/brush/brush-pipeline.ts @@ -1,9 +1,11 @@ +import { CommonParameters } from '../common-parameters'; +import { BrushSettings } from './brush-settings'; import shader from './brush.wgsl'; import { vec2 } from 'gl-matrix'; export class BrushPipeline { - private static readonly UNIFORM_COUNT = 2; + private static readonly UNIFORM_COUNT = 4; private static readonly MAX_LINE_COUNT = 100; private static readonly VERTICES_PER_LINE_SEGMENT = 6; @@ -85,8 +87,16 @@ export class BrushPipeline { this.linePoints.length = 0; } - public setParameters({ width, height }: { width: number; height: number }) { - this.device.queue.writeBuffer(this.uniforms, 0, new Float32Array([width, height])); + public setParameters({ + canvasSize, + deltaTime, + time, + }: CommonParameters & BrushSettings) { + this.device.queue.writeBuffer( + this.uniforms, + 0, + new Float32Array([canvasSize[0], canvasSize[1], deltaTime, time]) + ); this.device.queue.writeBuffer( this.vertexBuffer, diff --git a/src/pipelines/brush/brush-settings.ts b/src/pipelines/brush/brush-settings.ts new file mode 100644 index 0000000..4c10a34 --- /dev/null +++ b/src/pipelines/brush/brush-settings.ts @@ -0,0 +1 @@ +export interface BrushSettings {} diff --git a/src/pipelines/brush/brush.wgsl b/src/pipelines/brush/brush.wgsl index 4bc9437..8bf1731 100644 --- a/src/pipelines/brush/brush.wgsl +++ b/src/pipelines/brush/brush.wgsl @@ -12,7 +12,9 @@ fn vertex( } struct Settings { - size : vec2 + size : vec2, + deltaTime : f32, + time : f32 }; @group(0) @binding(0) var settings : Settings; diff --git a/src/pipelines/common-parameters.ts b/src/pipelines/common-parameters.ts new file mode 100644 index 0000000..f30bc66 --- /dev/null +++ b/src/pipelines/common-parameters.ts @@ -0,0 +1,7 @@ +import { vec2 } from 'gl-matrix'; + +export interface CommonParameters { + canvasSize: vec2; + deltaTime: number; + time: number; +} diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index e010435..802886b 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -1,9 +1,10 @@ struct Settings { size : vec2, - diffusionRate : f32, - decayRate : f32, deltaTime : f32, time : f32, + + diffusionRate : f32, + decayRate : f32, swipeRadius : f32, swipeBlur : f32, }; diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index eb26a76..53f1f4e 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -1,5 +1,7 @@ import { setUpFullScreenQuad } from '../../utils/full-screen-quad'; +import { CommonParameters } from '../common-parameters'; import shader from './diffuse.wgsl'; +import { DiffusionSettings } from './diffusion-settings'; export class DiffusionPipeline { private static readonly UNIFORM_COUNT = 16; @@ -41,34 +43,24 @@ export class DiffusionPipeline { } public setParameters({ - width, - height, - diffusionRate, - decayRate, + canvasSize, deltaTime, time, + diffusionRate, + decayRate, swipeRadius, swipeBlur, - }: { - width: number; - height: number; - diffusionRate: number; - decayRate: number; - deltaTime: number; - time: number; - swipeRadius: number; - swipeBlur: number; - }) { + }: CommonParameters & DiffusionSettings) { this.device.queue.writeBuffer( this.uniforms, 0, new Float32Array([ - width, - height, - diffusionRate, - decayRate, + canvasSize[0], + canvasSize[1], deltaTime, time, + diffusionRate, + decayRate, swipeRadius, swipeBlur, ]) diff --git a/src/pipelines/diffusion/diffusion-settings.ts b/src/pipelines/diffusion/diffusion-settings.ts new file mode 100644 index 0000000..25d77ed --- /dev/null +++ b/src/pipelines/diffusion/diffusion-settings.ts @@ -0,0 +1,6 @@ +export interface DiffusionSettings { + diffusionRate: number; + decayRate: number; + swipeRadius: number; + swipeBlur: number; +} diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts index 51d1af1..aba0533 100644 --- a/src/pipelines/render/render-pipeline.ts +++ b/src/pipelines/render/render-pipeline.ts @@ -1,8 +1,13 @@ import { setUpFullScreenQuad } from '../../utils/full-screen-quad'; +import { CommonParameters } from '../common-parameters'; +import { RenderSettings } from './render-settings'; import shader from './render.wgsl'; export class RenderPipeline { + private static readonly UNIFORM_COUNT = 4; + private readonly pipeline: GPURenderPipeline; + private readonly uniforms: GPUBuffer; private readonly quadVertexBuffer: GPUBuffer; private bindGroup?: GPUBindGroup; @@ -34,6 +39,23 @@ export class RenderPipeline { topology: 'triangle-strip', }, }); + + this.uniforms = this.device.createBuffer({ + size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + } + + public setParameters({ + canvasSize, + deltaTime, + time, + }: CommonParameters & RenderSettings) { + this.device.queue.writeBuffer( + this.uniforms, + 0, + new Float32Array([canvasSize[0], canvasSize[1], deltaTime, time]) + ); } public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) { @@ -64,13 +86,19 @@ export class RenderPipeline { entries: [ { binding: 0, + resource: { + buffer: this.uniforms, + }, + }, + { + binding: 1, resource: this.device.createSampler({ magFilter: 'linear', minFilter: 'linear', }), }, { - binding: 1, + binding: 2, resource: colorTexture.createView(), }, ], diff --git a/src/pipelines/render/render-settings.ts b/src/pipelines/render/render-settings.ts new file mode 100644 index 0000000..efa387a --- /dev/null +++ b/src/pipelines/render/render-settings.ts @@ -0,0 +1 @@ +export interface RenderSettings {} diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl index de8d0a4..1eebaf3 100644 --- a/src/pipelines/render/render.wgsl +++ b/src/pipelines/render/render.wgsl @@ -1,8 +1,14 @@ -@group(0) @binding(0) var mySampler: sampler; -@group(0) @binding(1) var TargetTexture : texture_2d; +struct Settings { + size : vec2, + deltaTime : f32, + time : f32, +}; +@group(0) @binding(0) var settings : Settings; +@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var TargetTexture : texture_2d; @fragment fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(textureSample(TargetTexture, mySampler, uv).rgb * 1.0, 1); + return vec4(textureSample(TargetTexture, mySampler, uv).r * 1.0, settings.deltaTime * 0.0, 0.0, 1.0); } diff --git a/src/settings.ts b/src/settings.ts index 6ed08ca..8fe5e36 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,32 +1,27 @@ -interface Settings { - agentCount: number; - renderSpeed: number; - startingRadius: number; - trailWeight: number; - decayRate: number; - diffusionRate: number; - moveSpeed: number; - turnSpeed: number; - sensorAngleDegrees: number; - sensorOffsetDst: number; - swipeRadius: number; - swipeBlur: number; -} +import { GameLoopSettings } from './game-loop/game-loop-settings'; +import { AgentSettings } from './pipelines/agents/agent-settings'; +import { BrushSettings } from './pipelines/brush/brush-settings'; +import { DiffusionSettings } from './pipelines/diffusion/diffusion-settings'; +import { RenderSettings } from './pipelines/render/render-settings'; -export const settings: Settings = { +export const settings: GameLoopSettings & + AgentSettings & + BrushSettings & + DiffusionSettings & + RenderSettings = { agentCount: 1_000, renderSpeed: 1, startingRadius: 0.15, - decayRate: 0.02, - diffusionRate: 0.8, - trailWeight: 5, moveSpeed: 0.025, turnSpeed: 6, sensorAngleDegrees: 30, sensorOffsetDst: 0.025, + decayRate: 0.02, + diffusionRate: 0.8, + swipeRadius: 0.003, swipeBlur: 0.002, }; diff --git a/src/index.scss b/src/styles/index.scss similarity index 100% rename from src/index.scss rename to src/styles/index.scss diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss new file mode 100644 index 0000000..207b151 --- /dev/null +++ b/src/styles/mixins.scss @@ -0,0 +1,25 @@ +@mixin card { + border: 2px solid white; + border-radius: 12px; + + backdrop-filter: blur(24px); + @supports not (backdrop-filter: blur(24px)) { + background-color: rgba(0, 0, 0, 0.15); + } + + &:focus { + outline: none; + border: 4px solid white; + } +} + +@mixin center-children { + display: flex; + justify-content: center; + align-items: center; +} + +@mixin square($size) { + width: $size; + height: $size; +} diff --git a/src/utils/clamp.ts b/src/utils/clamp.ts new file mode 100644 index 0000000..45da555 --- /dev/null +++ b/src/utils/clamp.ts @@ -0,0 +1,4 @@ +export const clamp = (value: number, min: number, max: number): number => + Math.min(max, Math.max(min, value)); + +export const clamp01 = (value: number): number => Math.min(1, Math.max(0, value)); diff --git a/src/utils/handle-full-screen.ts b/src/utils/handle-full-screen.ts new file mode 100644 index 0000000..8fc7fd1 --- /dev/null +++ b/src/utils/handle-full-screen.ts @@ -0,0 +1,48 @@ +export const handleFullScreen = ( + minimizeButton: HTMLElement, + maximizeButton: HTMLElement, + target: HTMLElement +) => { + if (!document.fullscreenEnabled) { + minimizeButton.style.visibility = 'hidden'; + maximizeButton.style.visibility = 'hidden'; + return; + } + + const isInFullScreen = (): boolean => document.fullscreenElement !== null; + + const showButtons = () => { + minimizeButton.style.visibility = isInFullScreen() ? 'visible' : 'hidden'; + maximizeButton.style.visibility = isInFullScreen() ? 'hidden' : 'visible'; + }; + + showButtons(); + + let currentWindowHeight = innerHeight; + + const followToggle = () => { + showButtons(); + currentWindowHeight = innerHeight; + }; + + const triggerToggle = async () => { + await (isInFullScreen() ? document.exitFullscreen() : target.requestFullscreen()); + followToggle(); + }; + + addEventListener('keydown', (e) => { + if (e.key === 'F11') { + triggerToggle(); + e.preventDefault(); + } + }); + + addEventListener('resize', () => { + if (isInFullScreen() && currentWindowHeight > innerHeight) { + followToggle(); + } + }); + + maximizeButton.addEventListener('click', triggerToggle); + minimizeButton.addEventListener('click', triggerToggle); +}; diff --git a/src/utils/last.ts b/src/utils/last.ts new file mode 100644 index 0000000..5c319b0 --- /dev/null +++ b/src/utils/last.ts @@ -0,0 +1,3 @@ +export function last(a: Array): T | null { + return a.length > 0 ? a[a.length - 1] : null; +} diff --git a/src/utils/mulberry32.ts b/src/utils/mulberry32.ts deleted file mode 100644 index 04d054d..0000000 --- a/src/utils/mulberry32.ts +++ /dev/null @@ -1,8 +0,0 @@ -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/pretty-print.ts b/src/utils/pretty-print.ts new file mode 100644 index 0000000..48fd593 --- /dev/null +++ b/src/utils/pretty-print.ts @@ -0,0 +1,4 @@ +export const prettyPrint = (o: any): string => + JSON.stringify(o, (_, v) => (v?.toFixed ? Number(v.toFixed(3)) : v), ' ') + .replace(/("|,|{|^\n)/g, '') + .replace(/(\W*}\n?)+/g, '\n\n'); diff --git a/src/utils/random-between.ts b/src/utils/random-between.ts deleted file mode 100644 index dd547a5..0000000 --- a/src/utils/random-between.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const randomBetween = (min: number, max: number) => { - return Math.random() * (max - min) + min; -}; diff --git a/src/utils/random.ts b/src/utils/random.ts new file mode 100644 index 0000000..907e764 --- /dev/null +++ b/src/utils/random.ts @@ -0,0 +1,20 @@ +export abstract class Random { + // https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript, Mulberry32 + + private static _seed = 42; + + public static set seed(value: number) { + Random._seed = value; + } + + public static getRandom(): number { + let t = (Random._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; + } + + public static randomBetween(from: number, to: number): number { + return from + Random.getRandom() * (to - from); + } +}