diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index 543786e..fd750f7 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -6,6 +6,7 @@ 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 { ResizableTexture } from '../utils/graphics/resizable-texture'; import { sleep } from '../utils/sleep'; import { spawnAgents } from './spawn-agents'; @@ -14,6 +15,8 @@ import { vec2 } from 'gl-matrix'; export default class GameLoop { private readonly deltaTimeCalculator = new DeltaTimeCalculator(); + private readonly trailMapA: ResizableTexture; + private readonly trailMapB: ResizableTexture; private readonly commonState: CommonState; private readonly copyPipeline: CopyPipeline; private readonly agentPipeline: AgentPipeline; @@ -21,11 +24,6 @@ export default class GameLoop { private readonly brushPipeline: BrushPipeline; private readonly diffusionPipeline: DiffusionPipeline; - private trailMapA?: GPUTexture; - private trailMapB?: GPUTexture; - private trailMapAView?: GPUTextureView; - private trailMapBView?: GPUTextureView; - private hasFinished = false; private readonly hasFinishedPromise: Promise = new Promise( (resolve) => (this.resolveHasFinished = resolve) @@ -45,6 +43,8 @@ export default class GameLoop { alphaMode: 'premultiplied', }); + this.trailMapA = new ResizableTexture(this.device, this.canvasSize); + this.trailMapB = new ResizableTexture(this.device, this.canvasSize); this.resize(); this.commonState = new CommonState(this.device); @@ -60,8 +60,8 @@ export default class GameLoop { window.addEventListener('resize', this.resize.bind(this)); - window.addEventListener('mousemove', this.onSwipe.bind(this)); - window.addEventListener('mousedown', (e) => { + canvas.addEventListener('mousemove', this.onSwipe.bind(this)); + canvas.addEventListener('mousedown', (e) => { this.brushPipeline.clearSwipes(); this.isSwipeActive = true; this.onSwipe(e); @@ -91,31 +91,6 @@ export default class GameLoop { const devicePixelRatio = window.devicePixelRatio || 1; this.canvas.width = this.canvas.clientWidth * devicePixelRatio; this.canvas.height = this.canvas.clientHeight * devicePixelRatio; - - this.trailMapA?.destroy(); - this.trailMapA = this.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, - }, - usage: - GPUTextureUsage.STORAGE_BINDING | - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.RENDER_ATTACHMENT, - }); } private async render(time: DOMHighResTimeStamp) { @@ -137,15 +112,23 @@ export default class GameLoop { const commandEncoder = this.device.createCommandEncoder(); for (let i = 0; i < settings.renderSpeed; i++) { - this.copyPipeline.execute(commandEncoder, this.trailMapAView, this.trailMapBView); - this.brushPipeline.execute(commandEncoder, this.trailMapBView); - this.agentPipeline.execute(commandEncoder, this.trailMapAView, this.trailMapBView); + this.copyPipeline.execute( + commandEncoder, + this.trailMapA.getTextureView(), + this.trailMapB.getTextureView() + ); + this.brushPipeline.execute(commandEncoder, this.trailMapB.getTextureView()); + this.agentPipeline.execute( + commandEncoder, + this.trailMapA.getTextureView(), + this.trailMapB.getTextureView() + ); this.diffusionPipeline.execute( commandEncoder, - this.trailMapBView, - this.trailMapAView + this.trailMapB.getTextureView(), + this.trailMapA.getTextureView() ); - this.renderPipeline.execute(commandEncoder, this.trailMapAView); + this.renderPipeline.execute(commandEncoder, this.trailMapA.getTextureView()); } this.device.queue.submit([commandEncoder.finish()]); @@ -157,6 +140,11 @@ export default class GameLoop { if (settings.simulatedDelayMs > 0) { await sleep(settings.simulatedDelayMs); } + + // avoid resizing during rendering + this.trailMapA.resize(this.canvasSize); + this.trailMapB.resize(this.canvasSize); + requestAnimationFrame(this.render.bind(this)); } diff --git a/src/game-loop/spawn-agents.ts b/src/game-loop/spawn-agents.ts index 295333e..0770008 100644 --- a/src/game-loop/spawn-agents.ts +++ b/src/game-loop/spawn-agents.ts @@ -6,22 +6,19 @@ 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); + const center = vec2.scale(vec2.create(), canvasSize, 0.5); + return new Array(agentCount).fill(0).map(() => { - const radius = Random.randomBetween(0, settings.startingRadius / ratio); + const radius = Random.randomBetween(0, minSize * settings.startingRadius); 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, + direction: vec2.fromValues(Math.cos(angle + Math.PI), Math.sin(angle + Math.PI)), species: 0, timeToLive: Random.randomBetween(10, 15000), }; diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index 2a48619..1e3c156 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -51,12 +51,10 @@ export class AgentPipeline { new Float32Array(this.agentsBuffer.getMappedRange()).set( agents.flatMap((agent) => [ - agent.position[0], - agent.position[1], - agent.angle, + ...agent.position, + ...agent.direction, agent.species, agent.timeToLive, - 0, // padding ]) ); this.agentsBuffer.unmap(); @@ -66,8 +64,8 @@ export class AgentPipeline { brushTrailWeight, moveSpeed, turnSpeed, - sensorAngleDegrees, - sensorOffsetDst, + sensorOffsetAngle, + sensorOffsetDistance, }: AgentSettings) { this.device.queue.writeBuffer( this.uniforms, @@ -76,8 +74,8 @@ export class AgentPipeline { brushTrailWeight, moveSpeed, turnSpeed, - (sensorAngleDegrees * Math.PI) / 180, - sensorOffsetDst, + (sensorOffsetAngle * Math.PI) / 180, + sensorOffsetDistance, ]) ); } diff --git a/src/pipelines/agents/agent-settings.ts b/src/pipelines/agents/agent-settings.ts index 6ca626d..aa309fe 100644 --- a/src/pipelines/agents/agent-settings.ts +++ b/src/pipelines/agents/agent-settings.ts @@ -2,6 +2,6 @@ export interface AgentSettings { brushTrailWeight: number; moveSpeed: number; turnSpeed: number; - sensorAngleDegrees: number; - sensorOffsetDst: number; + sensorOffsetAngle: number; + sensorOffsetDistance: number; } diff --git a/src/pipelines/agents/agent.ts b/src/pipelines/agents/agent.ts index 2f4cd1a..84e463d 100644 --- a/src/pipelines/agents/agent.ts +++ b/src/pipelines/agents/agent.ts @@ -2,7 +2,7 @@ import { vec2 } from 'gl-matrix'; export interface Agent { position: vec2; - angle: number; + direction: vec2; species: number; timeToLive: number; } diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 8bfb75d..e0dad82 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -1,6 +1,6 @@ struct Agent { position: vec2, - angle: f32, + direction: vec2, species: f32, timeToLive: f32 } @@ -15,10 +15,10 @@ struct Settings { @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; +@group(1) @binding(2) var trailMapIn: texture_2d; +@group(1) @binding(3) var trailMapOut: texture_storage_2d; -@compute @workgroup_size(8, 8) +@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) global_id: vec3) { let id = global_id.x; @@ -29,20 +29,20 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { var agent = agents[id]; if (agent.timeToLive <= 0.) { - agent.position = vec2( - 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) + state.time); - agent.species = 1; - agent.timeToLive = 1000; - agents[id] = agent; + // agent.position = vec2( + // 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) + state.time); + // agent.species = 1; + // agent.timeToLive = 1000; + // agents[id] = agent; return; } let random = random_with_seed(agent.position, f32(id) + state.time); + let trailCurrent = textureLoad(trailMapIn, vec2(agent.position), 0); - let trailCurrent = sense(agent, 0, 0); var weight: f32; if(agent.species == 0) { weight = trailCurrent.r - trailCurrent.g; @@ -53,10 +53,11 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { agent.timeToLive = 0; return; } + - let trailForward = sense(agent, settings.sensorOffset, 0); - let trailLeft = sense(agent, settings.sensorOffset, settings.sensorAngle); - let trailRight = sense(agent, settings.sensorOffset, -settings.sensorAngle); + let trailForward = sense(agent.position, agent.direction, settings.sensorOffset, 0); + let trailLeft = sense(agent.position, agent.direction, settings.sensorOffset, settings.sensorAngle); + let trailRight = sense(agent.position, agent.direction, settings.sensorOffset, -settings.sensorAngle); var weightForward: f32 = trailForward.a * settings.brushTrailWeight; var weightLeft: f32 = trailLeft.a * settings.brushTrailWeight; @@ -71,42 +72,44 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { weightRight += trailRight.g - trailRight.r; } + var rotation: f32 = 0; if (weightForward < weightLeft && weightForward < weightRight) { - agent.angle += (random - 0.5) * 2. * settings.turnRate * state.deltaTime; + rotation = (random - 0.5) * 2. * settings.turnRate * state.deltaTime; } else if (weightLeft < weightRight) { - agent.angle -= random * settings.turnRate * state.deltaTime; + rotation = random * -settings.turnRate * state.deltaTime; } else if (weightRight < weightLeft) { - agent.angle += random * settings.turnRate * state.deltaTime; + rotation = random * settings.turnRate * state.deltaTime; } - let direction = vec2(cos(agent.angle), sin(agent.angle)); - 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; + var nextDirection = agent.direction * mat2x2(cos(rotation), sin(rotation), -sin(rotation), cos(rotation)); + + var nextPosition = agent.position + agent.direction * settings.moveRate * state.deltaTime; + nextPosition = clamp(nextPosition, vec2(0, 0), state.size); + if nextPosition.x == 0 || nextPosition.x == state.size.x || nextPosition.y == 0 || nextPosition.y == state.size.y { + rotation = 3.14159265359 + random - 0.5; + nextDirection = agent.direction * mat2x2(cos(rotation), sin(rotation), -sin(rotation), cos(rotation)); } var trail = vec4(0, 1, 0, 0); if (agent.species == 0) { - trail = vec4(1, 0, 0, 0); + trail = vec4(0.1, 0, 0, 0); } - textureStore(TrailMapOut, vec2(newPos * state.size), trail); - agent.position = newPos; + let current = textureLoad(trailMapIn, vec2(nextPosition), 0); + textureStore(trailMapOut, vec2(nextPosition), vec4(trail.rgb + current.rgb, 0)); + + agent.position = nextPosition; + agent.direction = nextDirection; 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(state.size); - let sensorPos: vec2 = agent.position + sensorDir * sensorOffset; - return textureLoad(TrailMapIn, vec2(sensorPos * state.size), 0); +fn sense(agentPosition: vec2, agentDirection: vec2, sensorOffset: f32, sensorOffsetAngle: f32) -> vec4 { + let sensorDirection = agentDirection * mat2x2(cos(sensorOffsetAngle), sin(sensorOffsetAngle), -sin(sensorOffsetAngle), cos(sensorOffsetAngle)); + let sensorPosition = vec2(agentPosition + sensorDirection * sensorOffset); + return textureLoad(trailMapIn, sensorPosition, 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/copy/copy-pipeline.ts b/src/pipelines/copy/copy-pipeline.ts index fcf1b3a..021125d 100644 --- a/src/pipelines/copy/copy-pipeline.ts +++ b/src/pipelines/copy/copy-pipeline.ts @@ -1,10 +1,17 @@ import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad/full-screen-quad'; import { smartCompile } from '../../utils/graphics/smart-compile'; +import shader from './copy.wgsl'; + +import { vec2 } from 'gl-matrix'; export class CopyPipeline { + private static readonly UNIFORM_COUNT = 2; + private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPURenderPipeline; - private readonly quadVertexBuffer: GPUBuffer; + private readonly uniforms: GPUBuffer; + + private readonly vertexBuffer: GPUBuffer; private bindGroup?: GPUBindGroup; private previousTrailMapIn?: GPUTextureView; @@ -12,26 +19,50 @@ export class CopyPipeline { public constructor(private readonly device: GPUDevice) { this.bindGroupLayout = device.createBindGroupLayout(CopyPipeline.bindGroupLayout); - const { buffer, vertex } = setUpFullScreenQuad(device); - this.quadVertexBuffer = buffer; + this.uniforms = this.device.createBuffer({ + size: CopyPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + this.vertexBuffer = device.createBuffer({ + size: 2 * 4 * Float32Array.BYTES_PER_ELEMENT, // 4 x vec2 + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, + }); + // prettier-ignore + const vertexData = [ + // U V + 0.0, 1.0, + 1.0, 1.0, + 0.0, 0.0, + 1.0, 0.0, + ]; + new Float32Array(this.vertexBuffer.getMappedRange()).set(vertexData); + this.vertexBuffer.unmap(); this.pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [this.bindGroupLayout], }), - vertex, + vertex: { + module: smartCompile(device, shader), + entryPoint: 'vertex', + buffers: [ + { + arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, + stepMode: 'vertex', + attributes: [ + { + shaderLocation: 0, + offset: 0, + format: 'float32x2', + }, + ], + }, + ], + }, 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); - }` - ), + module: smartCompile(device, shader), entryPoint: 'fragment', targets: [ { @@ -48,8 +79,11 @@ export class CopyPipeline { public execute( commandEncoder: GPUCommandEncoder, trailMapIn: GPUTextureView, - trailMapOut: GPUTextureView + trailMapOut: GPUTextureView, + scale: vec2 = vec2.fromValues(1, 1) ) { + this.device.queue.writeBuffer(this.uniforms, 0, new Float32Array(scale)); + const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { @@ -64,13 +98,13 @@ export class CopyPipeline { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this.pipeline); passEncoder.setBindGroup(0, this.bindGroup); - passEncoder.setVertexBuffer(0, this.quadVertexBuffer); + passEncoder.setVertexBuffer(0, this.vertexBuffer); passEncoder.draw(4, 1); passEncoder.end(); } public destroy() { - this.quadVertexBuffer.destroy(); + this.vertexBuffer.destroy(); } private ensureBindGroupExists(trailMapIn: GPUTextureView) { @@ -80,13 +114,19 @@ export class CopyPipeline { entries: [ { binding: 0, + resource: { + buffer: this.uniforms, + }, + }, + { + binding: 1, resource: this.device.createSampler({ magFilter: 'linear', minFilter: 'linear', }), }, { - binding: 1, + binding: 2, resource: trailMapIn, }, ], @@ -101,13 +141,20 @@ export class CopyPipeline { entries: [ { binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { + type: 'uniform', + }, + }, + { + binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering', }, }, { - binding: 1, + binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', diff --git a/src/pipelines/copy/copy.wgsl b/src/pipelines/copy/copy.wgsl new file mode 100644 index 0000000..94b017f --- /dev/null +++ b/src/pipelines/copy/copy.wgsl @@ -0,0 +1,19 @@ +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +} + +@vertex +fn vertex(@location(0) uv: vec2) -> VertexOutput { + let ndc = uv * sourceScaler * vec2(2) - vec2(1); + return VertexOutput(vec4(ndc.x, -ndc.y, 0, 1), uv); +} + +@group(0) @binding(0) var sourceScaler: vec2; +@group(0) @binding(1) var Sampler: sampler; +@group(0) @binding(2) var original: texture_2d; + +@fragment +fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { + return textureSample(original, Sampler, uv); +} diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 0621c49..063bcdf 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -40,9 +40,9 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { current += change / 4; let decayed = vec4( - current.rgb, - current.a - ) - vec4(vec3(settings.decayRateTrails), settings.decayRateBrush) * state.deltaTime; + current.rgb * settings.decayRateTrails, + current.a * settings.decayRateBrush + ); return clamp(decayed, vec4(0), vec4(1)); } diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index 001befa..4fe649b 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -10,7 +10,7 @@ export class DiffusionPipeline { private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPURenderPipeline; private readonly uniforms: GPUBuffer; - private readonly quadVertexBuffer: GPUBuffer; + private readonly vertexBuffer: GPUBuffer; private readonly noise: GPUTextureView; private bindGroup?: GPUBindGroup; @@ -25,7 +25,7 @@ export class DiffusionPipeline { ); const { buffer, vertex } = setUpFullScreenQuad(device); - this.quadVertexBuffer = buffer; + this.vertexBuffer = buffer; this.pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ @@ -90,7 +90,7 @@ export class DiffusionPipeline { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this.pipeline); - passEncoder.setVertexBuffer(0, this.quadVertexBuffer); + passEncoder.setVertexBuffer(0, this.vertexBuffer); this.commonState.execute(passEncoder); passEncoder.setBindGroup(1, this.bindGroup); passEncoder.draw(4, 1); @@ -127,7 +127,7 @@ export class DiffusionPipeline { } public destroy() { - this.quadVertexBuffer.destroy(); + this.vertexBuffer.destroy(); this.uniforms.destroy(); } diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts index 5394998..3c38e9a 100644 --- a/src/pipelines/render/render-pipeline.ts +++ b/src/pipelines/render/render-pipeline.ts @@ -11,7 +11,7 @@ export class RenderPipeline { private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPURenderPipeline; private readonly uniforms: GPUBuffer; - private readonly quadVertexBuffer: GPUBuffer; + private readonly vertexBuffer: GPUBuffer; private bindGroup?: GPUBindGroup; private previousColorTexture?: GPUTextureView; @@ -24,7 +24,7 @@ export class RenderPipeline { this.bindGroupLayout = device.createBindGroupLayout(RenderPipeline.bindGroupLayout); const { buffer, vertex } = setUpFullScreenQuad(device); - this.quadVertexBuffer = buffer; + this.vertexBuffer = buffer; this.pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ @@ -82,7 +82,7 @@ export class RenderPipeline { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this.pipeline); this.commonState.execute(passEncoder); - passEncoder.setVertexBuffer(0, this.quadVertexBuffer); + passEncoder.setVertexBuffer(0, this.vertexBuffer); passEncoder.setBindGroup(1, this.bindGroup); passEncoder.draw(4, 1); passEncoder.end(); @@ -118,7 +118,7 @@ export class RenderPipeline { } public destroy() { - this.quadVertexBuffer.destroy(); + this.vertexBuffer.destroy(); this.uniforms.destroy(); } diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl index be95249..9fcd9d2 100644 --- a/src/pipelines/render/render.wgsl +++ b/src/pipelines/render/render.wgsl @@ -18,13 +18,13 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { let brushStrength = traces.a; let rgbColor = sqrt(vec3( - settings.speciesColorA * speciesAStrength + - settings.speciesColorB * speciesBStrength + + settings.speciesColorA * clamp(speciesAStrength, 0, 1) + + settings.speciesColorB * clamp(speciesBStrength, 0, 1) + settings.brushColor * brushStrength )); - let bg = vec3(0.9) + 0.05 * (random.r - 0.5); + let bg = vec3(0.9) + 0.075 * random.r; return vec4(bg - rgbColor, 1); diff --git a/src/settings.ts b/src/settings.ts index f01d649..2c06d78 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -19,7 +19,7 @@ export const settings: GameLoopSettings & BrushSettings & DiffusionSettings & RenderSettings = { - agentCount: 500, + agentCount: 1_000_000, startingRadius: 0.15, renderSpeed: 1, @@ -29,15 +29,15 @@ export const settings: GameLoopSettings & brushWidthRandomness: 8, brushTrailWeight: 5, - moveSpeed: 0.025, - turnSpeed: 6, - sensorAngleDegrees: 30, - sensorOffsetDst: 0.025, + moveSpeed: 80, + turnSpeed: 10, + sensorOffsetAngle: 30, + sensorOffsetDistance: 60, diffusionRateTrails: 4, - decayRateTrails: 1.5, + decayRateTrails: 0.9, diffusionRateBrush: 4, - decayRateBrush: 0.15, + decayRateBrush: 0.98, brushColor: palette.blue, speciesColorA: palette.yellow, diff --git a/src/utils/delta-time-calculator.ts b/src/utils/delta-time-calculator.ts index a798169..3cb6076 100644 --- a/src/utils/delta-time-calculator.ts +++ b/src/utils/delta-time-calculator.ts @@ -14,6 +14,7 @@ export class DeltaTimeCalculator { const delta = currentTime - this.previousTime; this.previousTime = currentTime; + return 1 / 60; return Math.min(delta / 1000, this.maxDeltaTimeInSeconds); } diff --git a/src/utils/graphics/fbm-noise/fbm-noise.ts b/src/utils/graphics/fbm-noise/fbm-noise.ts index 7e61a86..81ed618 100644 --- a/src/utils/graphics/fbm-noise/fbm-noise.ts +++ b/src/utils/graphics/fbm-noise/fbm-noise.ts @@ -36,7 +36,7 @@ export const generateFbmNoise = ({ const cacheKey = `${width}x${height}x${JSON.stringify(constants)}`; if (!textureCache.has(cacheKey)) { const { buffer, vertex } = setUpFullScreenQuad(device); - const quadVertexBuffer = buffer; + const vertexBuffer = buffer; const pipeline = device.createRenderPipeline({ layout: 'auto', @@ -81,7 +81,7 @@ export const generateFbmNoise = ({ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); - passEncoder.setVertexBuffer(0, quadVertexBuffer); + passEncoder.setVertexBuffer(0, vertexBuffer); passEncoder.draw(4, 1); passEncoder.end(); diff --git a/src/utils/graphics/full-screen-quad/full-screen-quad.ts b/src/utils/graphics/full-screen-quad/full-screen-quad.ts index 4075d38..e0c05f4 100644 --- a/src/utils/graphics/full-screen-quad/full-screen-quad.ts +++ b/src/utils/graphics/full-screen-quad/full-screen-quad.ts @@ -8,7 +8,7 @@ export const setUpFullScreenQuad = ( vertex: GPUVertexState; } => { const buffer = device.createBuffer({ - size: 4 * 4 * 4, // 4x vec4 + size: 4 * 4 * Float32Array.BYTES_PER_ELEMENT, // 4 x vec4 usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); @@ -30,7 +30,7 @@ export const setUpFullScreenQuad = ( entryPoint: 'vertex', buffers: [ { - arrayStride: 4 * 4, + arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT, stepMode: 'vertex', attributes: [ { diff --git a/src/utils/graphics/noise/noise.ts b/src/utils/graphics/noise/noise.ts index 0e37cf3..b39bb5f 100644 --- a/src/utils/graphics/noise/noise.ts +++ b/src/utils/graphics/noise/noise.ts @@ -16,7 +16,7 @@ export const generateNoise = ({ const cacheKey = `${width}x${height}`; if (!textureCache.has(cacheKey)) { const { buffer, vertex } = setUpFullScreenQuad(device); - const quadVertexBuffer = buffer; + const vertexBuffer = buffer; const pipeline = device.createRenderPipeline({ layout: 'auto', @@ -73,7 +73,7 @@ export const generateNoise = ({ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); - passEncoder.setVertexBuffer(0, quadVertexBuffer); + passEncoder.setVertexBuffer(0, vertexBuffer); passEncoder.draw(4, 1); passEncoder.end(); diff --git a/src/utils/graphics/resizable-texture.ts b/src/utils/graphics/resizable-texture.ts new file mode 100644 index 0000000..4fe25c7 --- /dev/null +++ b/src/utils/graphics/resizable-texture.ts @@ -0,0 +1,63 @@ +import { CopyPipeline } from '../../pipelines/copy/copy-pipeline'; + +import { vec2 } from 'gl-matrix'; + +export class ResizableTexture { + private texture: GPUTexture; + private textureView: GPUTextureView; + private readonly copyPipeline: CopyPipeline; + private size: vec2 | null = null; + + public constructor(private readonly device: GPUDevice, size: vec2) { + this.copyPipeline = new CopyPipeline(this.device); + this.resize(size); + } + + public resize(size: vec2): void { + if (this.size !== null && vec2.equals(this.size, size)) { + return; + } + + const newTexture = this.device.createTexture({ + format: 'rgba16float', + dimension: '2d', + mipLevelCount: 1, + size: { + width: size.x, + height: size.y, + depthOrArrayLayers: 1, + }, + usage: + GPUTextureUsage.STORAGE_BINDING | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const newTextureView = newTexture.createView(); + + if (this.textureView) { + const commandEncoder = this.device.createCommandEncoder(); + this.copyPipeline.execute( + commandEncoder, + this.textureView, + newTextureView, + vec2.div(vec2.create(), this.size, size) + ); + this.device.queue.submit([commandEncoder.finish()]); + this.texture.destroy(); + } + + this.size = size; + this.texture = newTexture; + this.textureView = newTextureView; + } + + public getTextureView(): GPUTextureView { + return this.textureView; + } + + public destroy(): void { + this.texture.destroy(); + this.copyPipeline.destroy(); + } +}