From 7e8ca4b16f248fb81603046c5f0d3573aad0145d Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 21 May 2023 11:03:37 +0100 Subject: [PATCH] Generate agents --- src/game-loop/game-loop-settings.ts | 4 + src/game-loop/game-loop.ts | 85 +++++++--- src/game-loop/game-rules.ts | 78 +++++++++ .../agent-generation-pipeline.ts | 152 +++++++++++++++--- .../agent-generation/agent-generation.wgsl | 58 +++++-- .../agent-generation/generation-counts.ts | 4 + src/pipelines/agents/agent-pipeline.ts | 9 +- src/pipelines/agents/agent.wgsl | 49 +++--- src/pipelines/common-state/common-state.ts | 10 +- src/settings.ts | 12 +- 10 files changed, 370 insertions(+), 91 deletions(-) create mode 100644 src/game-loop/game-rules.ts create mode 100644 src/pipelines/agents/agent-generation/generation-counts.ts diff --git a/src/game-loop/game-loop-settings.ts b/src/game-loop/game-loop-settings.ts index 9f1cd1e..507feaa 100644 --- a/src/game-loop/game-loop-settings.ts +++ b/src/game-loop/game-loop-settings.ts @@ -1,5 +1,9 @@ export interface GameLoopSettings { agentCount: number; + initialDeadRatio: number; renderSpeed: number; simulatedDelayMs: number; + + aggressionFactor: number; + nextGenerationSpawnRadius: number; } diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index d951e71..9cd68e4 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -1,4 +1,5 @@ import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline'; +import { GenerationCounts } from '../pipelines/agents/agent-generation/generation-counts'; import { AgentPipeline } from '../pipelines/agents/agent-pipeline'; import { BrushPipeline } from '../pipelines/brush/brush-pipeline'; import { CommonState } from '../pipelines/common-state/common-state'; @@ -7,16 +8,17 @@ 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 { initializeContext } from '../utils/graphics/initialize-context'; import { ResizableTexture } from '../utils/graphics/resizable-texture'; import { sleep } from '../utils/sleep'; +import { GameRules } from './game-rules'; 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 agentGenerationPipeline: AgentGenerationPipeline; @@ -25,6 +27,8 @@ export default class GameLoop { private readonly brushPipeline: BrushPipeline; private readonly diffusionPipeline: DiffusionPipeline; + private readonly gameRules = new GameRules(performance.now() / 1000); + private hasFinished = false; private readonly hasFinishedPromise: Promise = new Promise( (resolve) => (this.resolveHasFinished = resolve) @@ -35,33 +39,34 @@ export default class GameLoop { public constructor( private readonly canvas: HTMLCanvasElement, - private readonly device: GPUDevice + private readonly device: GPUDevice, + private readonly deltaTimeCalculator: DeltaTimeCalculator ) { - const context = this.canvas.getContext('webgpu') as any as GPUCanvasContext; - context.configure({ - device: this.device, - format: navigator.gpu.getPreferredCanvasFormat(), - alphaMode: 'premultiplied', - }); + const context = initializeContext({ device, canvas }); this.trailMapA = new ResizableTexture(this.device, this.canvasSize); this.trailMapB = new ResizableTexture(this.device, this.canvasSize); this.resize(); this.commonState = new CommonState(this.device); - this.commonState.setParameters(this.canvasSize, 0, 0); + this.commonState.setParameters({ + canvasSize: this.canvasSize, + time: 0, + deltaTime: 0, + }); this.copyPipeline = new CopyPipeline(this.device); this.agentGenerationPipeline = new AgentGenerationPipeline( this.device, - this.commonState + this.commonState, + settings.agentCount ); this.agentPipeline = new AgentPipeline( this.device, this.commonState, - this.agentGenerationPipeline.generateAgents(settings.agentCount) + this.agentGenerationPipeline.agentsBuffer ); this.brushPipeline = new BrushPipeline(this.device, this.commonState); this.diffusionPipeline = new DiffusionPipeline(this.device, this.commonState); @@ -85,6 +90,10 @@ export default class GameLoop { return this.hasFinishedPromise; } + public get aliveAgentCounts(): GenerationCounts { + return this.gameRules.generationCounts; + } + private onSwipe(event: MouseEvent) { if (!this.isSwipeActive) { return; @@ -104,23 +113,56 @@ export default class GameLoop { private async render(time: DOMHighResTimeStamp) { if (this.hasFinished) { + this.resolveHasFinished(); return; } const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time); - this.commonState.setParameters(this.canvasSize, deltaTime, time); + time *= settings.renderSpeed; + const timeInSeconds = time / 1000; [ + this.commonState, this.agentPipeline, this.brushPipeline, this.diffusionPipeline, this.renderPipeline, - ].forEach((pipeline) => pipeline.setParameters(settings)); - - const commandEncoder = this.device.createCommandEncoder(); + ].forEach((pipeline) => + pipeline.setParameters({ + time, + nextGenerationAggression: this.gameRules.nextGenerationAgression, + deltaTime, + canvasSize: this.canvasSize, + ...settings, + }) + ); for (let i = 0; i < settings.renderSpeed; i++) { + const commandEncoder = this.device.createCommandEncoder(); + + if ( + this.gameRules.generationCounts.currentGenerationCount == 0 && + this.gameRules.generationCounts.nextGenerationCount == 0 + ) { + this.gameRules.updateGenerationCounts( + await this.agentGenerationPipeline.spawnNextGenerationCover( + 0, + settings.agentCount * (1 - settings.initialDeadRatio) + ) + ); + } + + const spawnAction = this.gameRules.getSpawnAction(timeInSeconds, this.canvasSize); + this.gameRules.updateGenerationCounts( + await this.agentGenerationPipeline.spawnNextGenerationCircle( + spawnAction.generation, + spawnAction.count, + spawnAction.position, + settings.nextGenerationSpawnRadius + ) + ); + this.copyPipeline.execute( commandEncoder, this.trailMapA.getTextureView(), @@ -138,9 +180,9 @@ export default class GameLoop { this.trailMapA.getTextureView() ); this.renderPipeline.execute(commandEncoder, this.trailMapA.getTextureView()); - } - this.device.queue.submit([commandEncoder.finish()]); + this.device.queue.submit([commandEncoder.finish()]); + } if (!this.isSwipeActive) { this.brushPipeline.clearSwipes(); @@ -157,20 +199,19 @@ export default class GameLoop { requestAnimationFrame(this.render.bind(this)); } - public destroy() { + public async destroy() { this.hasFinished = true; + await this.hasFinishedPromise; this.copyPipeline?.destroy(); + this.agentGenerationPipeline?.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 { diff --git a/src/game-loop/game-rules.ts b/src/game-loop/game-rules.ts new file mode 100644 index 0000000..599763a --- /dev/null +++ b/src/game-loop/game-rules.ts @@ -0,0 +1,78 @@ +import { GenerationCounts } from '../pipelines/agents/agent-generation/generation-counts'; +import { settings } from '../settings'; +import { clamp01 } from '../utils/clamp'; +import { Random } from '../utils/random'; + +import { vec2 } from 'gl-matrix'; + +export interface SpawnAction { + generation: number; + position: vec2; + count: number; +} + +export class GameRules { + private static SPAWN_INTERVAL = 3; + + private lastSpawnTimeInSeconds = 0; + private nextGenerationId = 0; + public generationCounts: GenerationCounts = { + currentGenerationCount: 0, + nextGenerationCount: 0, + }; + + public constructor(startingTimeInSeconds: number) { + this.lastSpawnTimeInSeconds = startingTimeInSeconds; + } + + public getSpawnAction(timeInSeconds: number, canvasSize: vec2): SpawnAction { + if (timeInSeconds - this.lastSpawnTimeInSeconds < GameRules.SPAWN_INTERVAL) { + return { + generation: this.nextGenerationId, + position: vec2.create(), + count: 0, + }; + } + + this.lastSpawnTimeInSeconds = timeInSeconds; + + return { + generation: this.nextGenerationId, + position: vec2.fromValues( + Random.randomBetween(0, canvasSize.x), + Random.randomBetween(0, canvasSize.y) + ), + count: + settings.agentCount - + this.generationCounts.nextGenerationCount - + this.generationCounts.currentGenerationCount, + }; + } + + public updateGenerationCounts({ + currentGenerationCount, + nextGenerationCount, + }: GenerationCounts): void { + if (currentGenerationCount === 0) { + this.nextGenerationId++; + } + + this.generationCounts = { + currentGenerationCount, + nextGenerationCount, + }; + } + + public get nextGenerationAgression(): number { + if (this.generationCounts.currentGenerationCount === 0) { + return 0; + } + + return clamp01( + (this.generationCounts.nextGenerationCount / + this.generationCounts.currentGenerationCount - + 1) * + settings.aggressionFactor + ); + } +} diff --git a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts index e277e30..8710511 100644 --- a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts +++ b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts @@ -4,21 +4,42 @@ 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'; +import { GenerationCounts } from './generation-counts'; + +import { vec2 } from 'gl-matrix'; export class AgentGenerationPipeline { private static readonly WORKGROUP_SIZE = 64; + private static readonly UNIFORM_COUNT = 6; + private static readonly COUNTER_COUNT = 3; private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPUComputePipeline; + private readonly uniforms: GPUBuffer; + private readonly bindGroup: GPUBindGroup; - private bindGroup?: GPUBindGroup; + public readonly agentsBuffer: GPUBuffer; + public readonly countersBuffer: GPUBuffer; + public readonly countersStagingBuffer: GPUBuffer; public constructor( private readonly device: GPUDevice, - private readonly commonState: CommonState + private readonly commonState: CommonState, + private readonly agentCount: number ) { + if (agentCount <= 0 || agentCount != Math.floor(agentCount)) { + throw new Error('Agent count must be a positive integer'); + } + this.bindGroupLayout = device.createBindGroupLayout({ entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, { binding: 1, visibility: GPUShaderStage.COMPUTE, @@ -26,6 +47,57 @@ export class AgentGenerationPipeline { type: 'storage', }, }, + { + binding: 2, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + ], + }); + + this.agentsBuffer = this.device.createBuffer({ + size: agentCount * AGENT_SIZE_IN_BYTES, + usage: GPUBufferUsage.STORAGE, + }); + + this.countersBuffer = this.device.createBuffer({ + size: AgentGenerationPipeline.COUNTER_COUNT * Int32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + }); + + this.countersStagingBuffer = this.device.createBuffer({ + size: AgentGenerationPipeline.COUNTER_COUNT * Int32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, + }); + + this.uniforms = this.device.createBuffer({ + size: AgentGenerationPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + this.bindGroup = this.device.createBindGroup({ + layout: this.bindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: this.uniforms, + }, + }, + { + binding: 1, + resource: { + buffer: this.agentsBuffer, + }, + }, + { + binding: 2, + resource: { + buffer: this.countersBuffer, + }, + }, ], }); @@ -40,28 +112,39 @@ export class AgentGenerationPipeline { }); } - public generateAgents(agentCount: number): GPUBuffer { - if (agentCount <= 0 || agentCount != Math.floor(agentCount)) { - throw new Error('Agent count must be a positive integer'); - } + public async spawnNextGenerationCover( + generationId: number, + count: number + ): Promise { + this.device.queue.writeBuffer( + this.uniforms, + 0, + new Float32Array([0, 0, 0, generationId, 0]) + ); - const agentsBuffer = this.device.createBuffer({ - size: agentCount * AGENT_SIZE_IN_BYTES, - usage: GPUBufferUsage.STORAGE, - }); + this.device.queue.writeBuffer(this.countersBuffer, 0, new Int32Array([0, 0, count])); - this.bindGroup = this.device.createBindGroup({ - layout: this.bindGroupLayout, - entries: [ - { - binding: 1, - resource: { - buffer: agentsBuffer, - }, - }, - ], - }); + return this.execute(); + } + public async spawnNextGenerationCircle( + generationId: number, + count: number, + center: vec2, + radius: number + ): Promise { + this.device.queue.writeBuffer( + this.uniforms, + 0, + new Float32Array([...center, radius, generationId, 1]) + ); + + this.device.queue.writeBuffer(this.countersBuffer, 0, new Int32Array([0, 0, count])); + + return this.execute(); + } + + private async execute(): Promise { const commandEncoder = this.device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); @@ -69,11 +152,34 @@ export class AgentGenerationPipeline { this.commonState.execute(passEncoder); passEncoder.setBindGroup(1, this.bindGroup); passEncoder.dispatchWorkgroups( - Math.ceil(agentCount / AgentGenerationPipeline.WORKGROUP_SIZE) + Math.ceil(this.agentCount / AgentGenerationPipeline.WORKGROUP_SIZE) ); passEncoder.end(); + commandEncoder.copyBufferToBuffer( + this.countersBuffer, + 0, + this.countersStagingBuffer, + 0, + AgentGenerationPipeline.COUNTER_COUNT * Int32Array.BYTES_PER_ELEMENT + ); + this.device.queue.submit([commandEncoder.finish()]); - return agentsBuffer; + + await this.countersStagingBuffer.mapAsync(GPUMapMode.READ); + + const data = new Int32Array(this.countersStagingBuffer.getMappedRange().slice(0)); + this.countersStagingBuffer.unmap(); + return { + currentGenerationCount: data[0], + nextGenerationCount: data[1], + }; + } + + public destroy() { + this.uniforms.destroy(); + this.countersBuffer.destroy(); + this.countersStagingBuffer.destroy(); + this.agentsBuffer.destroy(); } } diff --git a/src/pipelines/agents/agent-generation/agent-generation.wgsl b/src/pipelines/agents/agent-generation/agent-generation.wgsl index 8c28fc6..4dafeb7 100644 --- a/src/pipelines/agents/agent-generation/agent-generation.wgsl +++ b/src/pipelines/agents/agent-generation/agent-generation.wgsl @@ -1,25 +1,65 @@ +struct Settings { + center: vec2, + radius: f32, + nextGenerationId: f32, + shape: f32, +}; + +struct Counters { + currentGenerationAlive: atomic, + nextGenerationAlive: atomic, + remaining: atomic +}; + +@group(1) @binding(0) var settings: Settings; +@group(1) @binding(2) var counters: Counters; + @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) global_id: vec3) { let id = global_id.x; - if (id >= arrayLength(&agents)) { + if id >= arrayLength(&agents) { return; } - let position = vec2( - hash(id) * state.size.x, - hash(id * id) * state.size.y, - ); + if agents[id].timeToLive > 0 { + if agents[id].species == settings.nextGenerationId { + atomicAdd(&counters.nextGenerationAlive, 1); + } else { + atomicAdd(&counters.currentGenerationAlive, 1); + } + return; + } - let center = state.size / 2.0; + if atomicSub(&counters.remaining, 1) <= 0 { + return; + } - let direction = position - center; - let angle = atan2(direction.y, direction.x); + var position: vec2; + var angle: f32; + + if settings.shape == 0.0 { + position = vec2( + hash(id) * state.size.x, + hash(id * id) * state.size.y, + ); + + let center = state.size / 2.0; + let direction = position - center; + angle = atan2(direction.y, direction.x); + } else if settings.shape == 1.0 { + angle = hash(id) * 2.0 * 3.1415; + let direction = vec2(cos(angle), sin(angle)); + position = settings.center + direction * settings.radius * hash(id * 12 + 3); + } + + atomicAdd(&counters.nextGenerationAlive, 1); agents[id] = Agent( position, angle, - 0, + settings.nextGenerationId, 1000000, ); + } diff --git a/src/pipelines/agents/agent-generation/generation-counts.ts b/src/pipelines/agents/agent-generation/generation-counts.ts new file mode 100644 index 0000000..8dc53f4 --- /dev/null +++ b/src/pipelines/agents/agent-generation/generation-counts.ts @@ -0,0 +1,4 @@ +export interface GenerationCounts { + currentGenerationCount: number; + nextGenerationCount: number; +} diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index 7658c74..7b3c30c 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -8,7 +8,7 @@ import shader from './agent.wgsl'; export class AgentPipeline { private static readonly WORKGROUP_SIZE = 64; - private static readonly UNIFORM_COUNT = 5; + private static readonly UNIFORM_COUNT = 6; private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPUComputePipeline; @@ -21,7 +21,7 @@ export class AgentPipeline { public constructor( private readonly device: GPUDevice, private readonly commonState: CommonState, - private readonly agentsBuffer: GPUBuffer + private readonly agentsBuffer: GPUBuffer // doesn't get destroyed ) { this.bindGroupLayout = device.createBindGroupLayout(AgentPipeline.bindGroupLayout); @@ -47,7 +47,8 @@ export class AgentPipeline { turnSpeed, sensorOffsetAngle, sensorOffsetDistance, - }: AgentSettings) { + nextGenerationAggression, + }: AgentSettings & { nextGenerationAggression: number }) { this.device.queue.writeBuffer( this.uniforms, 0, @@ -57,6 +58,7 @@ export class AgentPipeline { turnSpeed, (sensorOffsetAngle * Math.PI) / 180, sensorOffsetDistance, + nextGenerationAggression, ]) ); } @@ -118,7 +120,6 @@ export class AgentPipeline { public destroy() { this.uniforms.destroy(); - this.agentsBuffer.destroy(); } private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor { diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 3b568fb..1d94e19 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -4,6 +4,8 @@ struct Settings { turnRate: f32, sensorAngle: f32, sensorOffset: f32, + nextGenerationAggression: f32, + // nextGenerationParity: f32, }; @group(1) @binding(0) var settings: Settings; @@ -14,38 +16,29 @@ struct Settings { fn main(@builtin(global_invocation_id) global_id: vec3) { let id = global_id.x; - if (id >= arrayLength(&agents)) { + if (id >= arrayLength(&agents) || agents[id].timeToLive <= 0) { return; } 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; - // } - - let random = hash(id + u32(state.time * 16732.0)); + let random = hash(id + u32(state.time % 107 * 1673.7)); 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) { + if trailCurrent.r < trailCurrent.g { + agent.species = 1; + agents[id] = agent; + return; + } + } else { + if trailCurrent.g < trailCurrent.r { + agent.timeToLive = 0; + agents[id] = agent; + return; + } + } let trailForward = sense(agent.position, agent.angle, settings.sensorOffset, 0); let trailLeft = sense(agent.position, agent.angle, settings.sensorOffset, settings.sensorAngle); @@ -59,9 +52,9 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { weightLeft += trailLeft.r - trailLeft.g; weightRight += trailRight.r - trailRight.g; } else { - weightForward += trailForward.g - trailForward.r; - weightLeft += trailLeft.g - trailLeft.r; - weightRight += trailRight.g - trailRight.r; + weightForward += trailForward.g + trailForward.r * settings.nextGenerationAggression; + weightLeft += trailLeft.g + trailLeft.r * settings.nextGenerationAggression; + weightRight += trailRight.g + trailRight.r * settings.nextGenerationAggression; } var rotation: f32 = 0; diff --git a/src/pipelines/common-state/common-state.ts b/src/pipelines/common-state/common-state.ts index 6de804d..b70536e 100644 --- a/src/pipelines/common-state/common-state.ts +++ b/src/pipelines/common-state/common-state.ts @@ -86,7 +86,15 @@ export class CommonState { }); } - public setParameters(canvasSize: vec2, deltaTime: number, time: number) { + public setParameters({ + canvasSize, + deltaTime, + time, + }: { + canvasSize: vec2; + deltaTime: number; + time: number; + }) { this.device.queue.writeBuffer( this.uniforms, 0, diff --git a/src/settings.ts b/src/settings.ts index 17eb480..9044db5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -19,9 +19,13 @@ export const settings: GameLoopSettings & BrushSettings & DiffusionSettings & RenderSettings = { - agentCount: 4_000_000, + agentCount: 4_000_000, // requires restart + initialDeadRatio: 0.2, // requires restart - renderSpeed: 1, + aggressionFactor: 0.5, // requires restart + nextGenerationSpawnRadius: 50, + + renderSpeed: 5, simulatedDelayMs: 0, brushWidth: 20, @@ -39,7 +43,7 @@ export const settings: GameLoopSettings & decayRateBrush: 0.995, // inverse brushColor: palette.blue, - speciesColorA: palette.yellow, - speciesColorB: palette.purple, + speciesAColor: palette.yellow, + speciesBColor: palette.purple, clarity: 3, };