ants/src/shaders/antsCompute.frag
Jared Miller 5d25218433
Prevent ants from picking up the surface they walk on
Angle noise (±12° from wander) caused the ahead-cell pickup check
to sample into the sand cell directly below the ant. Ants were
grabbing sand on their first frame of existence.

Fix: skip powder pickup when the ahead cell equals the cell directly
below (the walking surface). Food pickup is still allowed from any
adjacent cell. Diagonal dig pickup still works since the DIG priority
angles ants at ~40° which targets a diagonal neighbor, not directly
below.
2026-03-12 10:16:14 -04:00

373 lines
No EOL
14 KiB
GLSL

precision highp float;
precision highp int;
#define PI 3.1415926535897932384626433832795
in vec2 vUv;
layout(location = 0) out vec4 FragColor;
layout(location = 1) out vec4 FragColorExt;
uniform float uTime;
uniform sampler2D tLastState;
uniform sampler2D tLastExtState;
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.;
const float cellSize = 1. / WORLD_SIZE;
float rand(vec2 co) {
float a = 12.9898;
float b = 78.233;
float c = 43758.5453;
float dt = dot(co.xy ,vec2(a,b));
float sn = mod(dt, 3.14);
return fract(sin(sn) * c);
}
vec2 roundUvToCellCenter(vec2 uv) {
return floor(uv * WORLD_SIZE) / WORLD_SIZE + cellSize * 0.5;
}
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;
}
bool isObstacle(vec2 pos) {
float materialId = texture(tWorld, roundUvToCellCenter(pos)).x;
int matInt = int(materialId);
// ants can only move through air and home cells
return matInt != MAT_AIR && matInt != MAT_HOME;
}
float smell(vec2 pos, float isCarrying) {
vec2 value = texture(tWorld, pos).yz;
if (isCarrying > 0.5) {
return value.y;
}
return value.x;
}
vec2 applyOffsetToPos(vec2 pos, vec2 offsetDirectaion) {
vec2 newPos = clamp(pos + offsetDirectaion * cellSize, 0., 1.);
if (!isObstacle(pos) && isObstacle(newPos)) {
return pos;
}
return newPos;
}
float getMaxScentStorage(vec2 antDataUv) {
float factor = 0.8 + rand(antDataUv * 100.) * 0.2;
return SCENT_MAX_STORAGE * factor;
}
void main() {
vec4 lastState = texture(tLastState, vUv);
vec4 lastExtState = texture(tLastExtState, vUv);
float noise = rand(vUv * 1000. + fract(uTime / 1000.));
vec2 pos = lastState.xy;
float angle = lastState.z;
float isCarrying = float(int(lastState.w) & 1);
float storage = float(int(lastState.w) >> 1);
bool wasObstacle = isObstacle(pos);
float personality = lastExtState.r;
float cargoMaterialId = lastExtState.g;
float pathIntDx = lastExtState.b;
float pathIntDy = lastExtState.a;
// calculate this ant's index from its UV position in the texture
ivec2 texSize = textureSize(tLastState, 0);
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
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;
}
// 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.;
}
}
// --- GRAVITY AND SURFACE CHECK ---
vec2 cellCenter = roundUvToCellCenter(pos);
float belowMat = texture(tWorld, cellCenter - vec2(0., cellSize)).x;
float currentMat = texture(tWorld, cellCenter).x;
bool belowSolid = (int(belowMat) != MAT_AIR);
// collision displacement: if current cell is now solid (sand fell on us), push to nearest air
if (int(currentMat) != MAT_AIR && int(currentMat) != MAT_HOME) {
vec2 upPos = cellCenter + vec2(0., cellSize);
vec2 leftPos = cellCenter - vec2(cellSize, 0.);
vec2 rightPos = cellCenter + vec2(cellSize, 0.);
if (int(texture(tWorld, upPos).x) == MAT_AIR) {
pos = upPos;
} else if (int(texture(tWorld, leftPos).x) == MAT_AIR) {
pos = leftPos;
} else if (int(texture(tWorld, rightPos).x) == MAT_AIR) {
pos = rightPos;
}
cellCenter = roundUvToCellCenter(pos);
belowMat = texture(tWorld, cellCenter - vec2(0., cellSize)).x;
belowSolid = (int(belowMat) != MAT_AIR);
}
bool isFalling = false;
// GRAVITY: if nothing solid below and not at world bottom, fall multiple cells
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;
}
isFalling = true;
}
if (!isFalling) {
bool acted = false;
// ---- PRIORITY 1: DEPOSIT ----
if (!acted && isCarrying == 1.) {
float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x;
// carrying food and at home -> drop food
if (int(cargoMaterialId) == MAT_FOOD && tryDropFood(pos)) {
isCarrying = 0.;
cargoMaterialId = 0.;
angle += PI;
storage = getMaxScentStorage(vUv);
acted = true;
}
// carrying powder and on surface (air cell, solid below) -> deposit
if (!acted && int(cargoMaterialId) != MAT_FOOD) {
if (int(cellMatId) == MAT_AIR) {
vec2 belowPos = pos - vec2(0., cellSize);
float belowMatId = texture(tWorld, roundUvToCellCenter(belowPos)).x;
vec2 abovePos = pos + vec2(0., cellSize);
float aboveMatId = texture(tWorld, roundUvToCellCenter(abovePos)).x;
if ((int(belowMatId) != MAT_AIR || belowPos.y <= 0.)
&& int(aboveMatId) == MAT_AIR) {
isCarrying = 0.;
// keep cargoMaterialId set so discretize reads it this frame
angle += PI;
storage = getMaxScentStorage(vUv);
acted = true;
}
}
}
}
// ---- PRIORITY 2: NAVIGATE (carrying) ----
if (!acted && isCarrying == 1.) {
if (int(cargoMaterialId) == MAT_FOOD) {
// follow toHome pheromone
float sAhead = smell(applyOffsetToPos(pos, vec2(cos(angle), sin(angle)) * sampleDistance), 1.);
float sLeft = smell(applyOffsetToPos(pos, vec2(cos(angle - ANT_ROTATION_ANGLE), sin(angle - ANT_ROTATION_ANGLE)) * sampleDistance), 1.);
float sRight = smell(applyOffsetToPos(pos, vec2(cos(angle + ANT_ROTATION_ANGLE), sin(angle + ANT_ROTATION_ANGLE)) * sampleDistance), 1.);
if (sLeft > sAhead && sLeft > sRight) {
angle -= ANT_ROTATION_ANGLE;
} else if (sRight > sAhead && sRight > sLeft) {
angle += ANT_ROTATION_ANGLE;
}
} else {
// carrying powder: bias upward toward surface
#if VIEW_MODE_SIDE
float upwardBias = PI * 0.5;
float angleDiff = upwardBias - angle;
angleDiff = mod(angleDiff + PI, 2.0 * PI) - PI;
angle += angleDiff * 0.3;
#endif
}
// add wander noise to carrying ants too
float noise2 = rand(vUv * 1000. + fract(uTime / 1000.) + 0.2);
if (noise2 > 0.5) {
angle += ANT_ROTATION_ANGLE;
} else {
angle -= ANT_ROTATION_ANGLE;
}
acted = true;
}
// ---- PRIORITY 3: FORAGE (not carrying, food scent detected) ----
if (!acted && isCarrying == 0.) {
float sAhead = smell(applyOffsetToPos(pos, vec2(cos(angle), sin(angle)) * sampleDistance), 0.);
float sLeft = smell(applyOffsetToPos(pos, vec2(cos(angle - ANT_ROTATION_ANGLE), sin(angle - ANT_ROTATION_ANGLE)) * sampleDistance), 0.);
float sRight = smell(applyOffsetToPos(pos, vec2(cos(angle + ANT_ROTATION_ANGLE), sin(angle + ANT_ROTATION_ANGLE)) * sampleDistance), 0.);
float maxSmell = max(sAhead, max(sLeft, sRight));
if (maxSmell > SCENT_THRESHOLD) {
if (sLeft > sAhead && sLeft > sRight) {
angle -= ANT_ROTATION_ANGLE;
} else if (sRight > sAhead && sRight > sLeft) {
angle += ANT_ROTATION_ANGLE;
}
acted = true;
}
}
// ---- PRIORITY 4: DIG (not carrying, diggable material nearby below surface) ----
#if VIEW_MODE_SIDE
if (!acted && isCarrying == 0.) {
vec2 belowUv = roundUvToCellCenter(pos - vec2(0., cellSize));
float belowMat2 = texture(tWorld, belowUv).x;
vec4 belowProps2 = texelFetch(uMaterialProps, ivec2(int(belowMat2), 0), 0);
// suppress digging if on the surface (air above) — don't dig topsoil into sky
vec2 aboveUv = roundUvToCellCenter(pos + vec2(0., cellSize));
float aboveMat2 = texture(tWorld, aboveUv).x;
bool onSurfaceTop = (int(aboveMat2) == MAT_AIR);
if (!onSurfaceTop
&& belowProps2.r == BEHAVIOR_POWDER
&& belowProps2.b <= ANT_CARRY_STRENGTH) {
// bias angle toward ~40 degrees below horizontal (angle of repose)
float targetAngle = (cos(angle) >= 0.)
? -0.7 // ~-40 degrees (right and down)
: -(PI - 0.7); // ~-(180-40) degrees (left and down)
float angleDiff2 = targetAngle - angle;
angleDiff2 = mod(angleDiff2 + PI, 2.0 * PI) - PI;
angle += angleDiff2 * 0.2;
acted = true;
}
}
#endif
// ---- PRIORITY 5: WANDER (fallback) ----
if (!acted) {
if (noise < 0.33) {
angle += ANT_ROTATION_ANGLE;
} else if (noise < 0.66) {
angle -= ANT_ROTATION_ANGLE;
}
float noise2 = rand(vUv * 1000. + fract(uTime / 1000.) + 0.2);
if (noise2 > 0.5) {
angle += ANT_ROTATION_ANGLE * 2.;
} else {
angle -= ANT_ROTATION_ANGLE * 2.;
}
}
// ---- MOVEMENT APPLICATION ----
vec2 offset = vec2(cos(angle), sin(angle));
pos = applyOffsetToPos(pos, offset);
if (fract(pos.x) == 0. || fract(pos.y) == 0. || (!wasObstacle && isObstacle(pos + offset * cellSize))) {
angle += PI * (noise - 0.5);
}
// ---- PICKUP (check cell ahead — ants walk in air, can't enter solid cells) ----
if (isCarrying == 0.) {
vec2 aheadUv = roundUvToCellCenter(pos + vec2(cos(angle), sin(angle)) * cellSize);
float aheadMatId = texture(tWorld, aheadUv).x;
int aheadMatInt = int(aheadMatId);
// food: pick up from any adjacent cell
if (aheadMatInt == MAT_FOOD) {
isCarrying = 1.;
cargoMaterialId = aheadMatId;
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);
}
}
}
}
} // end !isFalling
FragColor = vec4(
pos.x,
pos.y,
angle,
float((uint(max(storage - SCENT_PER_MARKER, 0.)) << 1) + uint(isCarrying))
);
FragColorExt = vec4(personality, cargoMaterialId, pathIntDx, pathIntDy);
}