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.
373 lines
No EOL
14 KiB
GLSL
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);
|
|
} |