Skip to content

Case Study2026-06-113 min read

Building the Manta Engine: A WebGL Navigation System, Rebuilt

Rewriting this site's 3D navigation from a legacy vanilla Three.js iframe into a GPU-driven react-three-fiber scene — instanced manta rays, shader-based animation, and a WebGL UI you can drive with a keyboard.

Role

Design & Engineering

Timeline

2026

Stack

three.js · react-three-fiber · drei · GLSL · zustand

Outcomes

  • ~47 animated creatures rendered in 3 draw calls (down from one draw call per manta)
  • Vertex animation moved entirely to the GPU — zero per-frame geometry uploads
  • Quality tiers + runtime fps governor keep the scene smooth from integrated GPUs to desktops
  • Full keyboard accessibility and a prefers-reduced-motion mode for a WebGL scene

The legacy system

The first version of this site's navigation was a school of manta rays swimming through space — built in vanilla Three.js 0.128, served inside an iframe, and held together with optimism. It looked good. It was also a museum of anti-patterns:

  • CPU vertex animation. Every frame, JavaScript looped over roughly 7,800 vertices, computed a wing-flap deformation, wrote the results into the position buffer, and re-uploaded it to the GPU. Per manta. The main thread spent its life doing work a vertex shader does for free.
  • The iframe wall. The 3D scene lived in a separate document, so every interaction — navigation clicks, hover states, locale changes — crossed a postMessage bridge. Internationalizing the HUD meant serializing translation payloads into the iframe and hoping both sides agreed on the schema.
  • No instancing, no tiers, no accessibility. Each creature was its own mesh and draw call. The scene ran the same workload on a four-year-old phone as on a desktop GPU. And being a WebGL canvas in an iframe, it was completely invisible to keyboard and screen-reader users.

The rebuild had one rule: the new system had to live inside the React tree as a first-class citizen — same router, same i18n context, same state — and the GPU had to do the animating.

GPU wing-flap: animation as a shader

The signature wing-flap moved from a JavaScript vertex loop into a GLSL vertex shader. Each vertex displaces as a function of its distance from the body's spine, a phase uniform, and per-instance attributes — flap frequency, amplitude, phase offset, and size — so every manta in the school beats its wings on its own rhythm without any per-creature JavaScript.

Because the deformation is computed from the rest pose every frame, there is no geometry upload at all after initialization. The entire school animates from a handful of uniform updates.

Three draw calls for a living scene

The ~47 creatures in the scene render as instanced meshes — the full school costs 3 draw calls (one per creature archetype). Per-instance attribute buffers carry each individual's flight parameters, tint, and animation phase, so instancing doesn't mean uniformity: every creature is distinct, but the GPU sees three batched draws.

Layout is generated by a seeded PRNG, which sounds like a small detail but does a lot of work: the school is "random" yet deterministic, so every visitor sees the same composition, visual regressions are diffable, and hydration never mismatches.

Iridescence and selective bloom

The mantas' rims carry an iridescent fresnel term — view-angle-dependent color that shifts as they bank. The trick is that the fresnel rim is pushed above luminance 1.0, and the bloom pass thresholds at exactly 1.0. The result is selective bloom with no extra render targets or layer juggling: only the iridescent rims glow, because only they cross the threshold.

Flight that feels like flight

Creatures follow curved paths, but the motion sells because of banking: acceleration is estimated by finite differences along the path, and each creature rolls into its turns proportionally to lateral acceleration — the way a real wing-borne body does. It is a few lines of math that separates "objects on rails" from "things that are flying."

Performance as a system, not a hope

Device capability is detected up front with detect-gpu and mapped to quality tiers (geometry density, bloom resolution, creature count). On top of that sits a runtime fps governor that watches frame times and steps the tier down if a device underdelivers its benchmark class — covering thermal throttling, browser tabs fighting for the GPU, and benchmarks that lie.

Accessibility in a WebGL scene

Two things most 3D sites skip:

  • prefers-reduced-motion switches the scene to a beauty-shot mode — a composed, nearly still rendering rather than a swirling school — instead of just ignoring the preference.
  • Keyboard navigation works through hidden DOM buttons overlaid on the scene graph: each navigable destination in the 3D space has a real focusable element, so Tab/Enter drive the WebGL camera the same way a pointer does. Screen readers see a normal navigation; sighted keyboard users see the scene respond.

What it replaced

The old iframe, the postMessage i18n bridge, the per-frame vertex uploads, and the duplicate state layer are all gone. The manta engine is now a react-three-fiber component tree reading the same zustand store and next-intl context as the rest of the site — one application, three draw calls, sixty frames.

Reach me on WhatsApp
prefer email?
© 2026 Curtis Forbes