Persistent-canvas WebGL website for the dark-fantasy novel RIFT. Next.js 16 + React Three Fiber, custom GLSL shaders, adaptive quality tiers, meshopt asset pipeline.
A promotional site for an unpublished novel, built as a single continuous 3D world instead of a page-per-route site. One WebGL canvas survives every route change; a scene registry maps 8 scene ids (intro, hub, read, map, song, art, codex, about) to App Router routes with deep-link support. If WebGL is unavailable, the site falls back to a fully static version. Solo project, built over 4 weeks.
- One canvas, many routes. The canvas mounts once and never unmounts. A
RouteSynccomponent keeps the active scene and the URL in agreement, so/codexdeep-links land in the right scene without a canvas teardown. - Procedural atmosphere. The hub maelstrom vortex (
components/scenes/hub/AetherVortex.tsx, 302 lines) and the sunset sky dome (components/canvas/SkyDome.tsx, 154 lines) are hand-written GLSL. No external effect assets. - Asset pipeline (
scripts/reproc-models.mjs): gltf-transform weld → meshopt simplify (ratio 0.45, error 0.004) → WebP texture compression → meshopt encoding. Blender originals of 60–92 MB (1–2M tris each) come out as six web GLBs of 9–12.5 MB, ~66 MB total. Originals stay outside the repo. - Keep-color decision. An earlier pass stripped textures during decimation and flattened every realm to a single tone. Reverted (b1ab31d, b90568d); the pipeline now decimates geometry only and preserves the original 4K baseColor materials. The reasoning is documented in the script header.
- Adaptive quality (
lib/quality.ts): high/mid/low tiers at ≥55 / ≥35 FPS via dreiPerformanceMonitor+AdaptiveDpr. Tiers scale DPR (up to 2x), particle count (1200/500/150), and toggle postprocessing. - Intro sequence. 6 narrative beats over ~7 seconds. ENTER click unlocks audio (browser autoplay policy), SKIP is always available, and reduced-motion or returning visitors auto-skip. The skip decision is a pure function with unit tests.
- Transitions. One primitive (descend/rise), wired to the router through a Zustand
SceneDirectorstore. Direction is derived from the from/to scene pair, not passed around by hand. - Terrain seating. Buildings sit on the terrain via a downward raycast against the actual mesh surface instead of hand-tuned Y offsets (14a78a0).
- Lighting. The rig (warm key / cool shadow, ember underglow, ACES tone mapping) was dialed interactively with leva, then hardcoded and the panel removed once the look was locked (2d1ff7b).
- Dev tooling never ships. leva renders hidden in production; r3f-perf is gated behind
NODE_ENV !== 'production'. - Bundler split. Dev runs webpack (
next dev --webpack) because r3f-perf 7.2.3 crashes Turbopack's dev server. Prod builds with Turbopack. Known regression-risk window; deliberate. - e2e against the real build. Playwright builds the production bundle, serves it on port 3100, and drives two journeys: ENTER → hub → READ → RISE → home, and a
/codexdeep-link landing. - Asset provenance.
public/ASSETS.mdlists every external asset as CC0 with source links (Poly Haven HDRI, ambientCG PBR textures). The 6 realm models and the theme audio are original work.
Tests: 8 Vitest suites (store, transitions, quality tiers, intro skip, nav a11y, sound pref, scene registry, smoke) + 1 Playwright spec.
- Next.js 16.2.9 (App Router), React 19.2.4, TypeScript 5
- three.js 0.184, @react-three/fiber 9.6.1, drei 10.7.7, postprocessing 3.0.4
- Zustand 5, GSAP 3.15, Tailwind CSS 4
- Vitest 4 + Testing Library (jsdom), Playwright 1.61
- gltf-transform v4 + meshoptimizer (offline asset pipeline)
- Vercel
None checked in yet. Run the dev server; the hub vortex and the map realms (Velsaria, Lexarion, Aegirheim) are the ones worth looking at.
npm install
npm run dev # webpack dev server (see bundler note above)
npm run build # production build (Turbopack)
npm test # Vitest unit suites
npm run e2e # Playwright, builds and serves prod on :3100Heads up: the repo carries ~66 MB of GLB/HDR/MP3 assets, so the clone is not small.
Code is MIT. The 6 realm models and public/audio/main-theme.mp3 are original creative work, all rights reserved — see public/ASSETS.md. Third-party assets are CC0, sourced in the same file.