Compare commits
10 commits
c81ecaf21e
...
6cef5a35b2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cef5a35b2 | |||
| 81526052f2 | |||
| c8a07d0783 | |||
| 5d25218433 | |||
| 328f8a76e2 | |||
| 64d7bf6437 | |||
| 9f3ecb57e8 | |||
| 4c11924580 | |||
| aa3c94091b | |||
| b8f49afcc4 |
10 changed files with 331 additions and 64 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.
|
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).
|
||||||
|
|
||||||
## 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), material-aware digging
|
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
|
||||||
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), feeds back as uniforms
|
7. `ColonyStats` — CPU readback of ant texture, computes aggregate stats (foragerRatio, active/budget counts), feeds back as uniforms
|
||||||
8. `DrawScene` — user painting with material palette
|
8. `DrawScene` — user painting with material palette; brush tool also spawns ants (key: A, `BRUSH_ANTS = 999` sentinel, `uAntSpawn` uniform)
|
||||||
9. `ScreenScene` — final composited output with side/top camera views (V to toggle)
|
9. `ScreenScene` — final composited output with side/top camera views (V to toggle); passes ant spawn position to AntsComputeScene
|
||||||
|
|
||||||
### GPU textures
|
### GPU textures
|
||||||
|
|
||||||
**ant state** — 2 RGBA Float32 textures per ping-pong target (MRT, `count: 2`):
|
**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):
|
||||||
- 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,22 +72,41 @@ 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)
|
- `src/Config.ts` — simulation parameters (per-channel pheromone configs); ant keys: `antsStartCount` (default 4), `antBudget` (pool size, default 512), `seedWorld` (boolean)
|
||||||
- `src/constants.ts` — material IDs (single source of truth for TS + GLSL)
|
- `src/constants.ts` — material IDs (single source of truth for TS + GLSL); `BRUSH_ANTS = 999` sentinel for ant-spawn brush
|
||||||
- `src/ColonyStats.ts` — CPU readback of ant texture for colony-level aggregate stats
|
- `src/ColonyStats.ts` — CPU readback of ant texture for colony-level aggregate stats (reports active/budget)
|
||||||
- `src/StatsOverlay.ts` — on-screen stats display (cursor position, TPS, colony info)
|
- `src/StatsOverlay.ts` — on-screen stats display (cursor position, TPS, colony info); ant count shown as "active / budget"
|
||||||
|
- `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)
|
- `src/shaders/antsCompute.frag` — ant behavior + MRT output (2 render targets via layout qualifiers); gravity, priority stack, spawn activation
|
||||||
- `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
|
||||||
|
|
||||||
|
|
|
||||||
161
docs/debug/air-color-corruption.md
Normal file
161
docs/debug/air-color-corruption.md
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
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,6 +73,7 @@ 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,6 +47,10 @@ 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();
|
||||||
|
|
||||||
|
|
@ -181,6 +185,7 @@ 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}`;
|
this.antsEl.textContent = `${colonyStats.totalAnts} / ${Config.antBudget}`;
|
||||||
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, 0, 0, 0],
|
color: [0.53, 0.81, 0.92, 1.0],
|
||||||
hardness: 0,
|
hardness: 0,
|
||||||
angleOfRepose: 0,
|
angleOfRepose: 0,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ 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;
|
||||||
|
|
@ -33,17 +35,29 @@ 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({
|
||||||
|
|
@ -58,7 +72,7 @@ export default class ScreenScene extends AbstractScene {
|
||||||
},
|
},
|
||||||
vertexShader: vertexShaderGround,
|
vertexShader: vertexShaderGround,
|
||||||
fragmentShader: fragmentShaderGround,
|
fragmentShader: fragmentShaderGround,
|
||||||
defines: this.renderer.getCommonMaterialDefines(),
|
defines: groundDefines,
|
||||||
glslVersion: THREE.GLSL3,
|
glslVersion: THREE.GLSL3,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -87,6 +101,7 @@ 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();
|
||||||
|
|
@ -188,6 +203,10 @@ 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";
|
||||||
|
|
@ -276,7 +295,16 @@ 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
|
||||||
|
|
||||||
varying vec2 vUv;
|
out vec2 vUv;
|
||||||
varying float vIsCarryingFood;
|
out float vIsCarryingFood;
|
||||||
|
|
||||||
uniform sampler2D tData;
|
uniform sampler2D tData;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ 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.;
|
||||||
|
|
@ -34,9 +35,9 @@ vec2 roundUvToCellCenter(vec2 uv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool tryDropFood(vec2 pos) {
|
bool tryDropFood(vec2 pos) {
|
||||||
float materialId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
float currentMatId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
||||||
|
float belowMatId = texture(tWorld, roundUvToCellCenter(pos - vec2(0., cellSize))).x;
|
||||||
return int(materialId) == MAT_HOME;
|
return int(currentMatId) == MAT_HOME || int(belowMatId) == MAT_HOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isObstacle(vec2 pos) {
|
bool isObstacle(vec2 pos) {
|
||||||
|
|
@ -94,14 +95,43 @@ 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
|
||||||
// only activate ants within the start count
|
bool spawnRequested = (uAntSpawn.w > 0.5);
|
||||||
if (antIndex >= ANTS_START_COUNT) {
|
|
||||||
// dormant ant — output zeros and skip everything
|
if (antIndex >= ANTS_START_COUNT && !spawnRequested) {
|
||||||
|
// 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.);
|
||||||
|
|
@ -115,7 +145,7 @@ void main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos = vec2(spawnX, surfaceY);
|
pos = vec2(spawnX, surfaceY);
|
||||||
angle = -PI * 0.5; // face downward initially
|
angle = (rand(vUv * 31337.) > 0.5) ? 0.0 : PI; // face randomly left or right
|
||||||
#else
|
#else
|
||||||
pos = vec2(0.5);
|
pos = vec2(0.5);
|
||||||
angle = rand(vUv * 10000.) * 2. * PI;
|
angle = rand(vUv * 10000.) * 2. * PI;
|
||||||
|
|
@ -127,6 +157,7 @@ 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);
|
||||||
|
|
@ -154,9 +185,14 @@ void main() {
|
||||||
|
|
||||||
bool isFalling = false;
|
bool isFalling = false;
|
||||||
|
|
||||||
// GRAVITY: if nothing solid below and not at world bottom, fall
|
// GRAVITY: if nothing solid below and not at world bottom, fall multiple cells
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,28 +331,36 @@ void main() {
|
||||||
angle += PI * (noise - 0.5);
|
angle += PI * (noise - 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- PICKUP ----
|
// ---- PICKUP (check cell ahead — ants walk in air, can't enter solid cells) ----
|
||||||
if (isCarrying == 0.) {
|
if (isCarrying == 0.) {
|
||||||
float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
vec2 aheadUv = roundUvToCellCenter(pos + vec2(cos(angle), sin(angle)) * cellSize);
|
||||||
int cellMatInt = int(cellMatId);
|
float aheadMatId = texture(tWorld, aheadUv).x;
|
||||||
|
int aheadMatInt = int(aheadMatId);
|
||||||
|
|
||||||
if (cellMatInt == MAT_FOOD) {
|
// food: pick up from any adjacent cell
|
||||||
|
if (aheadMatInt == MAT_FOOD) {
|
||||||
isCarrying = 1.;
|
isCarrying = 1.;
|
||||||
cargoMaterialId = cellMatId;
|
cargoMaterialId = aheadMatId;
|
||||||
angle += PI;
|
angle += PI;
|
||||||
storage = getMaxScentStorage(vUv);
|
storage = getMaxScentStorage(vUv);
|
||||||
} else if (cellMatInt != MAT_AIR && cellMatInt != MAT_HOME) {
|
} else if (aheadMatInt != MAT_AIR && aheadMatInt != MAT_HOME) {
|
||||||
vec4 props = texelFetch(uMaterialProps, ivec2(cellMatInt, 0), 0);
|
// powder: don't grab the surface we're walking on — only dig
|
||||||
|
// 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 = cellMatId;
|
cargoMaterialId = aheadMatId;
|
||||||
angle += PI;
|
angle += PI;
|
||||||
storage = getMaxScentStorage(vUv);
|
storage = getMaxScentStorage(vUv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} // end !isFalling
|
} // end !isFalling
|
||||||
|
|
||||||
FragColor = vec4(
|
FragColor = vec4(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ 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);
|
||||||
|
|
||||||
|
|
@ -25,12 +31,15 @@ void main() {
|
||||||
|
|
||||||
if (a == 0.) t = vec3(0);
|
if (a == 0.) t = vec3(0);
|
||||||
|
|
||||||
vec3 color = mix(vec3(1, 1, 1), t, a * 0.7);
|
vec3 color = mix(vec3(0.53, 0.81, 0.92), 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);
|
||||||
color = mix(matColor.rgb, t, a * 0.3);
|
// per-pixel color variation: +/-4% brightness noise
|
||||||
|
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