diff --git a/src/ColonyStats.ts b/src/ColonyStats.ts new file mode 100644 index 0000000..3b5dbeb --- /dev/null +++ b/src/ColonyStats.ts @@ -0,0 +1,67 @@ +import type * as THREE from "three"; + +export interface ColonyStatsData { + foragerRatio: number; // fraction of ants currently carrying food + totalAnts: number; // total ant count +} + +export default class ColonyStats { + private buffer: Float32Array; + private statsData: ColonyStatsData = { + foragerRatio: 0, + totalAnts: 0, + }; + + constructor() { + this.buffer = new Float32Array(0); + } + + public update( + renderer: THREE.WebGLRenderer, + antTarget: THREE.WebGLRenderTarget, + ): ColonyStatsData { + const width = antTarget.width; + const height = antTarget.height; + const pixelCount = width * height; + + // resize buffer if needed + if (this.buffer.length !== pixelCount * 4) { + this.buffer = new Float32Array(pixelCount * 4); + } + + renderer.readRenderTargetPixels( + antTarget, + 0, + 0, + width, + height, + this.buffer, + ); + + let carryingCount = 0; + let activeAnts = 0; + + for (let i = 0; i < pixelCount; i++) { + const posX = this.buffer[i * 4]; + const posY = this.buffer[i * 4 + 1]; + const w = this.buffer[i * 4 + 3]; // alpha = packed state + + // skip uninitialized ants (both coordinates at origin) + if (posX === 0 && posY === 0) continue; + + activeAnts++; + const isCarrying = Math.trunc(w) & 1; + if (isCarrying) carryingCount++; + } + + this.statsData.totalAnts = activeAnts; + this.statsData.foragerRatio = + activeAnts > 0 ? carryingCount / activeAnts : 0; + + return this.statsData; + } + + public get data(): ColonyStatsData { + return this.statsData; + } +} diff --git a/src/Renderer.ts b/src/Renderer.ts index d6d5fcd..e90564e 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -1,6 +1,7 @@ import type { WebGLRenderTarget } from "three"; import * as THREE from "three"; import type { SceneCollection } from "./App"; +import ColonyStats from "./ColonyStats"; import Config from "./Config"; import { FOOD_QUALITY_MASK, @@ -22,6 +23,7 @@ interface Resources { export default class Renderer { private renderer: THREE.WebGLRenderer; public resources!: Resources; + private colonyStats = new ColonyStats(); constructor(public canvas: HTMLCanvasElement) { this.renderer = new THREE.WebGLRenderer({ canvas }); @@ -157,6 +159,11 @@ export default class Renderer { antsComputeTarget.textures[0]; scenes.screen.groundMaterial.uniforms.map.value = this.resources.worldRenderTargetCopy.texture; + + // colony stats readback — read ant state texture, compute aggregate stats on CPU + this.colonyStats.update(this.renderer, antsComputeTarget); + scenes.ants.material.uniforms.uForagerRatio.value = + this.colonyStats.data.foragerRatio; } private setViewportFromRT(rt: WebGLRenderTarget) { diff --git a/src/scenes/AntsComputeScene.ts b/src/scenes/AntsComputeScene.ts index 5234ccc..aacb5c5 100644 --- a/src/scenes/AntsComputeScene.ts +++ b/src/scenes/AntsComputeScene.ts @@ -22,6 +22,7 @@ export default class AntsComputeScene extends AbstractScene { tLastExtState: { value: null }, tWorld: { value: null }, tPresence: { value: null }, + uForagerRatio: { value: 0 }, }, vertexShader, fragmentShader, diff --git a/src/shaders/antsCompute.frag b/src/shaders/antsCompute.frag index 995733f..df21707 100644 --- a/src/shaders/antsCompute.frag +++ b/src/shaders/antsCompute.frag @@ -13,6 +13,7 @@ uniform sampler2D tLastState; uniform sampler2D tLastExtState; uniform sampler2D tWorld; uniform sampler2D tPresence; +uniform float uForagerRatio; const float sampleDistance = 20.; const float cellSize = 1. / WORLD_SIZE;