From 3414f38c3abbfb0ba50ee8599005354616e786d0 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 21 May 2023 13:51:09 +0100 Subject: [PATCH] Add cancer --- src/game-loop/game-loop-settings.ts | 2 +- src/game-loop/game-loop.ts | 53 ++++--- src/game-loop/game-presentation.ts | 11 ++ src/game-loop/game-rules.ts | 32 +++-- .../agent-generation/agent-counting.wgsl | 21 +++ .../agent-first-generation.wgsl | 21 +++ .../agent-generation-pipeline.ts | 131 ++++++++++++------ .../agent-generation/agent-generation.wgsl | 49 +------ .../agents/agent-generation/agent-schema.wgsl | 1 - .../agents/agent-generation/agent.ts | 3 +- .../agent-generation/generation-counts.ts | 4 +- src/pipelines/agents/agent-pipeline.ts | 16 ++- src/pipelines/agents/agent.wgsl | 66 +++++---- src/utils/hash.ts | 9 ++ 14 files changed, 258 insertions(+), 161 deletions(-) create mode 100644 src/game-loop/game-presentation.ts create mode 100644 src/pipelines/agents/agent-generation/agent-counting.wgsl create mode 100644 src/pipelines/agents/agent-generation/agent-first-generation.wgsl create mode 100644 src/utils/hash.ts diff --git a/src/game-loop/game-loop-settings.ts b/src/game-loop/game-loop-settings.ts index 507feaa..b40b172 100644 --- a/src/game-loop/game-loop-settings.ts +++ b/src/game-loop/game-loop-settings.ts @@ -1,9 +1,9 @@ export interface GameLoopSettings { agentCount: number; - initialDeadRatio: number; renderSpeed: number; simulatedDelayMs: number; aggressionFactor: number; nextGenerationSpawnRadius: number; + nextGenerationSpawnInterval: number; } diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index 9cd68e4..10aae19 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -48,6 +48,8 @@ export default class GameLoop { this.trailMapB = new ResizableTexture(this.device, this.canvasSize); this.resize(); + this.copyPipeline = new CopyPipeline(this.device); + this.commonState = new CommonState(this.device); this.commonState.setParameters({ canvasSize: this.canvasSize, @@ -55,13 +57,12 @@ export default class GameLoop { deltaTime: 0, }); - this.copyPipeline = new CopyPipeline(this.device); - this.agentGenerationPipeline = new AgentGenerationPipeline( this.device, this.commonState, settings.agentCount ); + this.agentGenerationPipeline.spawnFirstGeneration(); this.agentPipeline = new AgentPipeline( this.device, @@ -87,10 +88,23 @@ export default class GameLoop { public async start(): Promise { requestAnimationFrame(this.render.bind(this)); + requestAnimationFrame(this.updateCounts.bind(this)); return this.hasFinishedPromise; } - public get aliveAgentCounts(): GenerationCounts { + private async updateCounts(): Promise { + if (this.hasFinished) { + return; + } + const generationCounts = await this.agentGenerationPipeline.countAgents(); + this.gameRules.updateGenerationCounts(generationCounts); + requestAnimationFrame(this.updateCounts.bind(this)); + } + + public get aliveAgentCounts(): { + currentGenerationCount: number; + nextGenerationCount: number; + } { return this.gameRules.generationCounts; } @@ -131,7 +145,15 @@ export default class GameLoop { ].forEach((pipeline) => pipeline.setParameters({ time, - nextGenerationAggression: this.gameRules.nextGenerationAgression, + evenGenerationAggression: + this.gameRules.nextGenerationId % 2 + ? -1 + : this.gameRules.nextGenerationAgression, + oddGenerationAggression: + this.gameRules.nextGenerationId % 2 + ? this.gameRules.nextGenerationAgression + : -1, + nextGenerationId: this.gameRules.nextGenerationId, deltaTime, canvasSize: this.canvasSize, ...settings, @@ -141,26 +163,11 @@ export default class GameLoop { 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.agentGenerationPipeline.spawnNextGeneration( + spawnAction.position, + spawnAction.radius, + spawnAction.generation ); this.copyPipeline.execute( diff --git a/src/game-loop/game-presentation.ts b/src/game-loop/game-presentation.ts new file mode 100644 index 0000000..8aade9c --- /dev/null +++ b/src/game-loop/game-presentation.ts @@ -0,0 +1,11 @@ +import { hsl } from '../utils/colors/hsl'; +import { hash } from '../utils/hash'; + +import { vec3 } from 'gl-matrix'; + +export class GamePresentation { + public getGenerationColor(generation: number): vec3 { + const hue = Math.round(hash(generation) * 360); + return hsl(hue, 100, 50); + } +} diff --git a/src/game-loop/game-rules.ts b/src/game-loop/game-rules.ts index 599763a..bf2d045 100644 --- a/src/game-loop/game-rules.ts +++ b/src/game-loop/game-rules.ts @@ -8,15 +8,16 @@ import { vec2 } from 'gl-matrix'; export interface SpawnAction { generation: number; position: vec2; - count: number; + radius: number; } export class GameRules { - private static SPAWN_INTERVAL = 3; - private lastSpawnTimeInSeconds = 0; - private nextGenerationId = 0; - public generationCounts: GenerationCounts = { + public nextGenerationId = 1; + public generationCounts: { + currentGenerationCount: number; + nextGenerationCount: number; + } = { currentGenerationCount: 0, nextGenerationCount: 0, }; @@ -26,11 +27,14 @@ export class GameRules { } public getSpawnAction(timeInSeconds: number, canvasSize: vec2): SpawnAction { - if (timeInSeconds - this.lastSpawnTimeInSeconds < GameRules.SPAWN_INTERVAL) { + if ( + timeInSeconds - this.lastSpawnTimeInSeconds < + settings.nextGenerationSpawnInterval + ) { return { generation: this.nextGenerationId, position: vec2.create(), - count: 0, + radius: 0, }; } @@ -42,17 +46,19 @@ export class GameRules { Random.randomBetween(0, canvasSize.x), Random.randomBetween(0, canvasSize.y) ), - count: - settings.agentCount - - this.generationCounts.nextGenerationCount - - this.generationCounts.currentGenerationCount, + radius: settings.nextGenerationSpawnRadius, }; } public updateGenerationCounts({ - currentGenerationCount, - nextGenerationCount, + evenGenerationCount, + oddGenerationCount, }: GenerationCounts): void { + const nextGenerationCount = + this.nextGenerationId % 2 === 1 ? oddGenerationCount : evenGenerationCount; + const currentGenerationCount = + this.nextGenerationId % 2 === 1 ? evenGenerationCount : oddGenerationCount; + if (currentGenerationCount === 0) { this.nextGenerationId++; } diff --git a/src/pipelines/agents/agent-generation/agent-counting.wgsl b/src/pipelines/agents/agent-generation/agent-counting.wgsl new file mode 100644 index 0000000..5394e06 --- /dev/null +++ b/src/pipelines/agents/agent-generation/agent-counting.wgsl @@ -0,0 +1,21 @@ +struct Counters { + evenGenerationAlive: atomic, + oddGenerationAlive: atomic, +}; + +@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) { + return; + } + + if agents[id].species % 2 == 0 { + atomicAdd(&counters.evenGenerationAlive, 1); + } else { + atomicAdd(&counters.oddGenerationAlive, 1); + } +} diff --git a/src/pipelines/agents/agent-generation/agent-first-generation.wgsl b/src/pipelines/agents/agent-generation/agent-first-generation.wgsl new file mode 100644 index 0000000..92b1564 --- /dev/null +++ b/src/pipelines/agents/agent-generation/agent-first-generation.wgsl @@ -0,0 +1,21 @@ +@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; + + agents[id] = Agent( + position, + atan2(direction.y, direction.x), + 0, + ); +} diff --git a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts index 8710511..4d193cf 100644 --- a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts +++ b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts @@ -2,7 +2,9 @@ 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 countingShader from './agent-counting.wgsl'; +import firstGenerationShader from './agent-first-generation.wgsl'; +import agentGenerationShader from './agent-generation.wgsl'; import agentSchema from './agent-schema.wgsl'; import { GenerationCounts } from './generation-counts'; @@ -10,14 +12,17 @@ import { vec2 } from 'gl-matrix'; export class AgentGenerationPipeline { private static readonly WORKGROUP_SIZE = 64; - private static readonly UNIFORM_COUNT = 6; + private static readonly UNIFORM_COUNT = 4; private static readonly COUNTER_COUNT = 3; private readonly bindGroupLayout: GPUBindGroupLayout; - private readonly pipeline: GPUComputePipeline; private readonly uniforms: GPUBuffer; private readonly bindGroup: GPUBindGroup; + private readonly firstGenerationPipeline: GPUComputePipeline; + private readonly nextGenerationPipeline: GPUComputePipeline; + private readonly countingPipeline: GPUComputePipeline; + public readonly agentsBuffer: GPUBuffer; public readonly countersBuffer: GPUBuffer; public readonly countersStagingBuffer: GPUBuffer; @@ -101,54 +106,98 @@ export class AgentGenerationPipeline { ], }); - this.pipeline = device.createComputePipeline({ + this.firstGenerationPipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], }), compute: { - module: smartCompile(device, CommonState.shaderCode, random, agentSchema, shader), + module: smartCompile( + device, + CommonState.shaderCode, + random, + agentSchema, + firstGenerationShader + ), + entryPoint: 'main', + }, + }); + + this.nextGenerationPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], + }), + compute: { + module: smartCompile( + device, + CommonState.shaderCode, + random, + agentSchema, + agentGenerationShader + ), + entryPoint: 'main', + }, + }); + + this.countingPipeline = device.createComputePipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], + }), + compute: { + module: smartCompile( + device, + CommonState.shaderCode, + random, + agentSchema, + countingShader + ), entryPoint: 'main', }, }); } - public async spawnNextGenerationCover( - generationId: number, - count: number - ): Promise { - this.device.queue.writeBuffer( - this.uniforms, - 0, - new Float32Array([0, 0, 0, generationId, 0]) - ); - - this.device.queue.writeBuffer(this.countersBuffer, 0, new Int32Array([0, 0, count])); - - 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 { + public spawnFirstGeneration(): void { const commandEncoder = this.device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); - passEncoder.setPipeline(this.pipeline); + this.commonState.execute(passEncoder); + passEncoder.setPipeline(this.firstGenerationPipeline); + passEncoder.setBindGroup(1, this.bindGroup); + passEncoder.dispatchWorkgroups( + Math.ceil(this.agentCount / AgentGenerationPipeline.WORKGROUP_SIZE) + ); + passEncoder.end(); + + this.device.queue.submit([commandEncoder.finish()]); + } + + public spawnNextGeneration(center: vec2, radius: number, generationId: number): void { + this.device.queue.writeBuffer( + this.uniforms, + 0, + new Float32Array([...center, radius, generationId]) + ); + + const commandEncoder = this.device.createCommandEncoder(); + + const passEncoder = commandEncoder.beginComputePass(); + this.commonState.execute(passEncoder); + passEncoder.setPipeline(this.nextGenerationPipeline); + passEncoder.setBindGroup(1, this.bindGroup); + passEncoder.dispatchWorkgroups( + Math.ceil(this.agentCount / AgentGenerationPipeline.WORKGROUP_SIZE) + ); + passEncoder.end(); + + this.device.queue.submit([commandEncoder.finish()]); + } + + public async countAgents(): Promise { + this.device.queue.writeBuffer(this.countersBuffer, 0, new Int32Array([0, 0])); + + const commandEncoder = this.device.createCommandEncoder(); + + const passEncoder = commandEncoder.beginComputePass(); + passEncoder.setPipeline(this.countingPipeline); this.commonState.execute(passEncoder); passEncoder.setBindGroup(1, this.bindGroup); passEncoder.dispatchWorkgroups( @@ -171,8 +220,8 @@ export class AgentGenerationPipeline { const data = new Int32Array(this.countersStagingBuffer.getMappedRange().slice(0)); this.countersStagingBuffer.unmap(); return { - currentGenerationCount: data[0], - nextGenerationCount: data[1], + evenGenerationCount: data[0], + oddGenerationCount: data[1], }; } diff --git a/src/pipelines/agents/agent-generation/agent-generation.wgsl b/src/pipelines/agents/agent-generation/agent-generation.wgsl index 4dafeb7..d34929e 100644 --- a/src/pipelines/agents/agent-generation/agent-generation.wgsl +++ b/src/pipelines/agents/agent-generation/agent-generation.wgsl @@ -2,17 +2,9 @@ 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) { @@ -22,44 +14,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { return; } - if agents[id].timeToLive > 0 { - if agents[id].species == settings.nextGenerationId { - atomicAdd(&counters.nextGenerationAlive, 1); - } else { - atomicAdd(&counters.currentGenerationAlive, 1); - } - return; + if length(settings.center - agents[id].position) < settings.radius { + agents[id].species = settings.nextGenerationId; } - - if atomicSub(&counters.remaining, 1) <= 0 { - return; - } - - 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, - settings.nextGenerationId, - 1000000, - ); - } diff --git a/src/pipelines/agents/agent-generation/agent-schema.wgsl b/src/pipelines/agents/agent-generation/agent-schema.wgsl index 44c0843..4e749b4 100644 --- a/src/pipelines/agents/agent-generation/agent-schema.wgsl +++ b/src/pipelines/agents/agent-generation/agent-schema.wgsl @@ -2,7 +2,6 @@ struct Agent { position: vec2, angle: f32, species: f32, - timeToLive: f32, } @group(1) @binding(1) var agents: array; diff --git a/src/pipelines/agents/agent-generation/agent.ts b/src/pipelines/agents/agent-generation/agent.ts index ebb83c9..92726be 100644 --- a/src/pipelines/agents/agent-generation/agent.ts +++ b/src/pipelines/agents/agent-generation/agent.ts @@ -4,7 +4,6 @@ export interface Agent { position: vec2; angle: number; species: number; - timeToLive: number; } -export const AGENT_SIZE_IN_BYTES = 5 * Float32Array.BYTES_PER_ELEMENT; +export const AGENT_SIZE_IN_BYTES = 4 * Float32Array.BYTES_PER_ELEMENT; diff --git a/src/pipelines/agents/agent-generation/generation-counts.ts b/src/pipelines/agents/agent-generation/generation-counts.ts index 8dc53f4..28a82a5 100644 --- a/src/pipelines/agents/agent-generation/generation-counts.ts +++ b/src/pipelines/agents/agent-generation/generation-counts.ts @@ -1,4 +1,4 @@ export interface GenerationCounts { - currentGenerationCount: number; - nextGenerationCount: number; + evenGenerationCount: number; + oddGenerationCount: number; } diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index 7b3c30c..82e7675 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 = 6; + private static readonly UNIFORM_COUNT = 8; private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPUComputePipeline; @@ -47,8 +47,14 @@ export class AgentPipeline { turnSpeed, sensorOffsetAngle, sensorOffsetDistance, - nextGenerationAggression, - }: AgentSettings & { nextGenerationAggression: number }) { + evenGenerationAggression, + oddGenerationAggression, + nextGenerationId, + }: AgentSettings & { + evenGenerationAggression: number; + oddGenerationAggression: number; + nextGenerationId: number; + }) { this.device.queue.writeBuffer( this.uniforms, 0, @@ -58,7 +64,9 @@ export class AgentPipeline { turnSpeed, (sensorOffsetAngle * Math.PI) / 180, sensorOffsetDistance, - nextGenerationAggression, + evenGenerationAggression, + oddGenerationAggression, + nextGenerationId, ]) ); } diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 1d94e19..049f211 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -4,8 +4,9 @@ struct Settings { turnRate: f32, sensorAngle: f32, sensorOffset: f32, - nextGenerationAggression: f32, - // nextGenerationParity: f32, + evenGenerationAggression: f32, + oddGenerationAggression: f32, + nextGenerationId: f32 }; @group(1) @binding(0) var settings: Settings; @@ -16,7 +17,7 @@ struct Settings { fn main(@builtin(global_invocation_id) global_id: vec3) { let id = global_id.x; - if (id >= arrayLength(&agents) || agents[id].timeToLive <= 0) { + if (id >= arrayLength(&agents)) { return; } @@ -26,19 +27,11 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { let trailCurrent = textureLoad(trailMapIn, vec2(agent.position), 0); 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; - } - } + + // even generation id -> red channel + // odd generation id -> green channel + + let isFromEvenGeneration = agent.species % 2 == 0; let trailForward = sense(agent.position, agent.angle, settings.sensorOffset, 0); let trailLeft = sense(agent.position, agent.angle, settings.sensorOffset, settings.sensorAngle); @@ -47,14 +40,14 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { var weightForward: f32 = trailForward.a * settings.brushTrailWeight; var weightLeft: f32 = trailLeft.a * settings.brushTrailWeight; var weightRight: f32 = trailRight.a * settings.brushTrailWeight; - if (agent.species == 0) { - weightForward += trailForward.r - trailForward.g; - weightLeft += trailLeft.r - trailLeft.g; - weightRight += trailRight.r - trailRight.g; + if (isFromEvenGeneration) { + weightForward += trailForward.r + settings.evenGenerationAggression * trailForward.g; + weightLeft += trailLeft.r + settings.evenGenerationAggression * trailLeft.g; + weightRight += trailRight.r + settings.evenGenerationAggression * trailRight.g; } else { - weightForward += trailForward.g + trailForward.r * settings.nextGenerationAggression; - weightLeft += trailLeft.g + trailLeft.r * settings.nextGenerationAggression; - weightRight += trailRight.g + trailRight.r * settings.nextGenerationAggression; + weightForward += trailForward.g + settings.oddGenerationAggression * trailForward.r; + weightLeft += trailLeft.g + settings.oddGenerationAggression * trailLeft.r; + weightRight += trailRight.g + settings.oddGenerationAggression * trailRight.r; } var rotation: f32 = 0; @@ -77,16 +70,35 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { } var trail = vec4(0, 0.1, 0, 0); - if (agent.species == 0) { + if (isFromEvenGeneration) { trail = vec4(0.1, 0, 0, 0); } - let current = textureLoad(trailMapIn, vec2(nextPosition), 0); - textureStore(trailMapOut, vec2(nextPosition), vec4(trail.rgb + current.rgb, current.a)); + let current = textureLoad(trailMapIn, vec2(nextPosition), 0); + let next = vec4(trail.rgb + current.rgb, current.a); + textureStore(trailMapOut, vec2(nextPosition), next); + + + if(isFromEvenGeneration) { + if next.r < next.g { + if agent.species == settings.nextGenerationId { + // agent.species -= 1; + } else { + agent.species += 1; + } + } + } else { + if next.g < next.r { + if agent.species == settings.nextGenerationId { + // agent.species -= 1; + } else { + agent.species += 1; + } + } + } agent.position = nextPosition; agent.angle = nextAngle; - agent.timeToLive -= state.deltaTime; agents[id] = agent; } diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100644 index 0000000..9be98b1 --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,9 @@ +export const hash = (state: number): number => { + state ^= 2747636419; + state *= 2654435769; + state ^= state >> 16; + state *= 2654435769; + state ^= state >> 16; + state *= 2654435769; + return state / 4294967295.0; +};