diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index f0036d3..d19ff81 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -1,17 +1,20 @@ -import { Agent } from '../pipelines/agents/agent'; import { AgentPipeline } from '../pipelines/agents/agent-pipeline'; import { BrushPipeline } from '../pipelines/brush/brush-pipeline'; +import { CommonState } from '../pipelines/common-state/common-state'; +import { CopyPipeline } from '../pipelines/copy/copy-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 { spawnAgents } from './spawn-agents'; import { vec2 } from 'gl-matrix'; export default class GameLoop { private readonly deltaTimeCalculator = new DeltaTimeCalculator(); + private readonly commonState: CommonState; + private readonly copyPipeline: CopyPipeline; private readonly agentPipeline: AgentPipeline; private readonly renderPipeline: RenderPipeline; private readonly brushPipeline: BrushPipeline; @@ -19,6 +22,8 @@ export default class GameLoop { private trailMapA?: GPUTexture; private trailMapB?: GPUTexture; + private trailMapAView?: GPUTextureView; + private trailMapBView?: GPUTextureView; private hasFinished = false; private readonly hasFinishedPromise: Promise = new Promise( @@ -41,10 +46,16 @@ export default class GameLoop { this.resize(); - this.agentPipeline = new AgentPipeline(this.device, this.spawnAgents()); - this.brushPipeline = new BrushPipeline(this.device); - this.diffusionPipeline = new DiffusionPipeline(this.device); - this.renderPipeline = new RenderPipeline(context, this.device); + this.commonState = new CommonState(this.device); + this.copyPipeline = new CopyPipeline(this.device); + this.agentPipeline = new AgentPipeline( + this.device, + spawnAgents(this.canvasSize, settings.agentCount), + this.commonState + ); + this.brushPipeline = new BrushPipeline(this.device, this.commonState); + this.diffusionPipeline = new DiffusionPipeline(this.device, this.commonState); + this.renderPipeline = new RenderPipeline(context, this.device, this.commonState); window.addEventListener('resize', this.resize.bind(this)); window.addEventListener('mousemove', this.onSwipe.bind(this)); @@ -73,33 +84,6 @@ export default class GameLoop { ); } - 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( - this.canvas.width / minSize, - this.canvas.height / minSize - ); - vec2.normalize(size, size); - return new Array(settings.agentCount).fill(0).map(() => { - 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); - vec2.divide(delta, delta, size); - - const position = vec2.add(vec2.create(), center, delta); - - return { - position, - angle: angle + Math.PI, - species: 0, - timeToLive: Random.randomBetween(10, 15000), - }; - }); - } - private resize() { const devicePixelRatio = window.devicePixelRatio || 1; this.canvas.width = this.canvas.clientWidth * devicePixelRatio; @@ -107,19 +91,23 @@ export default class GameLoop { this.trailMapA?.destroy(); this.trailMapA = this.createTrailMap(); + this.trailMapAView = this.trailMapA.createView(); this.trailMapB?.destroy(); this.trailMapB = this.createTrailMap(); + this.trailMapBView = this.trailMapB.createView(); } private createTrailMap(): GPUTexture { return this.device.createTexture({ + format: 'rgba16float', + dimension: '2d', + mipLevelCount: 1, size: { width: this.canvas.width, height: this.canvas.height, depthOrArrayLayers: 1, }, - format: 'rgba16float', usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | @@ -134,28 +122,27 @@ export default class GameLoop { const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time); - const params = { - canvasSize: vec2.fromValues(this.canvas.width, this.canvas.height), - time, - deltaTime, - ...settings, - }; + this.commonState.setParameters(this.canvasSize, deltaTime, time); [ this.agentPipeline, this.brushPipeline, this.diffusionPipeline, this.renderPipeline, - ].forEach((pipeline) => pipeline.setParameters(params)); + ].forEach((pipeline) => pipeline.setParameters(settings)); 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]; + this.copyPipeline.execute(commandEncoder, this.trailMapAView, this.trailMapBView); + this.brushPipeline.execute(commandEncoder, this.trailMapBView); + this.agentPipeline.execute(commandEncoder, this.trailMapAView, this.trailMapBView); + this.diffusionPipeline.execute( + commandEncoder, + this.trailMapBView, + this.trailMapAView + ); + this.renderPipeline.execute(commandEncoder, this.trailMapAView); } this.device.queue.submit([commandEncoder.finish()]); @@ -167,14 +154,20 @@ export default class GameLoop { public destroy() { this.hasFinished = true; + this.copyPipeline?.destroy(); this.agentPipeline?.destroy(); this.brushPipeline?.destroy(); this.diffusionPipeline?.destroy(); this.renderPipeline?.destroy(); + this.commonState?.destroy(); this.trailMapA?.destroy(); this.trailMapB?.destroy(); this.resolveHasFinished(); } + + private get canvasSize(): vec2 { + return vec2.fromValues(this.canvas.width, this.canvas.height); + } } diff --git a/src/game-loop/spawn-agents.ts b/src/game-loop/spawn-agents.ts new file mode 100644 index 0000000..295333e --- /dev/null +++ b/src/game-loop/spawn-agents.ts @@ -0,0 +1,29 @@ +import { Agent } from '../pipelines/agents/agent'; +import { settings } from '../settings'; +import { Random } from '../utils/random'; + +import { vec2 } from 'gl-matrix'; + +export const spawnAgents = (canvasSize: vec2, agentCount: number): Array => { + const minSize = Math.min(...canvasSize); + const ratio = Math.max(...canvasSize) / minSize; + const size = vec2.scale(vec2.create(), canvasSize, 1 / minSize); + vec2.normalize(size, size); + return new Array(agentCount).fill(0).map(() => { + 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); + vec2.divide(delta, delta, size); + + const position = vec2.add(vec2.create(), center, delta); + + return { + position, + angle: angle + Math.PI, + species: 0, + timeToLive: Random.randomBetween(10, 15000), + }; + }); +}; diff --git a/src/index.ts b/src/index.ts index f9e80f4..141b34d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import './index.scss'; import { applyArrayPlugins } from './utils/array'; import { ErrorHandler, Severity } from './utils/error-handler'; import { FullScreenHandler } from './utils/full-screen-handler'; -import { initializeGPU } from './utils/graphics/initialize-gpu'; +import { initializeGpu } from './utils/graphics/initialize-gpu'; declare global { interface Array { @@ -41,11 +41,15 @@ const getElements = () => ({ const main = async () => { const elements = getElements(); + let shouldStop = false; + let game: GameLoop | null = null; + ErrorHandler.addOnErrorListener((error, metadata) => { elements.errorContainer.innerHTML += `
${error.message}
-      

