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
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -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
|
||||
2. `WorldBlurScene` — diffuse + decay pheromones (3 channels: toHome, toFood, repellent, blocked by solid cells)
|
||||
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
|
||||
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
|
||||
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); passes ant spawn position to AntsComputeScene
|
||||
7. `ColonyStats` — CPU readback of ant texture, computes aggregate stats (foragerRatio), feeds back as uniforms
|
||||
8. `DrawScene` — user painting with material palette
|
||||
9. `ScreenScene` — final composited output with side/top camera views (V to toggle)
|
||||
|
||||
### 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 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
|
||||
- `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
|
||||
|
||||
- `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/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 (reports active/budget)
|
||||
- `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/Config.ts` — simulation parameters (per-channel pheromone configs)
|
||||
- `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
|
||||
- `src/StatsOverlay.ts` — on-screen stats display (cursor position, TPS, colony info)
|
||||
- `src/materials/types.ts` — Material interface and BehaviorType constants
|
||||
- `src/materials/registry.ts` — MaterialRegistry with 6 built-in materials
|
||||
- `src/materials/lookupTexture.ts` — builds GPU lookup textures from registry
|
||||
- `src/sand/margolus.ts` — Margolus block offset calculation
|
||||
- `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/world.frag` — material ID preservation + pheromone merging
|
||||
- `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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
"food (W)": MAT_FOOD,
|
||||
"home (Q)": MAT_HOME,
|
||||
"ants (A)": 999,
|
||||
};
|
||||
|
||||
// proxy object for lil-gui string dropdown — initialize from saved config
|
||||
|
|
|
|||
|
|
@ -47,10 +47,6 @@ export default class Renderer {
|
|||
|
||||
constructor(public canvas: HTMLCanvasElement) {
|
||||
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();
|
||||
|
||||
|
|
@ -185,7 +181,6 @@ export default class Renderer {
|
|||
);
|
||||
scenes.sandPhysics.material.uniforms.uFrame.value = this.frameCounter;
|
||||
this.renderer.render(scenes.sandPhysics, scenes.sandPhysics.camera);
|
||||
|
||||
this.frameCounter++;
|
||||
|
||||
this.setViewportFromRT(this.resources.worldBlurredRenderTarget);
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export default class StatsOverlay {
|
|||
this.tpsEl.textContent = `${Math.round(tps)}`;
|
||||
}
|
||||
|
||||
this.antsEl.textContent = `${colonyStats.totalAnts} / ${Config.antBudget}`;
|
||||
this.antsEl.textContent = `${colonyStats.totalAnts}`;
|
||||
const carrying = Math.round(
|
||||
colonyStats.foragerRatio * colonyStats.totalAnts,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const BUILTIN_MATERIALS: Material[] = [
|
|||
name: "air",
|
||||
behavior: BEHAVIOR_GAS,
|
||||
density: 0,
|
||||
color: [0.53, 0.81, 0.92, 1.0],
|
||||
color: [0, 0, 0, 0],
|
||||
hardness: 0,
|
||||
angleOfRepose: 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import fragmentShaderGround from "../shaders/screenWorld.frag";
|
|||
import vertexShaderGround from "../shaders/screenWorld.vert";
|
||||
import AbstractScene from "./AbstractScene";
|
||||
|
||||
export const BRUSH_ANTS = 999;
|
||||
|
||||
export default class ScreenScene extends AbstractScene {
|
||||
public readonly camera: THREE.OrthographicCamera;
|
||||
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
|
||||
public get effectiveDrawMode(): number {
|
||||
if (this.drawMode === BRUSH_ANTS) return -1;
|
||||
if (this.drawMode >= 0) return this.drawMode;
|
||||
if (this.isPointerDown && Config.brushMaterial >= 0) {
|
||||
if (Config.brushMaterial === BRUSH_ANTS) return -1;
|
||||
if (this.isPointerDown && Config.brushMaterial >= 0)
|
||||
return Config.brushMaterial;
|
||||
}
|
||||
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 renderHeight: number = 1;
|
||||
|
||||
constructor(renderer: Renderer) {
|
||||
super(renderer);
|
||||
|
||||
const groundDefines = this.renderer.getCommonMaterialDefines();
|
||||
|
||||
const ground = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(1, 1),
|
||||
new THREE.ShaderMaterial({
|
||||
|
|
@ -72,7 +58,7 @@ export default class ScreenScene extends AbstractScene {
|
|||
},
|
||||
vertexShader: vertexShaderGround,
|
||||
fragmentShader: fragmentShaderGround,
|
||||
defines: groundDefines,
|
||||
defines: this.renderer.getCommonMaterialDefines(),
|
||||
glslVersion: THREE.GLSL3,
|
||||
}),
|
||||
);
|
||||
|
|
@ -101,7 +87,6 @@ export default class ScreenScene extends AbstractScene {
|
|||
vertexShader: vertexShaderAnts,
|
||||
fragmentShader: fragmentShaderAnts,
|
||||
transparent: true,
|
||||
glslVersion: THREE.GLSL3,
|
||||
});
|
||||
|
||||
this.createInstancedAntsMesh();
|
||||
|
|
@ -203,10 +188,6 @@ export default class ScreenScene extends AbstractScene {
|
|||
this.drawMode = MAT_DIRT;
|
||||
break;
|
||||
}
|
||||
case "KeyA": {
|
||||
this.drawMode = BRUSH_ANTS;
|
||||
break;
|
||||
}
|
||||
case "KeyV": {
|
||||
Config.viewMode =
|
||||
Config.viewMode === "side" ? "top" : "side";
|
||||
|
|
@ -295,16 +276,7 @@ export default class ScreenScene extends AbstractScene {
|
|||
this.renderHeight = height;
|
||||
}
|
||||
|
||||
public update() {
|
||||
if (this.isAntBrushActive && this.isPointerDown) {
|
||||
this.antSpawnRequest.set(
|
||||
this.pointerPosition.x,
|
||||
this.pointerPosition.y,
|
||||
Config.brushRadius,
|
||||
1, // flag: spawn requested
|
||||
);
|
||||
}
|
||||
}
|
||||
public update() {}
|
||||
|
||||
public applyCameraZoom() {
|
||||
this.updateCameraZoom();
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ precision highp int;
|
|||
|
||||
#define PI 3.1415926535897932384626433832795
|
||||
|
||||
out vec2 vUv;
|
||||
out float vIsCarryingFood;
|
||||
varying vec2 vUv;
|
||||
varying float vIsCarryingFood;
|
||||
|
||||
uniform sampler2D tData;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ uniform sampler2D tWorld;
|
|||
uniform sampler2D tPresence;
|
||||
uniform float uForagerRatio;
|
||||
uniform sampler2D uMaterialProps;
|
||||
uniform vec4 uAntSpawn;
|
||||
|
||||
const float ANT_CARRY_STRENGTH = 1.0;
|
||||
const float sampleDistance = 20.;
|
||||
|
|
@ -35,9 +34,9 @@ vec2 roundUvToCellCenter(vec2 uv) {
|
|||
}
|
||||
|
||||
bool tryDropFood(vec2 pos) {
|
||||
float currentMatId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
||||
float belowMatId = texture(tWorld, roundUvToCellCenter(pos - vec2(0., cellSize))).x;
|
||||
return int(currentMatId) == MAT_HOME || int(belowMatId) == MAT_HOME;
|
||||
float materialId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
||||
|
||||
return int(materialId) == MAT_HOME;
|
||||
}
|
||||
|
||||
bool isObstacle(vec2 pos) {
|
||||
|
|
@ -95,68 +94,38 @@ void main() {
|
|||
int antIndex = int(vUv.y * float(texSize.y)) * texSize.x + int(vUv.x * float(texSize.x));
|
||||
|
||||
if (pos == vec2(0)) { // init new ant
|
||||
bool spawnRequested = (uAntSpawn.w > 0.5);
|
||||
|
||||
if (antIndex >= ANTS_START_COUNT && !spawnRequested) {
|
||||
// dormant ant, no spawn request — skip
|
||||
// only activate ants within the start count
|
||||
if (antIndex >= ANTS_START_COUNT) {
|
||||
// dormant ant — output zeros and skip everything
|
||||
FragColor = vec4(0);
|
||||
FragColorExt = vec4(0);
|
||||
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;
|
||||
#if VIEW_MODE_SIDE
|
||||
// spawn on sand surface: random X, scan down from top to find first non-air cell
|
||||
float spawnX = rand(vUv * 10000.);
|
||||
float pixelSize = 1.0 / float(textureSize(tWorld, 0).x);
|
||||
float surfaceY = 0.6; // fallback if scan finds nothing
|
||||
for (float scanY = 1.0; scanY > 0.0; scanY -= pixelSize) {
|
||||
float matId = texture(tWorld, vec2(spawnX, scanY)).x;
|
||||
if (matId > 0.5) { // non-air cell found
|
||||
surfaceY = scanY + pixelSize; // one cell above the surface
|
||||
break;
|
||||
}
|
||||
// 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
|
||||
// spawn on sand surface: random X, scan down from top to find first non-air cell
|
||||
float spawnX = rand(vUv * 10000.);
|
||||
float pixelSize = 1.0 / float(textureSize(tWorld, 0).x);
|
||||
float surfaceY = 0.6; // fallback if scan finds nothing
|
||||
for (float scanY = 1.0; scanY > 0.0; scanY -= pixelSize) {
|
||||
float matId = texture(tWorld, vec2(spawnX, scanY)).x;
|
||||
if (matId > 0.5) { // non-air cell found
|
||||
surfaceY = scanY + pixelSize; // one cell above the surface
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos = vec2(spawnX, surfaceY);
|
||||
angle = (rand(vUv * 31337.) > 0.5) ? 0.0 : PI; // face randomly left or right
|
||||
#else
|
||||
pos = vec2(0.5);
|
||||
angle = rand(vUv * 10000.) * 2. * PI;
|
||||
#endif
|
||||
isCarrying = 0.;
|
||||
storage = 0.;
|
||||
personality = rand(vUv * 42069.); // 0.0 = pure follower, 1.0 = pure explorer
|
||||
cargoMaterialId = 0.;
|
||||
pathIntDx = 0.;
|
||||
pathIntDy = 0.;
|
||||
}
|
||||
pos = vec2(spawnX, surfaceY);
|
||||
angle = -PI * 0.5; // face downward initially
|
||||
#else
|
||||
pos = vec2(0.5);
|
||||
angle = rand(vUv * 10000.) * 2. * PI;
|
||||
#endif
|
||||
isCarrying = 0.;
|
||||
storage = 0.;
|
||||
personality = rand(vUv * 42069.); // 0.0 = pure follower, 1.0 = pure explorer
|
||||
cargoMaterialId = 0.;
|
||||
pathIntDx = 0.;
|
||||
pathIntDy = 0.;
|
||||
}
|
||||
|
||||
// --- GRAVITY AND SURFACE CHECK ---
|
||||
|
|
@ -185,14 +154,9 @@ void main() {
|
|||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -331,33 +295,25 @@ void main() {
|
|||
angle += PI * (noise - 0.5);
|
||||
}
|
||||
|
||||
// ---- PICKUP (check cell ahead — ants walk in air, can't enter solid cells) ----
|
||||
// ---- PICKUP ----
|
||||
if (isCarrying == 0.) {
|
||||
vec2 aheadUv = roundUvToCellCenter(pos + vec2(cos(angle), sin(angle)) * cellSize);
|
||||
float aheadMatId = texture(tWorld, aheadUv).x;
|
||||
int aheadMatInt = int(aheadMatId);
|
||||
float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x;
|
||||
int cellMatInt = int(cellMatId);
|
||||
|
||||
// food: pick up from any adjacent cell
|
||||
if (aheadMatInt == MAT_FOOD) {
|
||||
if (cellMatInt == MAT_FOOD) {
|
||||
isCarrying = 1.;
|
||||
cargoMaterialId = aheadMatId;
|
||||
cargoMaterialId = cellMatId;
|
||||
angle += PI;
|
||||
storage = getMaxScentStorage(vUv);
|
||||
} else if (aheadMatInt != MAT_AIR && aheadMatInt != MAT_HOME) {
|
||||
// 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 hardness = props.b;
|
||||
if (behavior == BEHAVIOR_POWDER && hardness <= ANT_CARRY_STRENGTH) {
|
||||
isCarrying = 1.;
|
||||
cargoMaterialId = aheadMatId;
|
||||
angle += PI;
|
||||
storage = getMaxScentStorage(vUv);
|
||||
}
|
||||
} else if (cellMatInt != MAT_AIR && cellMatInt != MAT_HOME) {
|
||||
vec4 props = texelFetch(uMaterialProps, ivec2(cellMatInt, 0), 0);
|
||||
float behavior = props.r;
|
||||
float hardness = props.b;
|
||||
if (behavior == BEHAVIOR_POWDER && hardness <= ANT_CARRY_STRENGTH) {
|
||||
isCarrying = 1.;
|
||||
cargoMaterialId = cellMatId;
|
||||
angle += PI;
|
||||
storage = getMaxScentStorage(vUv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,6 @@ out vec4 FragColor;
|
|||
uniform sampler2D map;
|
||||
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() {
|
||||
vec4 value = texture(map, vUv);
|
||||
|
||||
|
|
@ -31,15 +25,12 @@ void main() {
|
|||
|
||||
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
|
||||
if (materialId != MAT_AIR) {
|
||||
vec4 matColor = texelFetch(uMaterialColors, ivec2(materialId, 0), 0);
|
||||
// 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);
|
||||
color = mix(matColor.rgb, t, a * 0.3);
|
||||
}
|
||||
|
||||
FragColor = vec4(color, 1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue