diff --git a/README.md b/README.md index b0bc85d..2242fad 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ ## todo -- deploy to github pages -- add infro page -- generate starting shpe on the gpu +- add info page +- generate starting shape on the gpu - add cancer +- graceful error messages when no support +- settings page diff --git a/src/game-loop/game-loop-settings.ts b/src/game-loop/game-loop-settings.ts index de3189a..9f1cd1e 100644 --- a/src/game-loop/game-loop-settings.ts +++ b/src/game-loop/game-loop-settings.ts @@ -1,7 +1,5 @@ export interface GameLoopSettings { agentCount: number; - startingRadius: number; - renderSpeed: number; simulatedDelayMs: number; } diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index 5f20596..91aadb8 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -1,4 +1,6 @@ +import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline'; import { AgentPipeline } from '../pipelines/agents/agent-pipeline'; +import { spawnAgents } from '../pipelines/agents/spawn-agents'; import { BrushPipeline } from '../pipelines/brush/brush-pipeline'; import { CommonState } from '../pipelines/common-state/common-state'; import { CopyPipeline } from '../pipelines/copy/copy-pipeline'; @@ -8,7 +10,6 @@ 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'; import { vec2 } from 'gl-matrix'; @@ -19,6 +20,7 @@ export default class GameLoop { private readonly trailMapB: ResizableTexture; private readonly commonState: CommonState; private readonly copyPipeline: CopyPipeline; + private readonly agentGenerationPipeline: AgentGenerationPipeline; private readonly agentPipeline: AgentPipeline; private readonly renderPipeline: RenderPipeline; private readonly brushPipeline: BrushPipeline; @@ -48,18 +50,25 @@ export default class GameLoop { this.resize(); this.commonState = new CommonState(this.device); + this.commonState.setParameters(this.canvasSize, 0, 0); + this.copyPipeline = new CopyPipeline(this.device); + + this.agentGenerationPipeline = new AgentGenerationPipeline( + this.device, + this.commonState + ); + this.agentPipeline = new AgentPipeline( this.device, - spawnAgents(this.canvasSize, settings.agentCount), - this.commonState + this.commonState, + this.agentGenerationPipeline.generateAgents(settings.agentCount) ); 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)); - canvas.addEventListener('mousemove', this.onSwipe.bind(this)); canvas.addEventListener('mousedown', (e) => { this.brushPipeline.clearSwipes(); @@ -90,7 +99,6 @@ export default class GameLoop { } private resize() { - const devicePixelRatio = window.devicePixelRatio || 1; this.canvas.width = this.canvas.clientWidth * this.devicePixelRatio; this.canvas.height = this.canvas.clientHeight * this.devicePixelRatio; } diff --git a/src/game-loop/spawn-agents.ts b/src/game-loop/spawn-agents.ts deleted file mode 100644 index 7420a3b..0000000 --- a/src/game-loop/spawn-agents.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 center = vec2.scale(vec2.create(), canvasSize, 0.5); - - return new Array(agentCount).fill(0).map(() => { - const radius = Random.randomBetween(0, minSize * settings.startingRadius); - const angle = Random.randomBetween(0, Math.PI * 2); - - const delta = vec2.fromValues(Math.cos(angle) * radius, Math.sin(angle) * radius); - - 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/pipelines/agents/agent-generation/agent-generation-pipeline.ts b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts new file mode 100644 index 0000000..a1433cd --- /dev/null +++ b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts @@ -0,0 +1,79 @@ +import random from '../../../utils/graphics/random.wgsl'; +import { smartCompile } from '../../../utils/graphics/smart-compile'; +import { CommonState } from '../../common-state/common-state'; +import { AGENT_SIZE_IN_BYTES, Agent } from '../agent'; +import shader from './agent-generation.wgsl'; +import agentSchema from './agent-schema.wgsl'; + +export class AgentGenerationPipeline { + private static readonly WORKGROUP_SIZE = 64; + + private readonly bindGroupLayout: GPUBindGroupLayout; + private readonly pipeline: GPUComputePipeline; + + private bindGroup?: GPUBindGroup; + + public constructor( + private readonly device: GPUDevice, + private readonly commonState: CommonState + ) { + this.bindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + ], + }); + + this.pipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], + }), + compute: { + module: smartCompile(device, CommonState.shaderCode, random, agentSchema, shader), + entryPoint: 'main', + }, + }); + } + + public generateAgents(agentCount: number): GPUBuffer { + if (agentCount <= 0 || agentCount != Math.floor(agentCount)) { + throw new Error('Agent count must be a positive integer'); + } + + const agentsBuffer = this.device.createBuffer({ + size: agentCount * AGENT_SIZE_IN_BYTES, + usage: GPUBufferUsage.STORAGE, + }); + + this.bindGroup = this.device.createBindGroup({ + layout: this.bindGroupLayout, + entries: [ + { + binding: 1, + resource: { + buffer: agentsBuffer, + }, + }, + ], + }); + + const commandEncoder = this.device.createCommandEncoder(); + + const passEncoder = commandEncoder.beginComputePass(); + passEncoder.setPipeline(this.pipeline); + this.commonState.execute(passEncoder); + passEncoder.setBindGroup(1, this.bindGroup); + passEncoder.dispatchWorkgroups( + Math.ceil(agentCount / AgentGenerationPipeline.WORKGROUP_SIZE) + ); + passEncoder.end(); + + this.device.queue.submit([commandEncoder.finish()]); + return agentsBuffer; + } +} diff --git a/src/pipelines/agents/agent-generation/agent-generation.wgsl b/src/pipelines/agents/agent-generation/agent-generation.wgsl new file mode 100644 index 0000000..69ceac9 --- /dev/null +++ b/src/pipelines/agents/agent-generation/agent-generation.wgsl @@ -0,0 +1,26 @@ +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let id = global_id.x; + + if (id >= arrayLength(&agents)) { + return; + } + + let position = vec2( + hash(id) * state.size.x, + hash(id * id) * state.size.y, + ); + + let center = state.size / 2.0; + + let direction = position - center; + let angle = atan2(direction.y, direction.x); + + agents[id] = Agent( + position, + angle, + 0, + 1000000, + 0 + ); +} diff --git a/src/pipelines/agents/agent-generation/agent-schema.wgsl b/src/pipelines/agents/agent-generation/agent-schema.wgsl new file mode 100644 index 0000000..2663ea6 --- /dev/null +++ b/src/pipelines/agents/agent-generation/agent-schema.wgsl @@ -0,0 +1,9 @@ +struct Agent { + position: vec2, + angle: f32, + species: f32, + timeToLive: f32, + timeToLive2: f32, +} + +@group(1) @binding(1) var agents: array; diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index 5096ec1..bdfa1ba 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -1,6 +1,8 @@ +import random from '../../utils/graphics/random.wgsl'; import { smartCompile } from '../../utils/graphics/smart-compile'; import { CommonState } from '../common-state/common-state'; import { AGENT_SIZE_IN_BYTES, Agent } from './agent'; +import agentSchme from './agent-generation/agent-schema.wgsl'; import { AgentSettings } from './agent-settings'; import shader from './agent.wgsl'; @@ -11,7 +13,6 @@ export class AgentPipeline { private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPUComputePipeline; private readonly uniforms: GPUBuffer; - private readonly agentsBuffer: GPUBuffer; private bindGroup?: GPUBindGroup; private previousTrailMapIn?: GPUTextureView; @@ -19,13 +20,9 @@ export class AgentPipeline { public constructor( private readonly device: GPUDevice, - agents: Array, - private readonly commonState: CommonState + private readonly commonState: CommonState, + private readonly agentsBuffer: GPUBuffer ) { - if (agents.length === 0) { - throw new Error('No agents provided'); - } - this.bindGroupLayout = device.createBindGroupLayout(AgentPipeline.bindGroupLayout); this.pipeline = device.createComputePipeline({ @@ -33,7 +30,7 @@ export class AgentPipeline { bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], }), compute: { - module: smartCompile(device, CommonState.shaderCode, shader), + module: smartCompile(device, CommonState.shaderCode, random, agentSchme, shader), entryPoint: 'main', }, }); @@ -42,23 +39,6 @@ export class AgentPipeline { size: AgentPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - - this.agentsBuffer = device.createBuffer({ - size: agents.length * AGENT_SIZE_IN_BYTES, - usage: GPUBufferUsage.STORAGE, - mappedAtCreation: true, - }); - - new Float32Array(this.agentsBuffer.getMappedRange()).set( - agents.flatMap((agent) => [ - ...agent.position, - agent.angle, - 0, // padding - agent.species, - agent.timeToLive, - ]) - ); - this.agentsBuffer.unmap(); } public setParameters({ @@ -81,25 +61,6 @@ export class AgentPipeline { ); } - public executeRenderPass( - 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(); - } - public execute( commandEncoder: GPUCommandEncoder, trailMapIn: GPUTextureView, diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index c063fb9..eaab47e 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -1,10 +1,3 @@ -struct Agent { - position: vec2, - angle: f32, - species: f32, - timeToLive: f32 -} - struct Settings { brushTrailWeight: f32, moveRate: f32, @@ -14,7 +7,6 @@ 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; @@ -28,31 +20,31 @@ 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; - // return; - } + // 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; + // return; + // } - let random = random_with_seed(agent.position, f32(id) + state.time); + let random = hash(id + u32(state.time * 16732.0)); let trailCurrent = textureLoad(trailMapIn, vec2(agent.position), 0); - var weight: f32; - if(agent.species == 0) { - weight = trailCurrent.r - trailCurrent.g; - } else { - weight = trailCurrent.g - trailCurrent.r; - } - if (weight < 0) { - agent.timeToLive = 0; - return; - } + // var weight: f32; + // if(agent.species == 0) { + // weight = trailCurrent.r - trailCurrent.g; + // } else { + // weight = trailCurrent.g - trailCurrent.r; + // } + // if (weight < 0) { + // agent.timeToLive = 0; + // return; + // } let trailForward = sense(agent.position, agent.angle, settings.sensorOffset, 0); @@ -111,7 +103,3 @@ fn sense(agentPosition: vec2, agentAngle: f32, sensorOffset: f32, sensorOff 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/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 063bcdf..7371f7e 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -12,37 +12,29 @@ struct Settings { @fragment fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { var current = textureSample(trailMap, Sampler, uv); - - let neighbours: vec4 = ( - 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 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 / state.size); - let random = textureSample(noise, noiseSampler, uv + offset / state.size * 0.5).r; - - let difference = neighbour - current; - change += vec4( - length(neighbour.rgb) * pow(random, settings.diffusionRateTrails) * difference.rgb, - min(1.0, length(neighbour.a)) * pow(random, settings.diffusionRateBrush) * difference.a - ); - } + 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 / state.size); + let random = textureSample(noise, noiseSampler, uv + offset / state.size * 0.5).r; + + let difference = clamp(neighbour - current, vec4(0), vec4(1)); + change += vec4( + length(neighbour.rgb) * pow(random, settings.diffusionRateTrails) * difference.rgb, + min(1.0, length(neighbour.a)) * pow(random, settings.diffusionRateBrush) * difference.a + ); } } - current += change / 4; + } + + current += change / 4; let decayed = vec4( current.rgb * settings.decayRateTrails, current.a * settings.decayRateBrush ); - return clamp(decayed, vec4(0), vec4(1)); + return decayed; } diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts index 3c38e9a..d3bed53 100644 --- a/src/pipelines/render/render-pipeline.ts +++ b/src/pipelines/render/render-pipeline.ts @@ -6,7 +6,7 @@ import { RenderSettings } from './render-settings'; import shader from './render.wgsl'; export class RenderPipeline { - private static readonly UNIFORM_COUNT = 12; + private static readonly UNIFORM_COUNT = 13; private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPURenderPipeline; @@ -51,7 +51,12 @@ export class RenderPipeline { }); } - public setParameters({ brushColor, speciesColorA, speciesColorB }: RenderSettings) { + public setParameters({ + brushColor, + speciesColorA, + speciesColorB, + clarity, + }: RenderSettings) { this.device.queue.writeBuffer( this.uniforms, 0, @@ -61,7 +66,7 @@ export class RenderPipeline { ...speciesColorA, 0, //padding ...speciesColorB, - 0, //padding + clarity, ]) ); } diff --git a/src/pipelines/render/render-settings.ts b/src/pipelines/render/render-settings.ts index 99ff690..8774148 100644 --- a/src/pipelines/render/render-settings.ts +++ b/src/pipelines/render/render-settings.ts @@ -4,4 +4,5 @@ export interface RenderSettings { brushColor: vec3; speciesColorA: vec3; speciesColorB: vec3; + clarity: number; } diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl index 9fcd9d2..1506fdc 100644 --- a/src/pipelines/render/render.wgsl +++ b/src/pipelines/render/render.wgsl @@ -2,6 +2,7 @@ struct Settings { brushColor: vec3, speciesColorA: vec3, speciesColorB: vec3, + clarity: f32, }; @group(1) @binding(0) var settings: Settings; @@ -13,19 +14,17 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 { let traces = textureSample(trailMap, Sampler, uv); let random = textureSample(noise, noiseSampler, uv); + let backgroundColor = vec3(0.9) + 0.075 * random.r; + let speciesAStrength = traces.r; let speciesBStrength = traces.g; let brushStrength = traces.a; let rgbColor = sqrt(vec3( - settings.speciesColorA * clamp(speciesAStrength, 0, 1) + - settings.speciesColorB * clamp(speciesBStrength, 0, 1) + + settings.speciesColorA * clamp(pow(speciesAStrength, settings.clarity), 0, 1) + + settings.speciesColorB * clamp(pow(speciesBStrength, settings.clarity), 0, 1) + settings.brushColor * brushStrength )); - - let bg = vec3(0.9) + 0.075 * random.r; - - - return vec4(bg - rgbColor, 1); + return vec4(backgroundColor - rgbColor, 1); } diff --git a/src/settings.ts b/src/settings.ts index f2ed019..761575c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -19,8 +19,7 @@ export const settings: GameLoopSettings & BrushSettings & DiffusionSettings & RenderSettings = { - agentCount: 1_000_000, - startingRadius: 0.15, + agentCount: 4_000_000, renderSpeed: 1, simulatedDelayMs: 0, @@ -34,12 +33,13 @@ export const settings: GameLoopSettings & sensorOffsetAngle: 30, sensorOffsetDistance: 60, - diffusionRateTrails: 4, + diffusionRateTrails: 0.4, // inverse decayRateTrails: 0.9, - diffusionRateBrush: 4, + diffusionRateBrush: 4, // inverse decayRateBrush: 0.98, brushColor: palette.blue, speciesColorA: palette.yellow, speciesColorB: palette.purple, + clarity: 3, }; diff --git a/src/utils/graphics/random.wgsl b/src/utils/graphics/random.wgsl index 8d7f78d..e182c16 100644 --- a/src/utils/graphics/random.wgsl +++ b/src/utils/graphics/random.wgsl @@ -5,3 +5,14 @@ fn random_with_seed(uv: vec2, seed: f32) -> f32 { fn random(uv: vec2) -> f32 { return fract(sin(dot(uv, vec2(12.9898, 78.233)))* 43758.5453123); } + +fn hash(state0 : u32) -> f32 { + var state : u32 = state0; + state = state ^ 2747636419u; + state = state * 2654435769u; + state = state ^ (state >> 16u); + state = state * 2654435769u; + state = state ^ (state >> 16u); + state = state * 2654435769u; + return f32(state) / 4294967295.0; +}