${JSON.stringify(metadata, null, 2)}

`; + game?.destroy(); + shouldStop = true; }); try { @@ -67,12 +71,11 @@ const main = async () => { document.body ); - const gpu = await initializeGPU(); - let game: GameLoop | null = null; + const gpu = await initializeGpu(); elements.restartButton.addEventListener('click', () => game?.destroy()); - while (true) { + while (!shouldStop) { game = new GameLoop(elements.canvas, gpu); await game.start(); } diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index c6f855c..2a48619 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -1,31 +1,39 @@ -import random from '../../utils/graphics/random.wgsl'; import { smartCompile } from '../../utils/graphics/smart-compile'; -import { CommonParameters } from '../common-parameters'; +import { CommonState } from '../common-state/common-state'; import { AGENT_SIZE_IN_BYTES, Agent } from './agent'; import { AgentSettings } from './agent-settings'; import shader from './agent.wgsl'; export class AgentPipeline { private static readonly WORKGROUP_SIZE = 64; - private static readonly UNIFORM_COUNT = 10; + private static readonly UNIFORM_COUNT = 5; + private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPUComputePipeline; private readonly uniforms: GPUBuffer; private readonly agentsBuffer: GPUBuffer; private bindGroup?: GPUBindGroup; - private previousTrailMapIn?: GPUTexture; - private previousTrailMapOut?: GPUTexture; + private previousTrailMapIn?: GPUTextureView; + private previousTrailMapOut?: GPUTextureView; - public constructor(private readonly device: GPUDevice, agents: Array) { + public constructor( + private readonly device: GPUDevice, + agents: Array, + private readonly commonState: CommonState + ) { if (agents.length === 0) { throw new Error('No agents provided'); } + this.bindGroupLayout = device.createBindGroupLayout(AgentPipeline.bindGroupLayout); + this.pipeline = device.createComputePipeline({ - layout: 'auto', + layout: device.createPipelineLayout({ + bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], + }), compute: { - module: smartCompile(device, random, shader), + module: smartCompile(device, CommonState.shaderCode, shader), entryPoint: 'main', }, }); @@ -55,42 +63,36 @@ export class AgentPipeline { } public setParameters({ - canvasSize, - deltaTime, - time, brushTrailWeight, moveSpeed, turnSpeed, sensorAngleDegrees, sensorOffsetDst, - }: CommonParameters & AgentSettings) { + }: AgentSettings) { this.device.queue.writeBuffer( this.uniforms, 0, new Float32Array([ - canvasSize[0], - canvasSize[1], - deltaTime, - time, brushTrailWeight, - moveSpeed * deltaTime, - turnSpeed * deltaTime, + moveSpeed, + turnSpeed, (sensorAngleDegrees * Math.PI) / 180, sensorOffsetDst, ]) ); } - public execute( + public executeRenderPass( commandEncoder: GPUCommandEncoder, - trailMapIn: GPUTexture, - trailMapOut: GPUTexture + trailMapIn: GPUTextureView, + trailMapOut: GPUTextureView ) { this.ensureBindGroupExists(trailMapIn, trailMapOut); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(this.pipeline); - passEncoder.setBindGroup(0, this.bindGroup); + this.commonState.execute(passEncoder); + passEncoder.setBindGroup(1, this.bindGroup); passEncoder.dispatchWorkgroups( Math.ceil( this.agentsBuffer.size / AGENT_SIZE_IN_BYTES / AgentPipeline.WORKGROUP_SIZE @@ -99,13 +101,32 @@ export class AgentPipeline { passEncoder.end(); } - private ensureBindGroupExists(trailMapIn: GPUTexture, trailMapOut: GPUTexture) { + public execute( + commandEncoder: GPUCommandEncoder, + trailMapIn: GPUTextureView, + trailMapOut: GPUTextureView + ) { + this.ensureBindGroupExists(trailMapIn, trailMapOut); + + const passEncoder = commandEncoder.beginComputePass(); + passEncoder.setPipeline(this.pipeline); + this.commonState.execute(passEncoder); + passEncoder.setBindGroup(1, this.bindGroup); + passEncoder.dispatchWorkgroups( + Math.ceil( + this.agentsBuffer.size / AGENT_SIZE_IN_BYTES / AgentPipeline.WORKGROUP_SIZE + ) + ); + passEncoder.end(); + } + + private ensureBindGroupExists(trailMapIn: GPUTextureView, trailMapOut: GPUTextureView) { if ( this.previousTrailMapIn !== trailMapIn || this.previousTrailMapOut !== trailMapOut ) { this.bindGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), + layout: this.bindGroupLayout, entries: [ { binding: 0, @@ -121,11 +142,11 @@ export class AgentPipeline { }, { binding: 2, - resource: trailMapIn.createView(), + resource: trailMapIn, }, { binding: 3, - resource: trailMapOut.createView(), + resource: trailMapOut, }, ], }); @@ -139,4 +160,39 @@ export class AgentPipeline { this.uniforms.destroy(); this.agentsBuffer.destroy(); } + + private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor { + return { + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, + { + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + { + binding: 2, + visibility: GPUShaderStage.COMPUTE, + texture: { + sampleType: 'float', + }, + }, + { + binding: 3, + visibility: GPUShaderStage.COMPUTE, + storageTexture: { + format: 'rgba16float', + }, + }, + ], + }; + } } diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 3a9328c..8bfb75d 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -6,10 +6,6 @@ struct Agent { } struct Settings { - size: vec2, - deltaTime: f32, - time: f32, - brushTrailWeight: f32, moveRate: f32, turnRate: f32, @@ -17,10 +13,10 @@ struct Settings { sensorOffset: 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; +@group(1) @binding(0) var settings: Settings; +@group(1) @binding(1) var agents: array; +@group(1) @binding(2) var TrailMapIn: texture_2d; +@group(1) @binding(3) var TrailMapOut: texture_storage_2d; @compute @workgroup_size(8, 8) fn main(@builtin(global_invocation_id) global_id: vec3) { @@ -34,17 +30,17 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { if (agent.timeToLive <= 0.) { agent.position = vec2( - random_with_seed(agent.position, f32(id) + settings.time), - random_with_seed(agent.position, f32(id) + settings.time + 12), + random_with_seed(agent.position, f32(id) + state.time), + random_with_seed(agent.position, f32(id) + state.time + 12), ); - agent.angle = random_with_seed(vec2(agent.angle), f32(id) + settings.time); + agent.angle = random_with_seed(vec2(agent.angle), f32(id) + state.time); agent.species = 1; agent.timeToLive = 1000; agents[id] = agent; return; } - let random = random_with_seed(agent.position, f32(id) + settings.time); + let random = random_with_seed(agent.position, f32(id) + state.time); let trailCurrent = sense(agent, 0, 0); var weight: f32; @@ -76,15 +72,15 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { } if (weightForward < weightLeft && weightForward < weightRight) { - agent.angle += (random - 0.5) * 2. * settings.turnRate; + agent.angle += (random - 0.5) * 2. * settings.turnRate * state.deltaTime; } else if (weightLeft < weightRight) { - agent.angle -= random * settings.turnRate; + agent.angle -= random * settings.turnRate * state.deltaTime; } else if (weightRight < weightLeft) { - agent.angle += random * settings.turnRate; + agent.angle += random * settings.turnRate * state.deltaTime; } let direction = vec2(cos(agent.angle), sin(agent.angle)); - var newPos = agent.position + direction / normalize(settings.size) * settings.moveRate; + var newPos = agent.position + direction / normalize(state.size) * settings.moveRate * state.deltaTime; newPos = clamp(newPos, vec2(0, 0), vec2(1, 1)); if (newPos.x == 0. || newPos.x == 1. || newPos.y == 0. || newPos.y == 1.) { agent.angle += 3.14159265359 + random - 0.5; @@ -94,19 +90,23 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { if (agent.species == 0) { trail = vec4(1, 0, 0, 0); } - textureStore(TrailMapOut, vec2(newPos * settings.size), trail); + textureStore(TrailMapOut, vec2(newPos * state.size), trail); agent.position = newPos; - agent.timeToLive -= settings.deltaTime; + agent.timeToLive -= state.deltaTime; agents[id] = agent; } fn sense(agent: Agent, sensorOffset: f32, sensorOffsetAngle: f32) -> vec4 { let sensorAngle = agent.angle + sensorOffsetAngle; - let sensorDir: vec2 = vec2(cos(sensorAngle), sin(sensorAngle)) / normalize(settings.size); + let sensorDir: vec2 = vec2(cos(sensorAngle), sin(sensorAngle)) / normalize(state.size); let sensorPos: vec2 = agent.position + sensorDir * sensorOffset; - return textureLoad(TrailMapIn, vec2(sensorPos * settings.size), 0); + return textureLoad(TrailMapIn, vec2(sensorPos * state.size), 0); } +fn random_with_seed(uv: vec2, seed: f32) -> f32 { + return fract(sin(dot(uv, vec2(12.9898 + seed, 78.233 + seed)))* 43758.5453123 + seed); +} + diff --git a/src/pipelines/brush/brush-pipeline.ts b/src/pipelines/brush/brush-pipeline.ts index 9cf2618..fd3b722 100644 --- a/src/pipelines/brush/brush-pipeline.ts +++ b/src/pipelines/brush/brush-pipeline.ts @@ -1,36 +1,33 @@ -import { generateNoise } from '../../utils/graphics/noise/noise'; +import { catmullRomInterpolation } from '../../utils/catmull-rom-interpolation'; +import { generateFbmNoise } from '../../utils/graphics/fbm-noise/fbm-noise'; import { smartCompile } from '../../utils/graphics/smart-compile'; -import { CommonParameters } from '../common-parameters'; +import { CommonState } from '../common-state/common-state'; import { BrushSettings } from './brush-settings'; import shader from './brush.wgsl'; import { vec2 } from 'gl-matrix'; export class BrushPipeline { - private static readonly UNIFORM_COUNT = 9; + private static readonly UNIFORM_COUNT = 2; private static readonly MAX_LINE_COUNT = 100; private static readonly VERTICES_PER_LINE_SEGMENT = 6; private static readonly ATTRIBUTES_PER_LINE_SEGMENT = 6; + private readonly bindGroupLayout: GPUBindGroupLayout; + private readonly bindGroup: GPUBindGroup; private readonly pipeline: GPURenderPipeline; private readonly uniforms: GPUBuffer; private readonly vertexBuffer: GPUBuffer; - private readonly noise: GPUTextureView; + private linePoints: Array = []; private previousPoints: Array = []; private nextPoint: vec2 | null = null; - private bindGroup: GPUBindGroup; - public constructor(private readonly device: GPUDevice) { - this.noise = generateNoise({ - device, - width: 512, - height: 512, - octaves: 16, - amplitude: 0.5, - gain: 0.8, - lacunarity: 80, - }); + public constructor( + private readonly device: GPUDevice, + private readonly commonState: CommonState + ) { + this.bindGroupLayout = device.createBindGroupLayout(BrushPipeline.bindGroupLayout); this.vertexBuffer = device.createBuffer({ size: @@ -42,9 +39,11 @@ export class BrushPipeline { }); this.pipeline = device.createRenderPipeline({ - layout: 'auto', + layout: device.createPipelineLayout({ + bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], + }), vertex: { - module: smartCompile(device, shader), + module: smartCompile(device, CommonState.shaderCode, shader), entryPoint: 'vertex', buffers: [ { @@ -70,7 +69,7 @@ export class BrushPipeline { ], }, fragment: { - module: smartCompile(device, shader), + module: smartCompile(device, CommonState.shaderCode, shader), entryPoint: 'fragment', targets: [ { @@ -100,8 +99,8 @@ export class BrushPipeline { usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - this.bindGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), + this.bindGroup = this.bindGroup = this.device.createBindGroup({ + layout: this.bindGroupLayout, entries: [ { binding: 0, @@ -109,17 +108,6 @@ export class BrushPipeline { buffer: this.uniforms, }, }, - { - binding: 1, - resource: this.device.createSampler({ - magFilter: 'linear', - minFilter: 'linear', - }), - }, - { - binding: 2, - resource: this.noise, - }, ], }); } @@ -135,32 +123,13 @@ export class BrushPipeline { this.nextPoint = null; } - public setParameters({ - canvasSize, - deltaTime, - time, - brushWidth, - brushWidthRandomness, - }: CommonParameters & BrushSettings) { + public setParameters({ brushWidth, brushWidthRandomness }: BrushSettings) { this.device.queue.writeBuffer( this.uniforms, 0, - new Float32Array([ - ...canvasSize, - deltaTime, - time, - brushWidth / 2, - brushWidthRandomness, - ]) + new Float32Array([brushWidth / 2, brushWidthRandomness]) ); - // this.linePoints = [ - // vec2.fromValues(0.1, 0.1), - // vec2.fromValues(0.8, 0.2), - // vec2.fromValues(0.75, 0.8), - // vec2.fromValues(0.1, 0.4), - // ].map((v) => vec2.multiply(v, v, canvasSize)); - if (this.nextPoint == null) { return; } @@ -222,11 +191,11 @@ export class BrushPipeline { ]; } - public execute(commandEncoder: GPUCommandEncoder, trailMapOut: GPUTexture) { + public execute(commandEncoder: GPUCommandEncoder, trailMapOut: GPUTextureView) { const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { - view: trailMapOut.createView(), + view: trailMapOut, loadOp: 'load', storeOp: 'store', }, @@ -235,7 +204,8 @@ export class BrushPipeline { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this.pipeline); - passEncoder.setBindGroup(0, this.bindGroup); + this.commonState.execute(passEncoder); + passEncoder.setBindGroup(1, this.bindGroup); passEncoder.setVertexBuffer(0, this.vertexBuffer); passEncoder.draw(BrushPipeline.VERTICES_PER_LINE_SEGMENT * this.lineCount, 1); passEncoder.end(); @@ -247,31 +217,18 @@ export class BrushPipeline { this.vertexBuffer.destroy(); this.uniforms.destroy(); } + + private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor { + return { + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + type: 'uniform', + }, + }, + ], + }; + } } - -const catmullRomInterpolation = ( - p0: vec2, - p1: vec2, - p2: vec2, - p3: vec2, - t: number -): vec2 => { - const t2 = t * t; - const t3 = t2 * t; - - const x = - 0.5 * - (2 * p1[0] + - (-p0[0] + p2[0]) * t + - (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + - (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3); - - const y = - 0.5 * - (2 * p1[1] + - (-p0[1] + p2[1]) * t + - (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + - (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3); - - return [x, y]; -}; diff --git a/src/pipelines/brush/brush.wgsl b/src/pipelines/brush/brush.wgsl index f5a6122..8b2b07a 100644 --- a/src/pipelines/brush/brush.wgsl +++ b/src/pipelines/brush/brush.wgsl @@ -1,14 +1,9 @@ struct Settings { - size: vec2, - deltaTime: f32, - time: f32, brushWidth: f32, brushWidthRandomness: f32 }; -@group(0) @binding(0) var settings: Settings; -@group(0) @binding(1) var Sampler: sampler; -@group(0) @binding(2) var noise: texture_2d; +@group(1) @binding(0) var settings: Settings; struct VertexOutput { @builtin(position) position: vec4, @@ -23,7 +18,7 @@ fn vertex( @location(1) @interpolate(flat) start: vec2, @location(2) @interpolate(flat) end: vec2 ) -> VertexOutput { - let uv = screenPosition / settings.size; + let uv = screenPosition / state.size; let position = uv * 2.0 - 1.0; return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, end); } @@ -35,7 +30,7 @@ fn fragment( @location(2) end: vec2 ) -> @location(0) vec4 { var distance = distanceFromLine(screenPosition, start, end); - let noise = textureSample(noise, Sampler, screenPosition / settings.size); + let noise = textureSample(noise, noiseSampler, screenPosition / state.size / 50); distance += noise.r * settings.brushWidthRandomness; if(distance > settings.brushWidth) { diff --git a/src/pipelines/common-parameters.ts b/src/pipelines/common-parameters.ts deleted file mode 100644 index f30bc66..0000000 --- a/src/pipelines/common-parameters.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { vec2 } from 'gl-matrix'; - -export interface CommonParameters { - canvasSize: vec2; - deltaTime: number; - time: number; -} diff --git a/src/pipelines/common-state/common-state.ts b/src/pipelines/common-state/common-state.ts new file mode 100644 index 0000000..6de804d --- /dev/null +++ b/src/pipelines/common-state/common-state.ts @@ -0,0 +1,104 @@ +import { generateNoise } from '../../utils/graphics/noise/noise'; + +import { vec2 } from 'gl-matrix'; + +export class CommonState { + private static readonly UNIFORM_COUNT = 4; + + private readonly uniforms: GPUBuffer; + private readonly noise: GPUTextureView; + private readonly bindGroup: GPUBindGroup; + + public readonly bindGroupLayout: GPUBindGroupLayout; + + public static readonly shaderCode = /* wgsl */ ` + struct State { + size: vec2, + deltaTime: f32, + time: f32, + }; + + @group(0) @binding(0) var state: State; + @group(0) @binding(1) var noiseSampler: sampler; + @group(0) @binding(2) var noise: texture_2d; + `; + + public constructor(private readonly device: GPUDevice) { + this.uniforms = this.device.createBuffer({ + size: CommonState.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + this.noise = generateNoise({ + device, + width: 2048, + height: 2048, + }); + + this.bindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: + GPUShaderStage.COMPUTE | GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { + type: 'uniform', + }, + }, + { + binding: 1, + visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT, + sampler: { + type: 'filtering', + }, + }, + { + binding: 2, + visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT, + texture: { + sampleType: 'float', + }, + }, + ], + }); + + this.bindGroup = this.device.createBindGroup({ + layout: this.bindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: this.uniforms, + }, + }, + { + binding: 1, + resource: this.device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + }), + }, + { + binding: 2, + resource: this.noise, + }, + ], + }); + } + + public setParameters(canvasSize: vec2, deltaTime: number, time: number) { + this.device.queue.writeBuffer( + this.uniforms, + 0, + new Float32Array([...canvasSize, deltaTime, time]) + ); + } + + public execute(passEncoder: GPUComputePassEncoder | GPURenderPassEncoder) { + passEncoder.setBindGroup(0, this.bindGroup); + } + + public destroy() { + this.uniforms.destroy(); + } +} diff --git a/src/pipelines/copy/copy-pipeline.ts b/src/pipelines/copy/copy-pipeline.ts new file mode 100644 index 0000000..fcf1b3a --- /dev/null +++ b/src/pipelines/copy/copy-pipeline.ts @@ -0,0 +1,119 @@ +import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad'; +import { smartCompile } from '../../utils/graphics/smart-compile'; + +export class CopyPipeline { + private readonly bindGroupLayout: GPUBindGroupLayout; + private readonly pipeline: GPURenderPipeline; + private readonly quadVertexBuffer: GPUBuffer; + + private bindGroup?: GPUBindGroup; + private previousTrailMapIn?: GPUTextureView; + + public constructor(private readonly device: GPUDevice) { + this.bindGroupLayout = device.createBindGroupLayout(CopyPipeline.bindGroupLayout); + + const { buffer, vertex } = setUpFullScreenQuad(device); + this.quadVertexBuffer = buffer; + + this.pipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [this.bindGroupLayout], + }), + vertex, + fragment: { + module: smartCompile( + device, + /* wgsl */ ` + @group(0) @binding(0) var Sampler: sampler; + @group(0) @binding(1) var original: texture_2d; + + @fragment + fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { + return textureSample(original, Sampler, uv); + }` + ), + entryPoint: 'fragment', + targets: [ + { + format: 'rgba16float', + }, + ], + }, + primitive: { + topology: 'triangle-strip', + }, + }); + } + + public execute( + commandEncoder: GPUCommandEncoder, + trailMapIn: GPUTextureView, + trailMapOut: GPUTextureView + ) { + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: trailMapOut, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + this.ensureBindGroupExists(trailMapIn); + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(this.pipeline); + passEncoder.setBindGroup(0, this.bindGroup); + passEncoder.setVertexBuffer(0, this.quadVertexBuffer); + passEncoder.draw(4, 1); + passEncoder.end(); + } + + public destroy() { + this.quadVertexBuffer.destroy(); + } + + private ensureBindGroupExists(trailMapIn: GPUTextureView) { + if (this.previousTrailMapIn !== trailMapIn) { + this.bindGroup = this.device.createBindGroup({ + layout: this.bindGroupLayout, + entries: [ + { + binding: 0, + resource: this.device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + }), + }, + { + binding: 1, + resource: trailMapIn, + }, + ], + }); + + this.previousTrailMapIn = trailMapIn; + } + } + + private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor { + return { + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + sampler: { + type: 'filtering', + }, + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: 'float', + }, + }, + ], + }; + } +} diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 0d4f28f..0621c49 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -1,58 +1,48 @@ struct Settings { - size: vec2, - deltaTime: f32, - time: f32, - diffusionRateTrails: f32, decayRateTrails: f32, diffusionRateBrush: f32, decayRateBrush: f32, }; -@group(0) @binding(0) var settings: Settings; -@group(0) @binding(1) var Sampler: sampler; -@group(0) @binding(2) var trailMap: texture_2d; -@group(0) @binding(3) var noiseMap: texture_2d; +@group(1) @binding(0) var settings: Settings; +@group(1) @binding(1) var Sampler: sampler; +@group(1) @binding(2) var trailMap: texture_2d; @fragment fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { var current = textureSample(trailMap, Sampler, uv); - let noise = textureSample(noiseMap, Sampler, uv); let neighbours: vec4 = ( - textureSample(trailMap, Sampler, uv + vec2(0, 1) / settings.size) - + textureSample(trailMap, Sampler, uv + vec2(0, -1) / settings.size) - + textureSample(trailMap, Sampler, uv + vec2(-1, 0) / settings.size) - + textureSample(trailMap, Sampler, uv + vec2(1, 0) / settings.size) + textureSample(trailMap, Sampler, uv + vec2(0, 1) / state.size) + + textureSample(trailMap, Sampler, uv + vec2(0, -1) / state.size) + + textureSample(trailMap, Sampler, uv + vec2(-1, 0) / state.size) + + textureSample(trailMap, Sampler, uv + vec2(1, 0) / state.size) ) / 4; - var q = vec4(0); + var change = vec4(0); for (var x: i32 = -1; x <= 1; x++) { for (var y: i32 = -1; y <= 1; y++) { if (x != 0 || y != 0) { let offset = vec2(f32(x), f32(y)); - let neighbour = textureSample(trailMap, Sampler, uv + offset / settings.size); - // let noise = textureSample(noiseMap, Sampler, uv + offset / settings.size * 0.5).r; - let noise = random(uv + offset / settings.size * 0.5); + let neighbour = textureSample(trailMap, Sampler, uv + offset / state.size); + let random = textureSample(noise, noiseSampler, uv + offset / state.size * 0.5).r; + let difference = neighbour - current; - - q += vec4( - min(1.0, length(neighbour.rgb)) * pow(noise, settings.diffusionRateTrails) * difference.rgb, - min(1.0, length(neighbour.a)) * pow(noise, settings.diffusionRateBrush) * difference.a + change += vec4( + length(neighbour.rgb) * pow(random, settings.diffusionRateTrails) * difference.rgb, + min(1.0, length(neighbour.a)) * pow(random, settings.diffusionRateBrush) * difference.a ); } } } - current += q / 4; - - let noise1 = random(uv); - + current += change / 4; let decayed = vec4( current.rgb, current.a - ) - vec4(vec3(settings.decayRateTrails), settings.decayRateBrush) * settings.deltaTime * ((noise1 - 0.5) * 0.25 + 1); + ) - vec4(vec3(settings.decayRateTrails), settings.decayRateBrush) * state.deltaTime; return clamp(decayed, vec4(0), vec4(1)); } diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index 986503e..001befa 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -1,41 +1,39 @@ import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad'; -import { generateNoise } from '../../utils/graphics/noise/noise'; -import random from '../../utils/graphics/random.wgsl'; import { smartCompile } from '../../utils/graphics/smart-compile'; -import { CommonParameters } from '../common-parameters'; +import { CommonState } from '../common-state/common-state'; import shader from './diffuse.wgsl'; import { DiffusionSettings } from './diffusion-settings'; export class DiffusionPipeline { - private static readonly UNIFORM_COUNT = 18; + private static readonly UNIFORM_COUNT = 4; + private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPURenderPipeline; private readonly uniforms: GPUBuffer; private readonly quadVertexBuffer: GPUBuffer; private readonly noise: GPUTextureView; private bindGroup?: GPUBindGroup; - private previousTrailMapIn?: GPUTexture; + private previousTrailMapIn?: GPUTextureView; - public constructor(private readonly device: GPUDevice) { - this.noise = generateNoise({ - device, - width: 256, - height: 256, - octaves: 8, - amplitude: 0.12, - gain: 0.7, - lacunarity: 80, - }); + public constructor( + private readonly device: GPUDevice, + private readonly commonState: CommonState + ) { + this.bindGroupLayout = device.createBindGroupLayout( + DiffusionPipeline.bindGroupLayout + ); const { buffer, vertex } = setUpFullScreenQuad(device); this.quadVertexBuffer = buffer; this.pipeline = device.createRenderPipeline({ - layout: 'auto', + layout: device.createPipelineLayout({ + bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], + }), vertex, fragment: { - module: smartCompile(device, random, shader), + module: smartCompile(device, CommonState.shaderCode, shader), entryPoint: 'fragment', targets: [ { @@ -55,22 +53,15 @@ export class DiffusionPipeline { } public setParameters({ - canvasSize, - deltaTime, - time, diffusionRateTrails, decayRateTrails, diffusionRateBrush, decayRateBrush, - }: CommonParameters & DiffusionSettings) { + }: DiffusionSettings) { this.device.queue.writeBuffer( this.uniforms, 0, new Float32Array([ - canvasSize[0], - canvasSize[1], - deltaTime, - time, diffusionRateTrails, decayRateTrails, diffusionRateBrush, @@ -81,16 +72,16 @@ export class DiffusionPipeline { public execute( commandEncoder: GPUCommandEncoder, - trailMapIn: GPUTexture, - trailMapOut: GPUTexture + trailMapIn: GPUTextureView, + trailMapOut: GPUTextureView ) { this.ensureBindGroupExists(trailMapIn); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { - view: trailMapOut.createView(), - clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, + view: trailMapOut, + clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store', }, @@ -100,15 +91,16 @@ export class DiffusionPipeline { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this.pipeline); passEncoder.setVertexBuffer(0, this.quadVertexBuffer); - passEncoder.setBindGroup(0, this.bindGroup); + this.commonState.execute(passEncoder); + passEncoder.setBindGroup(1, this.bindGroup); passEncoder.draw(4, 1); passEncoder.end(); } - private ensureBindGroupExists(trailMapIn: GPUTexture) { + private ensureBindGroupExists(trailMapIn: GPUTextureView) { if (this.previousTrailMapIn !== trailMapIn) { this.bindGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), + layout: this.bindGroupLayout, entries: [ { binding: 0, @@ -125,11 +117,7 @@ export class DiffusionPipeline { }, { binding: 2, - resource: trailMapIn.createView(), - }, - { - binding: 3, - resource: this.noise, + resource: trailMapIn, }, ], }); @@ -142,4 +130,32 @@ export class DiffusionPipeline { this.quadVertexBuffer.destroy(); this.uniforms.destroy(); } + + private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor { + return { + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + type: 'uniform', + }, + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: { + type: 'filtering', + }, + }, + { + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: 'float', + }, + }, + ], + }; + } } diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts index 88f35ee..5394998 100644 --- a/src/pipelines/render/render-pipeline.ts +++ b/src/pipelines/render/render-pipeline.ts @@ -1,44 +1,38 @@ +import { generateFbmNoise } from '../../utils/graphics/fbm-noise/fbm-noise'; import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad'; -import { generateNoise } from '../../utils/graphics/noise/noise'; -import random from '../../utils/graphics/random.wgsl'; import { smartCompile } from '../../utils/graphics/smart-compile'; -import { CommonParameters } from '../common-parameters'; +import { CommonState } from '../common-state/common-state'; import { RenderSettings } from './render-settings'; import shader from './render.wgsl'; export class RenderPipeline { - private static readonly UNIFORM_COUNT = 16; + private static readonly UNIFORM_COUNT = 12; + private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPURenderPipeline; private readonly uniforms: GPUBuffer; private readonly quadVertexBuffer: GPUBuffer; - private readonly noise: GPUTextureView; private bindGroup?: GPUBindGroup; - private previousColorTexture?: GPUTexture; + private previousColorTexture?: GPUTextureView; public constructor( private readonly context: GPUCanvasContext, - private readonly device: GPUDevice + private readonly device: GPUDevice, + private readonly commonState: CommonState ) { - this.noise = generateNoise({ - device, - width: 512, - height: 512, - octaves: 16, - amplitude: 0.3, - gain: 0.8, - lacunarity: 80, - }); + this.bindGroupLayout = device.createBindGroupLayout(RenderPipeline.bindGroupLayout); const { buffer, vertex } = setUpFullScreenQuad(device); this.quadVertexBuffer = buffer; this.pipeline = device.createRenderPipeline({ - layout: 'auto', + layout: device.createPipelineLayout({ + bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], + }), vertex, fragment: { - module: smartCompile(device, random, shader), + module: smartCompile(device, CommonState.shaderCode, shader), entryPoint: 'fragment', targets: [ { @@ -57,21 +51,11 @@ export class RenderPipeline { }); } - public setParameters({ - canvasSize, - deltaTime, - time, - brushColor, - speciesColorA, - speciesColorB, - }: CommonParameters & RenderSettings) { + public setParameters({ brushColor, speciesColorA, speciesColorB }: RenderSettings) { this.device.queue.writeBuffer( this.uniforms, 0, new Float32Array([ - ...canvasSize, - deltaTime, - time, ...brushColor, 0, //padding ...speciesColorA, @@ -82,14 +66,14 @@ export class RenderPipeline { ); } - public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) { + public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTextureView) { this.ensureBindGroupExists(colorTexture); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: this.context.getCurrentTexture().createView(), - clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, + clearValue: { r: 0, g: 1, b: 1, a: 1 }, loadOp: 'clear', storeOp: 'store', }, @@ -97,16 +81,17 @@ export class RenderPipeline { }; const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this.pipeline); + this.commonState.execute(passEncoder); passEncoder.setVertexBuffer(0, this.quadVertexBuffer); - passEncoder.setBindGroup(0, this.bindGroup); + passEncoder.setBindGroup(1, this.bindGroup); passEncoder.draw(4, 1); passEncoder.end(); } - private ensureBindGroupExists(colorTexture: GPUTexture) { + private ensureBindGroupExists(colorTexture: GPUTextureView) { if (this.previousColorTexture !== colorTexture) { this.bindGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), + layout: this.bindGroupLayout, entries: [ { binding: 0, @@ -123,11 +108,7 @@ export class RenderPipeline { }, { binding: 2, - resource: colorTexture.createView(), - }, - { - binding: 3, - resource: this.noise, + resource: colorTexture, }, ], }); @@ -140,4 +121,32 @@ export class RenderPipeline { this.quadVertexBuffer.destroy(); this.uniforms.destroy(); } + + private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor { + return { + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + type: 'uniform', + }, + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: { + type: 'filtering', + }, + }, + { + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: 'float', + }, + }, + ], + }; + } } diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl index 3fc886b..be95249 100644 --- a/src/pipelines/render/render.wgsl +++ b/src/pipelines/render/render.wgsl @@ -1,21 +1,17 @@ struct Settings { - size: vec2, - deltaTime: f32, - time: f32, brushColor: vec3, speciesColorA: vec3, speciesColorB: vec3, }; -@group(0) @binding(0) var settings: Settings; -@group(0) @binding(1) var Sampler: sampler; -@group(0) @binding(2) var trailMap: texture_2d; -@group(0) @binding(3) var noiseMap: texture_2d; +@group(1) @binding(0) var settings: Settings; +@group(1) @binding(1) var Sampler: sampler; +@group(1) @binding(2) var trailMap: texture_2d; @fragment fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { let traces = textureSample(trailMap, Sampler, uv); - let noise = textureSample(noiseMap, Sampler, uv); + let random = textureSample(noise, noiseSampler, uv); let speciesAStrength = traces.r; let speciesBStrength = traces.g; @@ -28,7 +24,7 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { )); - let bg = vec3(0.9) + 0.2 * (noise.r - 0.5); + let bg = vec3(0.9) + 0.05 * (random.r - 0.5); return vec4(bg - rgbColor, 1); diff --git a/src/settings.ts b/src/settings.ts index 1093507..a5b9700 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -32,12 +32,12 @@ export const settings: GameLoopSettings & sensorAngleDegrees: 30, sensorOffsetDst: 0.025, - diffusionRateTrails: 6, - decayRateTrails: 1, + diffusionRateTrails: 4, + decayRateTrails: 1.5, diffusionRateBrush: 4, decayRateBrush: 0.15, brushColor: palette.blue, speciesColorA: palette.yellow, - speciesColorB: palette.green, + speciesColorB: palette.purple, }; diff --git a/src/utils/catmull-rom-interpolation.ts b/src/utils/catmull-rom-interpolation.ts new file mode 100644 index 0000000..0d5fc14 --- /dev/null +++ b/src/utils/catmull-rom-interpolation.ts @@ -0,0 +1,28 @@ +import { vec2 } from 'gl-matrix'; + +export const catmullRomInterpolation = ( + p0: vec2, + p1: vec2, + p2: vec2, + p3: vec2, + t: number +): vec2 => { + const t2 = t * t; + const t3 = t2 * t; + + const x = + 0.5 * + (2 * p1[0] + + (-p0[0] + p2[0]) * t + + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + + (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3); + + const y = + 0.5 * + (2 * p1[1] + + (-p0[1] + p2[1]) * t + + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + + (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3); + + return [x, y]; +}; diff --git a/src/utils/delta-time-calculator.ts b/src/utils/delta-time-calculator.ts index 9feb969..a798169 100644 --- a/src/utils/delta-time-calculator.ts +++ b/src/utils/delta-time-calculator.ts @@ -1,7 +1,7 @@ export class DeltaTimeCalculator { private previousTime: DOMHighResTimeStamp | null = null; - constructor() { + constructor(private readonly maxDeltaTimeInSeconds: number = 1 / 30) { document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); } @@ -14,7 +14,7 @@ export class DeltaTimeCalculator { const delta = currentTime - this.previousTime; this.previousTime = currentTime; - return delta / 1000; + return Math.min(delta / 1000, this.maxDeltaTimeInSeconds); } private handleVisibilityChange() { diff --git a/src/utils/graphics/fbm-noise/fbm-noise.ts b/src/utils/graphics/fbm-noise/fbm-noise.ts new file mode 100644 index 0000000..7e61a86 --- /dev/null +++ b/src/utils/graphics/fbm-noise/fbm-noise.ts @@ -0,0 +1,93 @@ +import { Random } from '../../random'; +import { setUpFullScreenQuad } from '../full-screen-quad/full-screen-quad'; +import random from '../random.wgsl'; +import { smartCompile } from '../smart-compile'; +import noise from './fbm-noise.wgsl'; + +const textureCache = new Map(); + +export const generateFbmNoise = ({ + device, + width = 1024, + height = 1024, + octaves = 8, + lacunarity = 2, + amplitude = 0.5, + gain = 0.5, +}: { + device: GPUDevice; + width?: number; + height?: number; + octaves?: number; + lacunarity?: number; + amplitude?: number; + gain?: number; +}): GPUTextureView => { + const constants = { + octaves, + lacunarity, + amplitude, + gain, + seedR: Random.getRandom(), + seedG: Random.getRandom(), + seedB: Random.getRandom(), + seedA: Random.getRandom(), + }; + const cacheKey = `${width}x${height}x${JSON.stringify(constants)}`; + if (!textureCache.has(cacheKey)) { + const { buffer, vertex } = setUpFullScreenQuad(device); + const quadVertexBuffer = buffer; + + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex, + fragment: { + module: smartCompile(device, random, noise), + entryPoint: 'fragment', + constants, + targets: [ + { + format: 'rgba16float', + }, + ], + }, + primitive: { + topology: 'triangle-strip', + }, + }); + + const colorTexture = device.createTexture({ + size: { + width, + height, + depthOrArrayLayers: 1, + }, + format: 'rgba16float', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: colorTexture.createView(), + clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + const commandEncoder = device.createCommandEncoder(); + + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setVertexBuffer(0, quadVertexBuffer); + passEncoder.draw(4, 1); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); + textureCache.set(cacheKey, colorTexture); + } + + return textureCache.get(cacheKey).createView(); +}; diff --git a/src/utils/graphics/noise/noise.wgsl b/src/utils/graphics/fbm-noise/fbm-noise.wgsl similarity index 100% rename from src/utils/graphics/noise/noise.wgsl rename to src/utils/graphics/fbm-noise/fbm-noise.wgsl diff --git a/src/utils/graphics/initialize-gpu.ts b/src/utils/graphics/initialize-gpu.ts index ba16e1d..1aacdce 100644 --- a/src/utils/graphics/initialize-gpu.ts +++ b/src/utils/graphics/initialize-gpu.ts @@ -1,6 +1,6 @@ import { ErrorHandler, Severity } from '../error-handler'; -export const initializeGPU = async (): Promise => { +export const initializeGpu = async (): Promise => { const gpu = navigator.gpu; if (!gpu) { throw new Error('WebGPU is not supported in your browser'); diff --git a/src/utils/graphics/noise/noise.ts b/src/utils/graphics/noise/noise.ts index 5224510..0e37cf3 100644 --- a/src/utils/graphics/noise/noise.ts +++ b/src/utils/graphics/noise/noise.ts @@ -1,8 +1,6 @@ -import { Random } from '../../random'; import { setUpFullScreenQuad } from '../full-screen-quad/full-screen-quad'; import random from '../random.wgsl'; import { smartCompile } from '../smart-compile'; -import noise from './noise.wgsl'; const textureCache = new Map(); @@ -10,20 +8,12 @@ export const generateNoise = ({ device, width = 1024, height = 1024, - octaves = 8, - lacunarity = 2, - amplitude = 0.5, - gain = 0.5, }: { device: GPUDevice; width?: number; height?: number; - octaves?: number; - lacunarity?: number; - amplitude?: number; - gain?: number; }): GPUTextureView => { - const cacheKey = `${width}x${height}x${octaves}x${lacunarity}x${amplitude}x${gain}`; + const cacheKey = `${width}x${height}`; if (!textureCache.has(cacheKey)) { const { buffer, vertex } = setUpFullScreenQuad(device); const quadVertexBuffer = buffer; @@ -32,18 +22,21 @@ export const generateNoise = ({ layout: 'auto', vertex, fragment: { - module: smartCompile(device, random, noise), + module: smartCompile( + device, + random, + /* wgsl */ ` + @fragment + fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4( + random_with_seed(uv, 1), + random_with_seed(uv, 2), + random_with_seed(uv, 3), + random_with_seed(uv, 4), + ); + }` + ), entryPoint: 'fragment', - constants: { - octaves, - lacunarity, - amplitude, - gain, - seedR: Random.getRandom(), - seedG: Random.getRandom(), - seedB: Random.getRandom(), - seedA: Random.getRandom(), - }, targets: [ { format: 'rgba16float', diff --git a/src/utils/graphics/smart-compile.ts b/src/utils/graphics/smart-compile.ts index 275969b..044ec24 100644 --- a/src/utils/graphics/smart-compile.ts +++ b/src/utils/graphics/smart-compile.ts @@ -1,6 +1,9 @@ import { ErrorHandler, Severity } from '../error-handler'; -export const smartCompile = (device: GPUDevice, ...code: Array) => { +export const smartCompile = ( + device: GPUDevice, + ...code: Array +): GPUShaderModule => { const concatenated = code.join('\n\n'); const module = device.createShaderModule({ @@ -15,7 +18,9 @@ export const smartCompile = (device: GPUDevice, ...code: Array) => { warning: Severity.WARNING, error: Severity.ERROR, }[message.type], - `${message.message}\n${concatenated.split('\n')[message.lineNum - 1]}` + `${message.message}\n${ + concatenated.split('\n')[message.lineNum - 1] + }\n\nCode:\n${concatenated}\n` ) ) );