Why
A member dashboard is usually a table of links nobody opens.
Making it a place people walk through turns navigation into something they'd do for its own sake.
3D Game Dashboard
React Three Fiber
The Tethos member dashboard is a walkable island. You are a 2D sprite on a billboard in a 3D world, and the buildings are the app's routes.
Context
Why
A member dashboard is usually a table of links nobody opens.
Making it a place people walk through turns navigation into something they'd do for its own sake.
Constraint
It has to hold 60fps on an 8GB M1 and on student Chromebooks.
Most of the interesting work below is a consequence of that.
Build
Terrain
Value noise under 4-octave FBM produces rolling hills across a 40-unit island.
Buildings and paths flatten the ground beneath them, then ramp smoothly back to noise so nothing sits on a visible seam.
The Hard Part
Every entity ground-follows, so each samples terrain height per frame. Player, ghosts, NPCs and filler NPCs together ran roughly 960 FBM calls a second, and each FBM is 16 noise samples. Around 15,000 sin/floor calls a second just to stand on the ground. Fine on desktop, hot on an M1.
So the final height is baked once into a 257×257 grid at module load, and runtime sampling is a bilinear lookup: 4 array reads and 4 lerps. About 50× cheaper.
The subtlety is that it bakes the post-flattening height, not the raw noise, so building and path corrections survive the optimization. The grid fills lazily on first sample to keep the bake off the SSR path.
Sky
The sun follows a real arc rather than a spin: elevation is sin(s·π)·60°, tracking east to south to west on a northern-hemisphere convention. The moon runs the opposite arc.
Sky colour interpolates between time-of-day keys, including the awkward night-to-dawn wrap from hour 21 back to 5.
A coarser four-phase signal (dawn, day, dusk, night) lets ambient life swap creatures without re-rendering on every hour tick.
Rendering
Characters are 2D sprites on billboards inside the 3D scene, in the vein of Dave the Diver. Animation is UV-window scrolling across a 3×10 sheet with nearest-neighbour filtering and no mipmaps, because pixel art must not be smoothed.
The walk cycle is driven by measured speed, not intended speed, so an avatar pinned against the world boundary slows its legs instead of moon-walking.
Nature GLBs draw through instanced meshes, one instance group per sub-mesh, cutting draws by roughly 5–15×.
Empty World
There is no multiplayer. There is also never an empty world.
Members' positions heartbeat every 30 seconds, and the world repopulates itself with the last 10 real members seen in the past 24 hours, rendered as translucent ghosts with a "last seen 3h ago" nameplate.
Ghosts are desaturated to 20% against the NPCs' 50%, so the past reads as the past.
NPCs
NPCs are LLM-backed with per-user memory that persists across sessions.
The memory is extracted from the model's own reply: it appends a structured block that gets stripped from the visible text, parsed, and set-unioned into stored state. It only writes on success, so a failed call can't wipe what it knows about you.
Three gates protect the bill: a profanity check before any spend, a 30-message rate limit, and a monthly cap that silently degrades to canned dialogue. The player never sees an error, the NPC just gets less chatty.
Budget
Frame cost is measured, not guessed. Shadows cost about 7 FPS on an M1 (60 down to 53), bloom about 12, vignette half a frame.
So shadows ship off, bloom is opt-in, and distant trees drop shadow casting entirely because fog hides the loss.
Devices reporting 4GB or less auto-enter lite mode.
Stack
Rendering
React Three Fiber, drei, three.js, TypeScript.
App
Next.js 16, React 19, Supabase, Zod. Claude Haiku for NPC dialogue.
Scale
~66k lines across the web app. 50+ API routes, 22 migrations.