Compare commits
No commits in common. "6cef5a35b2a7c47074e548591d58b7a2f67fc472" and "c81ecaf21e798b359cda5c4a1e3f22f5df340c3f" have entirely different histories.
6cef5a35b2
...
c81ecaf21e
10 changed files with 64 additions and 331 deletions
41
CLAUDE.md
41
CLAUDE.md
|
|
@ -1,6 +1,6 @@
|
||||||
# ants-simulation
|
# ants-simulation
|
||||||
|
|
||||||
GPU-accelerated ant colony simulation. ants navigate via pheromone trails, all computed in GLSL fragment shaders rendered to offscreen textures. uses WebGL2 features (MRT, GLSL3). side-view ant farm with sand/powder physics and material-based world. ants use a fixed budget pool with probabilistic activation; behavior is driven by a priority-stack brain (fall → deposit → navigate → forage → dig → wander).
|
GPU-accelerated ant colony simulation. ants navigate via pheromone trails, all computed in GLSL fragment shaders rendered to offscreen textures. uses WebGL2 features (MRT, GLSL3). side-view ant farm with sand/powder physics and material-based world.
|
||||||
|
|
||||||
## stack
|
## stack
|
||||||
|
|
||||||
|
|
@ -27,16 +27,16 @@ all simulation logic runs on the GPU via ping-pong render targets. no JS-side si
|
||||||
1. `SandPhysicsScene` — Margolus block CA for sand/powder physics
|
1. `SandPhysicsScene` — Margolus block CA for sand/powder physics
|
||||||
2. `WorldBlurScene` — diffuse + decay pheromones (3 channels: toHome, toFood, repellent, blocked by solid cells)
|
2. `WorldBlurScene` — diffuse + decay pheromones (3 channels: toHome, toFood, repellent, blocked by solid cells)
|
||||||
3. clear `antsPresenceRenderTarget` (ant-ant spatial queries, stub)
|
3. clear `antsPresenceRenderTarget` (ant-ant spatial queries, stub)
|
||||||
4. `AntsComputeScene` — per-ant state via MRT (writes 2 textures simultaneously); gravity pass runs first (ants fall through air), then priority-stack brain for active ants; handles collision displacement out of solid cells
|
4. `AntsComputeScene` — per-ant state via MRT (writes 2 textures simultaneously), material-aware digging
|
||||||
5. `AntsDiscretizeScene` — maps continuous ant positions to discrete world grid cells
|
5. `AntsDiscretizeScene` — maps continuous ant positions to discrete world grid cells
|
||||||
6. `WorldComputeScene` — merges ant deposits into world pheromone grid
|
6. `WorldComputeScene` — merges ant deposits into world pheromone grid
|
||||||
7. `ColonyStats` — CPU readback of ant texture, computes aggregate stats (foragerRatio, active/budget counts), feeds back as uniforms
|
7. `ColonyStats` — CPU readback of ant texture, computes aggregate stats (foragerRatio), feeds back as uniforms
|
||||||
8. `DrawScene` — user painting with material palette; brush tool also spawns ants (key: A, `BRUSH_ANTS = 999` sentinel, `uAntSpawn` uniform)
|
8. `DrawScene` — user painting with material palette
|
||||||
9. `ScreenScene` — final composited output with side/top camera views (V to toggle); passes ant spawn position to AntsComputeScene
|
9. `ScreenScene` — final composited output with side/top camera views (V to toggle)
|
||||||
|
|
||||||
### GPU textures
|
### GPU textures
|
||||||
|
|
||||||
**ant state** — 2 RGBA Float32 textures per ping-pong target (MRT, `count: 2`). texture size is `Math.ceil(Math.sqrt(antBudget))` — a fixed pool. only `antsStartCount` ants activate on init; dormant slots sit at pos=(0,0). new ants can be activated via the brush tool (probabilistic activation per frame when spawn is requested):
|
**ant state** — 2 RGBA Float32 textures per ping-pong target (MRT, `count: 2`):
|
||||||
- texture 0: `[pos.x, pos.y, angle, packed(storage << 1 | isCarrying)]`
|
- texture 0: `[pos.x, pos.y, angle, packed(storage << 1 | isCarrying)]`
|
||||||
- texture 1: `[personality, cargoMaterialId, pathIntDx, pathIntDy]`
|
- texture 1: `[personality, cargoMaterialId, pathIntDx, pathIntDy]`
|
||||||
|
|
||||||
|
|
@ -72,41 +72,22 @@ Margolus neighborhood cellular automata runs as the first render pass each frame
|
||||||
- physics pass writes back to world ping-pong target before pheromone diffusion runs
|
- physics pass writes back to world ping-pong target before pheromone diffusion runs
|
||||||
- `src/sand/margolus.ts` computes the per-frame block offset (JS side, passed as uniform)
|
- `src/sand/margolus.ts` computes the per-frame block offset (JS side, passed as uniform)
|
||||||
|
|
||||||
### ant physics and brain
|
|
||||||
|
|
||||||
ants run a gravity pass before the priority stack each frame:
|
|
||||||
|
|
||||||
- **gravity**: ants in air cells fall downward. steering logic is skipped while falling (`!isFalling` guard).
|
|
||||||
- **collision displacement**: if an ant ends up inside a solid cell, it is pushed out.
|
|
||||||
- **surface constraint**: grounded ants follow surface contours.
|
|
||||||
|
|
||||||
the priority stack resolves behavior in order, first match wins:
|
|
||||||
|
|
||||||
1. **fall** — handled before stack; sets `isFalling`, skips steering
|
|
||||||
2. **deposit** — drop food at home tile, or deposit carried powder on surface
|
|
||||||
3. **navigate** — pheromone-following when carrying food; upward bias when carrying powder
|
|
||||||
4. **forage** — follow food scent when empty
|
|
||||||
5. **dig** — bias toward diggable powder below surface (side-view only)
|
|
||||||
6. **wander** — fallback random movement
|
|
||||||
|
|
||||||
### key files
|
### key files
|
||||||
|
|
||||||
- `src/Renderer.ts` — render target creation, pass orchestration, MRT setup, colony stats readback
|
- `src/Renderer.ts` — render target creation, pass orchestration, MRT setup, colony stats readback
|
||||||
- `src/Config.ts` — simulation parameters (per-channel pheromone configs); ant keys: `antsStartCount` (default 4), `antBudget` (pool size, default 512), `seedWorld` (boolean)
|
- `src/Config.ts` — simulation parameters (per-channel pheromone configs)
|
||||||
- `src/constants.ts` — material IDs (single source of truth for TS + GLSL); `BRUSH_ANTS = 999` sentinel for ant-spawn brush
|
- `src/constants.ts` — material IDs (single source of truth for TS + GLSL)
|
||||||
- `src/ColonyStats.ts` — CPU readback of ant texture for colony-level aggregate stats (reports active/budget)
|
- `src/ColonyStats.ts` — CPU readback of ant texture for colony-level aggregate stats
|
||||||
- `src/StatsOverlay.ts` — on-screen stats display (cursor position, TPS, colony info); ant count shown as "active / budget"
|
- `src/StatsOverlay.ts` — on-screen stats display (cursor position, TPS, colony info)
|
||||||
- `src/__tests__/worldInit.test.ts` — tests for world seeding (home/food placement via mulberry32 PRNG)
|
|
||||||
- `src/materials/types.ts` — Material interface and BehaviorType constants
|
- `src/materials/types.ts` — Material interface and BehaviorType constants
|
||||||
- `src/materials/registry.ts` — MaterialRegistry with 6 built-in materials
|
- `src/materials/registry.ts` — MaterialRegistry with 6 built-in materials
|
||||||
- `src/materials/lookupTexture.ts` — builds GPU lookup textures from registry
|
- `src/materials/lookupTexture.ts` — builds GPU lookup textures from registry
|
||||||
- `src/sand/margolus.ts` — Margolus block offset calculation
|
- `src/sand/margolus.ts` — Margolus block offset calculation
|
||||||
- `src/scenes/SandPhysicsScene.ts` — sand physics render pass
|
- `src/scenes/SandPhysicsScene.ts` — sand physics render pass
|
||||||
- `src/shaders/antsCompute.frag` — ant behavior + MRT output (2 render targets via layout qualifiers); gravity, priority stack, spawn activation
|
- `src/shaders/antsCompute.frag` — ant behavior + MRT output (2 render targets via layout qualifiers)
|
||||||
- `src/shaders/worldBlur.frag` — per-channel pheromone diffusion/decay (solid cells block diffusion)
|
- `src/shaders/worldBlur.frag` — per-channel pheromone diffusion/decay (solid cells block diffusion)
|
||||||
- `src/shaders/world.frag` — material ID preservation + pheromone merging
|
- `src/shaders/world.frag` — material ID preservation + pheromone merging
|
||||||
- `src/shaders/sandPhysics.frag` — Margolus block CA for powder/sand movement
|
- `src/shaders/sandPhysics.frag` — Margolus block CA for powder/sand movement
|
||||||
- `src/shaders/screenWorld.frag` — final composite; applies per-pixel hash noise (+/-4% brightness) to material colors
|
|
||||||
|
|
||||||
## planning docs
|
## planning docs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
air cell corruption debug log
|
|
||||||
=============================
|
|
||||||
|
|
||||||
problem
|
|
||||||
-------
|
|
||||||
air cells (materialId=0) in the world render target get corrupted to
|
|
||||||
materialId=255 after ~10 simulation frames. this causes air to render
|
|
||||||
as BLACK instead of any intended color, because the material color
|
|
||||||
lookup texture has no entry at index 255 (returns all zeros).
|
|
||||||
|
|
||||||
the original request was to change air from black to light blue.
|
|
||||||
the shader change is trivial but has no effect because the materialId
|
|
||||||
is wrong — the shader's `materialId == MAT_AIR` branch never fires.
|
|
||||||
|
|
||||||
|
|
||||||
what we confirmed
|
|
||||||
-----------------
|
|
||||||
1. init data is correct: air cells = [0,0,0,0], sand cells = [1,0,0,0]
|
|
||||||
(sync readback immediately after generateSideViewWorld + copyTextureToTexture)
|
|
||||||
|
|
||||||
2. frame 0 is clean: every render pass in the pipeline produces [0,0,0,0]
|
|
||||||
for an air cell at top-center (px=512, py=1023). checked after each:
|
|
||||||
- sandPhysicsRT: [0,0,0,0]
|
|
||||||
- worldBlurredRT: [0,0,0,0]
|
|
||||||
- antsDiscreteRT: [0,0,0,0]
|
|
||||||
- worldRT OUTPUT: [0,0,0,0]
|
|
||||||
|
|
||||||
3. by frame 10, worldRT INPUT (read before sandPhysics) = [255,0,0,0]
|
|
||||||
meaning the corruption enters between frame 9's worldCompute output
|
|
||||||
(which passed the check) and frame 10's sandPhysics input
|
|
||||||
|
|
||||||
4. renderToScreen runs between those frames (the sim loop does ~1 step
|
|
||||||
per rAF, then calls renderToScreen). so the corruption window is
|
|
||||||
during renderToScreen or between renderToScreen and the next sim step.
|
|
||||||
|
|
||||||
5. the screen shader IS being used for the full viewport (confirmed by
|
|
||||||
outputting solid blue — entire screen turned blue)
|
|
||||||
|
|
||||||
6. MAT_AIR define is correctly 0 (logged from ScreenScene constructor)
|
|
||||||
|
|
||||||
7. the shader source reaching the material IS the correct file
|
|
||||||
(first 200 chars logged and verified)
|
|
||||||
|
|
||||||
|
|
||||||
what we ruled out
|
|
||||||
-----------------
|
|
||||||
- shader not loading: confirmed via all-blue test and console.debug of source
|
|
||||||
- material color lookup texture: registry has correct values, texture
|
|
||||||
generation code (generateColorData) is straightforward
|
|
||||||
- texture filtering: all render targets use NearestFilter
|
|
||||||
- sand physics: passes through materialId unchanged for non-swapping cells
|
|
||||||
- worldBlur: outputs s0.x (material ID) unchanged
|
|
||||||
- worldCompute (world.frag): preserves materialId from input, only changes
|
|
||||||
it for deposits (depositMatId > 0) or cell clears (discreteAnts.z == 1)
|
|
||||||
- draw.frag: preserves materialId unless actively painting (drawMode >= 0)
|
|
||||||
- vite HMR: shaders load as raw strings at construction, HMR doesn't
|
|
||||||
reconstruct ShaderMaterial. confirmed shader changes take effect after
|
|
||||||
full dev server restart
|
|
||||||
- copyFramebufferToTexture: disabled it, corruption still happened at
|
|
||||||
frame 10. the copy was suspected because it writes to worldRT.texture
|
|
||||||
while that texture may still be bound from DrawScene's tWorld uniform.
|
|
||||||
BUT disabling it did not fix the issue. (this call still needs a proper
|
|
||||||
fix — either use a render pass copy or unbind the texture first)
|
|
||||||
|
|
||||||
|
|
||||||
what we have NOT tested
|
|
||||||
-----------------------
|
|
||||||
- whether the corruption happens without renderToScreen at all (skip it
|
|
||||||
entirely — won't see anything on screen but could log worldRT state)
|
|
||||||
- whether ColonyStats.update (readRenderTargetPixels on antTarget) has
|
|
||||||
side effects that corrupt worldRT. it changes the bound render target.
|
|
||||||
- whether the draw scene render itself corrupts worldRT via some WebGL
|
|
||||||
state leak (even though it writes to worldRenderTargetCopy, maybe
|
|
||||||
binding worldRT.texture as tWorld uniform causes a side effect)
|
|
||||||
- a minimal repro: disable ALL passes except sandPhysics + worldBlur +
|
|
||||||
worldCompute and see if corruption still happens (isolate whether
|
|
||||||
ant passes or screen passes introduce it)
|
|
||||||
- reading worldRT between renderToScreen and the next renderSimulation
|
|
||||||
(requires readback in the rAF callback, after renderToScreen returns)
|
|
||||||
- whether THREE.js 0.173 has known issues with Float32 render targets
|
|
||||||
and copyFramebufferToTexture / readRenderTargetPixels
|
|
||||||
|
|
||||||
|
|
||||||
render pipeline (per frame)
|
|
||||||
---------------------------
|
|
||||||
in renderSimulation():
|
|
||||||
1. sandPhysics: reads worldRT -> writes sandPhysicsRT
|
|
||||||
2. worldBlur: reads sandPhysicsRT -> writes worldBlurredRT
|
|
||||||
3. antsPresence: cleared
|
|
||||||
4. antsCompute: reads worldBlurredRT + antsPresenceRT -> writes antsComputeTarget (MRT)
|
|
||||||
5. antsDiscretize: reads antsComputeTarget -> writes antsDiscreteRT (NOT cleared per frame!)
|
|
||||||
6. worldCompute: reads worldBlurredRT + antsDiscreteRT -> writes worldRT
|
|
||||||
7. ColonyStats.update: readRenderTargetPixels on antsComputeTarget (CPU readback)
|
|
||||||
|
|
||||||
in renderToScreen():
|
|
||||||
8. drawScene: reads worldRT (tWorld) -> writes worldRenderTargetCopy
|
|
||||||
9. copyFramebufferToTexture: copies worldRenderTargetCopy framebuffer -> worldRT.texture
|
|
||||||
(CURRENTLY DISABLED in local changes)
|
|
||||||
10. screenScene: reads worldRenderTargetCopy (map) -> writes to canvas (null target)
|
|
||||||
|
|
||||||
worldRT is the persistent world state. it feeds back into itself:
|
|
||||||
worldRT -> sandPhysics -> worldBlur -> worldCompute -> worldRT
|
|
||||||
|
|
||||||
|
|
||||||
current local changes (uncommitted)
|
|
||||||
------------------------------------
|
|
||||||
1. src/shaders/screenWorld.frag:
|
|
||||||
- air background color changed from white vec3(1,1,1) to light blue vec3(0.53, 0.81, 0.92)
|
|
||||||
- this change works correctly IF materialId is 0 for air cells
|
|
||||||
- also: the committed version has stale debug grayscale code from a previous session
|
|
||||||
that needs to be cleaned up
|
|
||||||
|
|
||||||
2. src/Renderer.ts:
|
|
||||||
- copyFramebufferToTexture disabled (replaced with comment)
|
|
||||||
- minor whitespace change
|
|
||||||
- both should be reverted when the real fix is found
|
|
||||||
|
|
||||||
3. src/scenes/ScreenScene.ts:
|
|
||||||
- groundDefines extracted to variable (cosmetic, from debug session)
|
|
||||||
|
|
||||||
4. src/materials/registry.ts:
|
|
||||||
- air color changed from [0,0,0,0] to [0.53,0.81,0.92,1.0]
|
|
||||||
- this was an early attempt that doesn't matter since the shader
|
|
||||||
hardcodes the air branch (doesn't use the lookup for air)
|
|
||||||
|
|
||||||
|
|
||||||
key file locations
|
|
||||||
------------------
|
|
||||||
- world init: src/WorldInit.ts (generateSideViewWorld)
|
|
||||||
- render pipeline: src/Renderer.ts (renderSimulation + renderToScreen)
|
|
||||||
- world texture format: RGBA Float32, R=materialId, G=scentToHome, B=scentToFood, A=repellent
|
|
||||||
- screen shader: src/shaders/screenWorld.frag
|
|
||||||
- world compute: src/shaders/world.frag
|
|
||||||
- sand physics: src/shaders/sandPhysics.frag
|
|
||||||
- pheromone blur: src/shaders/worldBlur.frag
|
|
||||||
- draw shader: src/shaders/draw.frag
|
|
||||||
- material constants: src/constants.ts (MAT_AIR=0 through MAT_HOME=5)
|
|
||||||
- material registry: src/materials/registry.ts
|
|
||||||
- color lookup texture gen: src/materials/lookupTexture.ts
|
|
||||||
|
|
||||||
|
|
||||||
suggested next steps
|
|
||||||
--------------------
|
|
||||||
1. try disabling renderToScreen entirely and adding a single
|
|
||||||
console.debug readback of worldRT after 20 frames. if worldRT stays
|
|
||||||
[0,0,0,0] for air, the corruption is caused by renderToScreen or
|
|
||||||
something it triggers.
|
|
||||||
|
|
||||||
2. if step 1 still shows corruption, try disabling the ant passes
|
|
||||||
(antsCompute, antsDiscretize, antsPresence clear) and see if the
|
|
||||||
corruption disappears. the world pipeline without ants is just
|
|
||||||
sandPhysics -> worldBlur -> worldCompute, which should be a no-op
|
|
||||||
for air cells.
|
|
||||||
|
|
||||||
3. if step 2 still shows corruption, the issue might be in sandPhysics
|
|
||||||
(Margolus block CA) or worldCompute. try disabling each individually.
|
|
||||||
|
|
||||||
4. check THREE.js 0.173 changelog/issues for Float32 render target bugs.
|
|
||||||
|
|
||||||
5. consider adding a "sanitizer" pass that clamps materialId to [0,5]
|
|
||||||
at the start of each frame as a workaround while debugging.
|
|
||||||
|
|
@ -73,7 +73,6 @@ class GUIController {
|
||||||
"rock (E)": MAT_ROCK,
|
"rock (E)": MAT_ROCK,
|
||||||
"food (W)": MAT_FOOD,
|
"food (W)": MAT_FOOD,
|
||||||
"home (Q)": MAT_HOME,
|
"home (Q)": MAT_HOME,
|
||||||
"ants (A)": 999,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// proxy object for lil-gui string dropdown — initialize from saved config
|
// proxy object for lil-gui string dropdown — initialize from saved config
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,6 @@ export default class Renderer {
|
||||||
|
|
||||||
constructor(public canvas: HTMLCanvasElement) {
|
constructor(public canvas: HTMLCanvasElement) {
|
||||||
this.renderer = new THREE.WebGLRenderer({ canvas });
|
this.renderer = new THREE.WebGLRenderer({ canvas });
|
||||||
// clear alpha must be 0: UnsignedByte render targets (antsDiscreteRT) retain
|
|
||||||
// the clear value in pixels not covered by instanced draws. alpha=1 produces
|
|
||||||
// byte 255, which world.frag misinterprets as depositMatId=255.
|
|
||||||
this.renderer.setClearColor(0x000000, 0);
|
|
||||||
|
|
||||||
this.initResources();
|
this.initResources();
|
||||||
|
|
||||||
|
|
@ -185,7 +181,6 @@ export default class Renderer {
|
||||||
);
|
);
|
||||||
scenes.sandPhysics.material.uniforms.uFrame.value = this.frameCounter;
|
scenes.sandPhysics.material.uniforms.uFrame.value = this.frameCounter;
|
||||||
this.renderer.render(scenes.sandPhysics, scenes.sandPhysics.camera);
|
this.renderer.render(scenes.sandPhysics, scenes.sandPhysics.camera);
|
||||||
|
|
||||||
this.frameCounter++;
|
this.frameCounter++;
|
||||||
|
|
||||||
this.setViewportFromRT(this.resources.worldBlurredRenderTarget);
|
this.setViewportFromRT(this.resources.worldBlurredRenderTarget);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export default class StatsOverlay {
|
||||||
this.tpsEl.textContent = `${Math.round(tps)}`;
|
this.tpsEl.textContent = `${Math.round(tps)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.antsEl.textContent = `${colonyStats.totalAnts} / ${Config.antBudget}`;
|
this.antsEl.textContent = `${colonyStats.totalAnts}`;
|
||||||
const carrying = Math.round(
|
const carrying = Math.round(
|
||||||
colonyStats.foragerRatio * colonyStats.totalAnts,
|
colonyStats.foragerRatio * colonyStats.totalAnts,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const BUILTIN_MATERIALS: Material[] = [
|
||||||
name: "air",
|
name: "air",
|
||||||
behavior: BEHAVIOR_GAS,
|
behavior: BEHAVIOR_GAS,
|
||||||
density: 0,
|
density: 0,
|
||||||
color: [0.53, 0.81, 0.92, 1.0],
|
color: [0, 0, 0, 0],
|
||||||
hardness: 0,
|
hardness: 0,
|
||||||
angleOfRepose: 0,
|
angleOfRepose: 0,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ import fragmentShaderGround from "../shaders/screenWorld.frag";
|
||||||
import vertexShaderGround from "../shaders/screenWorld.vert";
|
import vertexShaderGround from "../shaders/screenWorld.vert";
|
||||||
import AbstractScene from "./AbstractScene";
|
import AbstractScene from "./AbstractScene";
|
||||||
|
|
||||||
export const BRUSH_ANTS = 999;
|
|
||||||
|
|
||||||
export default class ScreenScene extends AbstractScene {
|
export default class ScreenScene extends AbstractScene {
|
||||||
public readonly camera: THREE.OrthographicCamera;
|
public readonly camera: THREE.OrthographicCamera;
|
||||||
public readonly material: THREE.ShaderMaterial;
|
public readonly material: THREE.ShaderMaterial;
|
||||||
|
|
@ -35,29 +33,17 @@ export default class ScreenScene extends AbstractScene {
|
||||||
|
|
||||||
// resolves active draw mode: key-held takes priority, then GUI brush selection
|
// resolves active draw mode: key-held takes priority, then GUI brush selection
|
||||||
public get effectiveDrawMode(): number {
|
public get effectiveDrawMode(): number {
|
||||||
if (this.drawMode === BRUSH_ANTS) return -1;
|
|
||||||
if (this.drawMode >= 0) return this.drawMode;
|
if (this.drawMode >= 0) return this.drawMode;
|
||||||
if (this.isPointerDown && Config.brushMaterial >= 0) {
|
if (this.isPointerDown && Config.brushMaterial >= 0)
|
||||||
if (Config.brushMaterial === BRUSH_ANTS) return -1;
|
|
||||||
return Config.brushMaterial;
|
return Config.brushMaterial;
|
||||||
}
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isAntBrushActive(): boolean {
|
|
||||||
if (this.drawMode === BRUSH_ANTS) return true;
|
|
||||||
if (this.isPointerDown && Config.brushMaterial === BRUSH_ANTS)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public renderWidth: number = 1;
|
public renderWidth: number = 1;
|
||||||
public renderHeight: number = 1;
|
public renderHeight: number = 1;
|
||||||
|
|
||||||
constructor(renderer: Renderer) {
|
constructor(renderer: Renderer) {
|
||||||
super(renderer);
|
super(renderer);
|
||||||
|
|
||||||
const groundDefines = this.renderer.getCommonMaterialDefines();
|
|
||||||
|
|
||||||
const ground = new THREE.Mesh(
|
const ground = new THREE.Mesh(
|
||||||
new THREE.PlaneGeometry(1, 1),
|
new THREE.PlaneGeometry(1, 1),
|
||||||
new THREE.ShaderMaterial({
|
new THREE.ShaderMaterial({
|
||||||
|
|
@ -72,7 +58,7 @@ export default class ScreenScene extends AbstractScene {
|
||||||
},
|
},
|
||||||
vertexShader: vertexShaderGround,
|
vertexShader: vertexShaderGround,
|
||||||
fragmentShader: fragmentShaderGround,
|
fragmentShader: fragmentShaderGround,
|
||||||
defines: groundDefines,
|
defines: this.renderer.getCommonMaterialDefines(),
|
||||||
glslVersion: THREE.GLSL3,
|
glslVersion: THREE.GLSL3,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -101,7 +87,6 @@ export default class ScreenScene extends AbstractScene {
|
||||||
vertexShader: vertexShaderAnts,
|
vertexShader: vertexShaderAnts,
|
||||||
fragmentShader: fragmentShaderAnts,
|
fragmentShader: fragmentShaderAnts,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
glslVersion: THREE.GLSL3,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.createInstancedAntsMesh();
|
this.createInstancedAntsMesh();
|
||||||
|
|
@ -203,10 +188,6 @@ export default class ScreenScene extends AbstractScene {
|
||||||
this.drawMode = MAT_DIRT;
|
this.drawMode = MAT_DIRT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "KeyA": {
|
|
||||||
this.drawMode = BRUSH_ANTS;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "KeyV": {
|
case "KeyV": {
|
||||||
Config.viewMode =
|
Config.viewMode =
|
||||||
Config.viewMode === "side" ? "top" : "side";
|
Config.viewMode === "side" ? "top" : "side";
|
||||||
|
|
@ -295,16 +276,7 @@ export default class ScreenScene extends AbstractScene {
|
||||||
this.renderHeight = height;
|
this.renderHeight = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
public update() {
|
public update() {}
|
||||||
if (this.isAntBrushActive && this.isPointerDown) {
|
|
||||||
this.antSpawnRequest.set(
|
|
||||||
this.pointerPosition.x,
|
|
||||||
this.pointerPosition.y,
|
|
||||||
Config.brushRadius,
|
|
||||||
1, // flag: spawn requested
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public applyCameraZoom() {
|
public applyCameraZoom() {
|
||||||
this.updateCameraZoom();
|
this.updateCameraZoom();
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ precision highp int;
|
||||||
|
|
||||||
#define PI 3.1415926535897932384626433832795
|
#define PI 3.1415926535897932384626433832795
|
||||||
|
|
||||||
out vec2 vUv;
|
varying vec2 vUv;
|
||||||
out float vIsCarryingFood;
|
varying float vIsCarryingFood;
|
||||||
|
|
||||||
uniform sampler2D tData;
|
uniform sampler2D tData;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ uniform sampler2D tWorld;
|
||||||
uniform sampler2D tPresence;
|
uniform sampler2D tPresence;
|
||||||
uniform float uForagerRatio;
|
uniform float uForagerRatio;
|
||||||
uniform sampler2D uMaterialProps;
|
uniform sampler2D uMaterialProps;
|
||||||
uniform vec4 uAntSpawn;
|
|
||||||
|
|
||||||
const float ANT_CARRY_STRENGTH = 1.0;
|
const float ANT_CARRY_STRENGTH = 1.0;
|
||||||
const float sampleDistance = 20.;
|
const float sampleDistance = 20.;
|
||||||
|
|
@ -35,9 +34,9 @@ vec2 roundUvToCellCenter(vec2 uv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool tryDropFood(vec2 pos) {
|
bool tryDropFood(vec2 pos) {
|
||||||
float currentMatId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
float materialId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
||||||
float belowMatId = texture(tWorld, roundUvToCellCenter(pos - vec2(0., cellSize))).x;
|
|
||||||
return int(currentMatId) == MAT_HOME || int(belowMatId) == MAT_HOME;
|
return int(materialId) == MAT_HOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isObstacle(vec2 pos) {
|
bool isObstacle(vec2 pos) {
|
||||||
|
|
@ -95,43 +94,14 @@ void main() {
|
||||||
int antIndex = int(vUv.y * float(texSize.y)) * texSize.x + int(vUv.x * float(texSize.x));
|
int antIndex = int(vUv.y * float(texSize.y)) * texSize.x + int(vUv.x * float(texSize.x));
|
||||||
|
|
||||||
if (pos == vec2(0)) { // init new ant
|
if (pos == vec2(0)) { // init new ant
|
||||||
bool spawnRequested = (uAntSpawn.w > 0.5);
|
// only activate ants within the start count
|
||||||
|
if (antIndex >= ANTS_START_COUNT) {
|
||||||
if (antIndex >= ANTS_START_COUNT && !spawnRequested) {
|
// dormant ant — output zeros and skip everything
|
||||||
// dormant ant, no spawn request — skip
|
|
||||||
FragColor = vec4(0);
|
FragColor = vec4(0);
|
||||||
FragColorExt = vec4(0);
|
FragColorExt = vec4(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (antIndex >= ANTS_START_COUNT && spawnRequested) {
|
|
||||||
// probabilistic activation: ~brushRadius ants per frame
|
|
||||||
float activationChance = uAntSpawn.z / float(ANT_BUDGET);
|
|
||||||
float roll = rand(vUv * 50000. + fract(uTime / 1000.));
|
|
||||||
if (roll > activationChance) {
|
|
||||||
// not selected this frame — stay dormant
|
|
||||||
FragColor = vec4(0);
|
|
||||||
FragColorExt = vec4(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// activate at spawn position with scatter
|
|
||||||
float scatter = uAntSpawn.z / WORLD_SIZE;
|
|
||||||
float rngX = rand(vUv * 10000. + fract(uTime / 1000.));
|
|
||||||
float rngY = rand(vUv * 20000. + fract(uTime / 1000.) + 0.5);
|
|
||||||
pos = vec2(
|
|
||||||
uAntSpawn.x + (rngX - 0.5) * scatter,
|
|
||||||
uAntSpawn.y + (rngY - 0.5) * scatter
|
|
||||||
);
|
|
||||||
pos = clamp(pos, 0., 1.);
|
|
||||||
angle = rand(vUv * 42069.) * 2.0 * PI;
|
|
||||||
isCarrying = 0.;
|
|
||||||
storage = 0.;
|
|
||||||
personality = rand(vUv * 42069.);
|
|
||||||
cargoMaterialId = 0.;
|
|
||||||
pathIntDx = 0.;
|
|
||||||
pathIntDy = 0.;
|
|
||||||
} else {
|
|
||||||
// normal init for starting ants
|
|
||||||
#if VIEW_MODE_SIDE
|
#if VIEW_MODE_SIDE
|
||||||
// spawn on sand surface: random X, scan down from top to find first non-air cell
|
// spawn on sand surface: random X, scan down from top to find first non-air cell
|
||||||
float spawnX = rand(vUv * 10000.);
|
float spawnX = rand(vUv * 10000.);
|
||||||
|
|
@ -145,7 +115,7 @@ void main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos = vec2(spawnX, surfaceY);
|
pos = vec2(spawnX, surfaceY);
|
||||||
angle = (rand(vUv * 31337.) > 0.5) ? 0.0 : PI; // face randomly left or right
|
angle = -PI * 0.5; // face downward initially
|
||||||
#else
|
#else
|
||||||
pos = vec2(0.5);
|
pos = vec2(0.5);
|
||||||
angle = rand(vUv * 10000.) * 2. * PI;
|
angle = rand(vUv * 10000.) * 2. * PI;
|
||||||
|
|
@ -157,7 +127,6 @@ void main() {
|
||||||
pathIntDx = 0.;
|
pathIntDx = 0.;
|
||||||
pathIntDy = 0.;
|
pathIntDy = 0.;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- GRAVITY AND SURFACE CHECK ---
|
// --- GRAVITY AND SURFACE CHECK ---
|
||||||
vec2 cellCenter = roundUvToCellCenter(pos);
|
vec2 cellCenter = roundUvToCellCenter(pos);
|
||||||
|
|
@ -185,14 +154,9 @@ void main() {
|
||||||
|
|
||||||
bool isFalling = false;
|
bool isFalling = false;
|
||||||
|
|
||||||
// GRAVITY: if nothing solid below and not at world bottom, fall multiple cells
|
// GRAVITY: if nothing solid below and not at world bottom, fall
|
||||||
if (!belowSolid && pos.y > cellSize) {
|
if (!belowSolid && pos.y > cellSize) {
|
||||||
for (int g = 0; g < 4; g++) {
|
|
||||||
if (pos.y <= cellSize) break;
|
|
||||||
float matBelow = texture(tWorld, roundUvToCellCenter(pos - vec2(0., cellSize))).x;
|
|
||||||
if (int(matBelow) != MAT_AIR) break;
|
|
||||||
pos.y -= cellSize;
|
pos.y -= cellSize;
|
||||||
}
|
|
||||||
isFalling = true;
|
isFalling = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,36 +295,28 @@ void main() {
|
||||||
angle += PI * (noise - 0.5);
|
angle += PI * (noise - 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- PICKUP (check cell ahead — ants walk in air, can't enter solid cells) ----
|
// ---- PICKUP ----
|
||||||
if (isCarrying == 0.) {
|
if (isCarrying == 0.) {
|
||||||
vec2 aheadUv = roundUvToCellCenter(pos + vec2(cos(angle), sin(angle)) * cellSize);
|
float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
||||||
float aheadMatId = texture(tWorld, aheadUv).x;
|
int cellMatInt = int(cellMatId);
|
||||||
int aheadMatInt = int(aheadMatId);
|
|
||||||
|
|
||||||
// food: pick up from any adjacent cell
|
if (cellMatInt == MAT_FOOD) {
|
||||||
if (aheadMatInt == MAT_FOOD) {
|
|
||||||
isCarrying = 1.;
|
isCarrying = 1.;
|
||||||
cargoMaterialId = aheadMatId;
|
cargoMaterialId = cellMatId;
|
||||||
angle += PI;
|
angle += PI;
|
||||||
storage = getMaxScentStorage(vUv);
|
storage = getMaxScentStorage(vUv);
|
||||||
} else if (aheadMatInt != MAT_AIR && aheadMatInt != MAT_HOME) {
|
} else if (cellMatInt != MAT_AIR && cellMatInt != MAT_HOME) {
|
||||||
// powder: don't grab the surface we're walking on — only dig
|
vec4 props = texelFetch(uMaterialProps, ivec2(cellMatInt, 0), 0);
|
||||||
// when facing diagonally into material (DIG priority angles us there)
|
|
||||||
vec2 belowCell = roundUvToCellCenter(pos - vec2(0., cellSize));
|
|
||||||
bool isWalkingSurface = all(equal(aheadUv, belowCell));
|
|
||||||
if (!isWalkingSurface) {
|
|
||||||
vec4 props = texelFetch(uMaterialProps, ivec2(aheadMatInt, 0), 0);
|
|
||||||
float behavior = props.r;
|
float behavior = props.r;
|
||||||
float hardness = props.b;
|
float hardness = props.b;
|
||||||
if (behavior == BEHAVIOR_POWDER && hardness <= ANT_CARRY_STRENGTH) {
|
if (behavior == BEHAVIOR_POWDER && hardness <= ANT_CARRY_STRENGTH) {
|
||||||
isCarrying = 1.;
|
isCarrying = 1.;
|
||||||
cargoMaterialId = aheadMatId;
|
cargoMaterialId = cellMatId;
|
||||||
angle += PI;
|
angle += PI;
|
||||||
storage = getMaxScentStorage(vUv);
|
storage = getMaxScentStorage(vUv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} // end !isFalling
|
} // end !isFalling
|
||||||
|
|
||||||
FragColor = vec4(
|
FragColor = vec4(
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,6 @@ out vec4 FragColor;
|
||||||
uniform sampler2D map;
|
uniform sampler2D map;
|
||||||
uniform sampler2D uMaterialColors;
|
uniform sampler2D uMaterialColors;
|
||||||
|
|
||||||
float hash(ivec2 p) {
|
|
||||||
int h = p.x * 374761393 + p.y * 668265263;
|
|
||||||
h = (h ^ (h >> 13)) * 1274126177;
|
|
||||||
return float(h & 0x7fffffff) / float(0x7fffffff);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec4 value = texture(map, vUv);
|
vec4 value = texture(map, vUv);
|
||||||
|
|
||||||
|
|
@ -31,15 +25,12 @@ void main() {
|
||||||
|
|
||||||
if (a == 0.) t = vec3(0);
|
if (a == 0.) t = vec3(0);
|
||||||
|
|
||||||
vec3 color = mix(vec3(0.53, 0.81, 0.92), t, a * 0.7);
|
vec3 color = mix(vec3(1, 1, 1), t, a * 0.7);
|
||||||
|
|
||||||
// non-air cells use material color as base, with pheromone tint
|
// non-air cells use material color as base, with pheromone tint
|
||||||
if (materialId != MAT_AIR) {
|
if (materialId != MAT_AIR) {
|
||||||
vec4 matColor = texelFetch(uMaterialColors, ivec2(materialId, 0), 0);
|
vec4 matColor = texelFetch(uMaterialColors, ivec2(materialId, 0), 0);
|
||||||
// per-pixel color variation: +/-4% brightness noise
|
color = mix(matColor.rgb, t, a * 0.3);
|
||||||
float colorNoise = hash(ivec2(gl_FragCoord.xy)) * 0.08 - 0.04;
|
|
||||||
vec3 variedColor = matColor.rgb + colorNoise;
|
|
||||||
color = mix(variedColor, t, a * 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FragColor = vec4(color, 1);
|
FragColor = vec4(color, 1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue