From a7aeb064b3c82cad804103a5817f3dfff6b7d512 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Wed, 11 Mar 2026 20:53:22 -0400 Subject: [PATCH] Rewrite ant behavior as priority stack with suppressors --- src/shaders/antsCompute.frag | 289 +++++++++++++++++------------------ 1 file changed, 136 insertions(+), 153 deletions(-) diff --git a/src/shaders/antsCompute.frag b/src/shaders/antsCompute.frag index e52b3c0..cdf8b60 100644 --- a/src/shaders/antsCompute.frag +++ b/src/shaders/antsCompute.frag @@ -89,8 +89,6 @@ void main() { float pathIntDx = lastExtState.b; float pathIntDy = lastExtState.a; - bool movementProcessed = false; - // 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)); @@ -163,177 +161,162 @@ void main() { } if (!isFalling) { + bool acted = false; - if (isCarrying == 0.) { - if (noise < 0.33) { - vec2 offset = vec2(cos(angle), sin(angle)) * sampleDistance; - vec2 point = applyOffsetToPos(pos, offset); - - if (tryGetFood(point)) { - movementProcessed = true; - } - } else if (noise < 0.66) { - float newAngle = angle - ANT_ROTATION_ANGLE; - vec2 offset = vec2(cos(newAngle), sin(newAngle)) * sampleDistance; - vec2 point = applyOffsetToPos(pos, offset); - - if (tryGetFood(point)) { - movementProcessed = true; - angle = newAngle; - } - } else { - float newAngle = angle + ANT_ROTATION_ANGLE; - vec2 offset = vec2(cos(newAngle), sin(newAngle)) * sampleDistance; - vec2 point = applyOffsetToPos(pos, offset); - - if (tryGetFood(point)) { - movementProcessed = true; - angle = newAngle; - } - } - } else if (isCarrying == 1.) { - if (noise < 0.33) { - vec2 offset = vec2(cos(angle), sin(angle)) * sampleDistance; - vec2 point = applyOffsetToPos(pos, offset); - - if (tryDropFood(point)) { - movementProcessed = true; - } - } else if (noise < 0.66) { - float newAngle = angle - ANT_ROTATION_ANGLE; - vec2 offset = vec2(cos(newAngle), sin(newAngle)) * sampleDistance; - vec2 point = applyOffsetToPos(pos, offset); - - if (tryDropFood(point)) { - movementProcessed = true; - angle = newAngle; - } - } else { - float newAngle = angle + ANT_ROTATION_ANGLE; - vec2 offset = vec2(cos(newAngle), sin(newAngle)) * sampleDistance; - vec2 point = applyOffsetToPos(pos, offset); - - if (tryDropFood(point)) { - movementProcessed = true; - angle = newAngle; - } - } - } - - if (!movementProcessed) { - #if VIEW_MODE_SIDE - // vertical bias for digging behavior - if (isCarrying == 1. && int(cargoMaterialId) != MAT_FOOD) { - // carrying powder: bias upward toward surface - float upwardBias = PI * 0.5; // straight up - float angleDiff = upwardBias - angle; - // normalize to [-PI, PI] - angleDiff = mod(angleDiff + PI, 2.0 * PI) - PI; - // gentle steering toward up (blend 30% toward upward) - angle += angleDiff * 0.3; - } else if (isCarrying == 0.) { - // not carrying: check if surrounded by diggable material - // if so, prefer downward movement + // ---- PRIORITY 1: DEPOSIT ---- + if (!acted && isCarrying == 1.) { float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x; - vec4 props = texelFetch(uMaterialProps, ivec2(int(cellMatId), 0), 0); - // check cell below for diggable material - vec2 belowUv = roundUvToCellCenter(pos - vec2(0., cellSize)); - float belowDigMat = texture(tWorld, belowUv).x; - vec4 belowProps = texelFetch(uMaterialProps, ivec2(int(belowDigMat), 0), 0); + // 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; + } - if (belowProps.r == BEHAVIOR_POWDER && belowProps.b <= ANT_CARRY_STRENGTH) { - // diggable material below — bias downward - float downwardBias = -PI * 0.5; // straight down - float angleDiff = downwardBias - angle; + // 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; - // gentle steering toward down (20% blend) - angle += angleDiff * 0.2; + 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 - float noise2 = rand(vUv * 1000. + fract(uTime / 1000.) + 0.2); - - float sampleAhead = smell(applyOffsetToPos(pos, vec2(cos(angle), sin(angle)) * sampleDistance), isCarrying); - float sampleLeft = smell(applyOffsetToPos(pos, vec2(cos(angle - ANT_ROTATION_ANGLE), sin(angle - ANT_ROTATION_ANGLE)) * sampleDistance), isCarrying); - float sampleRight = smell(applyOffsetToPos(pos, vec2(cos(angle + ANT_ROTATION_ANGLE), sin(angle + ANT_ROTATION_ANGLE)) * sampleDistance), isCarrying); - - if (sampleAhead > sampleLeft && sampleAhead > sampleRight) { - // don't change direction - } else if (sampleLeft > sampleAhead && sampleLeft > sampleRight) { - angle -= ANT_ROTATION_ANGLE; // steer left - } else if (sampleRight > sampleAhead && sampleRight > sampleLeft) { - angle += ANT_ROTATION_ANGLE; // steer right - } else if (noise < 0.33) { - angle += ANT_ROTATION_ANGLE; // no smell detected, do random movement - } else if (noise < 0.66) { - angle -= ANT_ROTATION_ANGLE; + // ---- 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.; + } } - 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); } - } - vec2 offset = vec2(cos(angle), sin(angle)); - pos = applyOffsetToPos(pos, offset); + // ---- PICKUP ---- + if (isCarrying == 0.) { + float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x; + int cellMatInt = int(cellMatId); - if (fract(pos.x) == 0. || fract(pos.y) == 0. || (!wasObstacle && isObstacle(pos + offset * cellSize))) { - angle += PI * (noise - 0.5); - } - - if (isCarrying == 0.) { - float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x; - int cellMatInt = int(cellMatId); - - if (cellMatInt == MAT_FOOD) { - // food pickup (existing foraging behavior) - isCarrying = 1.; - cargoMaterialId = cellMatId; - angle += PI; - storage = getMaxScentStorage(vUv); - } else if (cellMatInt != MAT_AIR && cellMatInt != MAT_HOME) { - // check if diggable powder material - vec4 props = texelFetch(uMaterialProps, ivec2(cellMatInt, 0), 0); - float behavior = props.r; - float hardness = props.b; - if (behavior == BEHAVIOR_POWDER && hardness <= ANT_CARRY_STRENGTH) { + if (cellMatInt == MAT_FOOD) { isCarrying = 1.; cargoMaterialId = cellMatId; 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); + } } } - } - - if (tryDropFood(pos)) { - storage = getMaxScentStorage(vUv); - - if (isCarrying == 1.) { - isCarrying = 0.; - cargoMaterialId = 0.; - angle += PI; - } - } - - // deposit carried powder material when standing on air with solid ground below - if (isCarrying == 1. && int(cargoMaterialId) != MAT_FOOD) { - float cellMatId = texture(tWorld, roundUvToCellCenter(pos)).x; - if (int(cellMatId) == MAT_AIR) { - vec2 belowPos = pos - vec2(0., cellSize); - float belowMatId = texture(tWorld, roundUvToCellCenter(belowPos)).x; - if (int(belowMatId) != MAT_AIR || belowPos.y <= 0.) { - isCarrying = 0.; - // keep cargoMaterialId set so discretize can read it this frame - angle += PI; - storage = getMaxScentStorage(vUv); - } - } - } - } // end !isFalling FragColor = vec4(