u/IAmOneAnxiousDude

Two-species boids in three.js: narwhals hunting fish, with a custom water shader on top

Two-species boids in three.js: narwhals hunting fish, with a custom water shader on top

Built this for the hero of a Godot documentation chatbot I shipped at work (games.surmado.com — Godot mention done, moving on to the three.js).

The interesting bit for this sub is the underwater scene: two species sharing the swim volume with cross-species behavior.

https://preview.redd.it/qj5d2g4ib71h1.png?width=2057&format=png&auto=webp&s=012ed607edf4e26ff3b37eb1ee9037f24684e275

Setup

  • 35 rigged narwhals + 120 rigged fish, both GLB with skinned meshes and a "Swim" animation clip
  • SkeletonUtils.clone() per instance so each boid runs its own AnimationMixer with a random timeScale — no two on the same flap
  • Full 3D flocking (alignment + cohesion + separation summed into a maxAccel cap)
  • Cohesion vector is rotated around the world y-axis by a per-species spiral angle (~60° narwhals, ~83° fish) so dense groups form horizontal cyclones instead of tumbling 3D clouds

Why the spiral, specifically for the fish

First passes had straight cohesion — fish pulled directly at the centroid of their neighbors. The result was spherical clumps, and it just didn't match what I see when I dive. Real fish schools aren't just stuck together matching velocities in one big clump. they're swirling spheres that rotate. There's a tangential component that straight cohesion doesn't capture.

Rotating the cohesion vector around the y-axis by a sinusoidal angle (cos/sin of spiralAngle) lets me bias the pull from "directly at centroid" toward "perpendicular to the radial direction"

Narwhals use a gentler ~60° spiral because their schools are looser and more meandering than fish. Might remove this tbh need to think about it. Fits the fish far better.

Cross-species behavior

  • Fish flee any narwhal within radius 300 with a 1.4× force multiplier. While at least one narwhal is in that radius, the fish's speed cap goes to 1.7× and accel cap to 2.2× — a panic burst.
  • Each non-hunting narwhal has a 0.0009/frame chance to lock onto the nearest fish within radius 450 and steer toward it for 3.5s. Most of the time the school is mellow, but occasionally one narwhal breaks formation and you get a documentary moment.
  • Fish flocking is 5× tighter cohesion + smaller perception radius than narwhals so the school stays compact when fleeing.

Bounded swim volume

  • Sand floor at y=-220, water surface at y=220, with cubic wall-repulsion (force = peak × penetration³) on all six walls so even a panic-bursting fish can't punch through.
  • Three.js fog for the atmospheric depth fade.

Two-canvas pipeline for water effects

  • three.js canvas with preserveDrawingBuffer: true is the source.
  • A second <canvas> on top with a custom WebGL fragment shader samples the three.js canvas as a texture, applies caustic patterns (two scrolling noise samples multiplied + powered to isolate bright filaments) and an atmospheric darken toward the top.
  • Two passes lets me keep the three.js scene clean and do screen-space water effects without bloating the main shader.

Perf

  • Spatial grid (cell size = max same-species perception radius) for the inner-loop neighbor scan.
  • Cross-species checks just iterate the other species — under 6k checks/frame total at these counts.
  • Holds 60fps on desktop with both canvases active.

Looking for feedback on

  • The "documentary hunt" cadence (0.0009/frame ≈ one hunt every ~60s in expectation) — feels about right to me but might be too rare for a marketing hero where most visitors only watch for 10–15 seconds.
  • Caustics work on the close water but I haven't figured out a clean way to project them onto the sand floor cleanly.

Will be adding more as time goes on to make it more immersive and realistic. Great way to kill time at work lmao.

Live: https://games.surmado.com/

reddit.com
u/IAmOneAnxiousDude — 6 hours ago
▲ 15 r/GameDevelopment+2 crossposts

Free Godot 4.6 documentation assistant (+ a silly WebGL narwhal sim on the side)

I just pushed out a free Godot documentation assistant. I've been a casual Godot dev for ~3 years and work with a tech startup (Surmado) where my boss gave me the leeway to ship my personal project as a free tool on the site.

The idea is to help with the gap between "I know what I want to build" and "I know which Godot 4.6 nodes/signals to wire together." Answers in GDScript or C# with citations from the docs.

Pushed it out today, would love to hear what the community thinks and what I should add next.

You'll also catch my predator-prey (narwhal vs fish) simulator made with three.js + some basic Blender assets at the top of the page lol. Gonna keep expanding on this and will continue posting updates.

https://games.surmado.com/

u/IAmOneAnxiousDude — 6 hours ago