Try it live About this project
ANOMALY is an archive of sixteen real animals, each built around one structure that looks like it should not work: the transparent fluid-filled dome over the barreleye's skull, the mantis shrimp's cavitation-bubble strike, the ocean sunfish's missing tail fin. Each record, A-001 through A-016, carries its own Tripo model to drag, zoom and reset.
The collection is filed by the anomaly itself rather than by taxonomy — transparency, vision, bioluminescence, camouflage, predation, gigantism and body plan, plus an orthogonal deep-sea cut — and one specimen can sit under several categories at once: the barreleye's dome and its rotating tubular eyes are two separate anomalies on one animal. That is why each record leads with the anatomical structure instead of the species name. Everything is locked to a single screen: picking a category, a carousel card or a search result swaps content in place, with the archive plate, observations and a collapsible field note on the left, the 3D stage in the centre, and species data plus a range map on the right.
Highlights
- Pick any of the sixteen records, 01–16, from the bottom rail and the stage plus both side panels swap in place; drag to turn, scroll to zoom, and every switch resets the camera so all sixteen specimens present at a comparable size
- Click the archive plate and it morphs out of its own thumbnail into a backlit viewing box — the halo is light escaping past the plate's edge; magnify up to 3.2× and drag to pan
- Filter by the anomaly itself — transparency, vision, bioluminescence, camouflage, predation, gigantism, body plan — plus an orthogonal deep-sea cut matched on habitat; one specimen can sit under several categories at once, and search spans species names, Latin names, structures and specimen IDs
- While a model downloads, GSAP draws the specimen's measured line plate stroke by stroke as a caliper travels its rule — the blueprint arrives before the body, then cross-fades into the 3D build; even a cached model holds the plate for 1.5 seconds so it never flashes past unread
Technical breakdown
3D assets · Taking Tripo high-poly models to the web
The sixteen specimen models are generated with Tripo; the originals run ~60 MB and ~1.9M triangles each — sculpt-grade density that cannot ship. A gltf-transform meshopt pipeline takes the geometry to 2.4–11 MB: welded, simplified at an error tolerance of 0.0002, pruned and meshopt-compressed. Textures deliberately stay at 4K rather than downsampled — downsampling saved little and was the main cause of softness — while anything delivered at 8K comes down to 4K, cutting VRAM from roughly 358 MB to 89 MB per base colour map with no visible loss at on-screen size.
Every specimen degrades independently, GLB → still → cutout → hand-authored line plate, with a drafting grid behind even a missing plate, so no missing file can blank the stage.
Specimen stage · Auto-framing and context protection
The sixteen models were not authored to a shared convention and none is hand-transformed: each is measured at load against its own root matrix — measuring in world space reads back the scale already applied, shrinking the model on every re-measure. Measurement uses percentile bounds rather than a raw Box3: several of these animals have antennae and filaments that carry a tiny fraction of the vertices yet would define the box and shrink the body; trimming the extremes boxes the mass of the animal, so all sixteen present at a comparable visual weight. The automatic fit turns the longest horizontal axis across the screen, centres it and fills the canvas — a new specimen drops into the catalogue and frames itself, with at most a per-specimen nudge (a clam is a lid, not a profile).
The Canvas is not keyed on the specimen: that would tear down and rebuild the WebGL context per switch, and browsers drop the oldest live context. Only the inner Scene is keyed. The primitive also requires dispose={null} — drei's GLTF cache shares geometry between clones, and letting R3F dispose on unmount destroys buffers still in use.
Lighting · Lit like a vitrine, not a copy stand
One modelling key, two grazing rims from behind, and no floor bounce — most of these animals are dark, and an edge is what separates a dark body from a dark ground, which is the rims' whole job. The environment map is baked in place from area lights inside the scene (the Environment sits outside the keyed Scene, so switching specimens never re-bakes it), with no HDRI download and no external request.
Relighting alone could not separate the darkest specimens — a dark red-brown squid and a warm brown ground are genuinely close in value — so the specimen stands in a soft pool of light and the contact shadow bites into that pool, which is what opens the silhouette. Tone mapping is Khronos PBR Neutral at 1.34 exposure rather than the ACES default, which rolls midtones down hard — exactly what buried these specimens.
Loading · A progress state that refuses to lie
drei's useProgress counts files, not bytes: a self-contained GLB is one item, so the bar sits at zero for the whole download and jumps to a hundred at the end — the number is a lie. So the loader shows no percentage: DrawSVGPlugin draws the specimen's measured line plate stroke by stroke while a caliper travels its rule, claiming only that work is happening without pretending to know how much is left.
Once the model is ready the plate cross-fades into the 3D build; even a cached model holds the plate for at least 1.5 seconds, or it would flash past unread. Under prefers-reduced-motion the decorative animation stops and the minimum hold is lifted — reducing motion should remove motion, not function, and never add waiting.


