Inspiration
Almost every website is a rectangle of text you scan once and forget. The web can hold depth, space and light, and it almost never bothers. LUCID started from that frustration, plus a fascination with lucid dreaming — a place that's familiar but slightly wrong, where you know you're inside something and can still move through it. We wanted a site that isn't a page you read but a place you fall into.
What it does
A single-page, scroll-driven 3D experience. You descend through five chambers — Threshold, Arrival, The Drift, The Archive, The Waking — and your scroll position is the only thing moving the camera. No pages, only depth. A single glass crystal is the throughline: dim and fractured at the Threshold, turning and drifting through Arrival, lit in dawn-amber by The Waking — the only place the temperature turns warm. Hover brightens it; clicking it opens a glass panel with that chamber's story.
How we built it
- React Three Fiber + drei + @react-three/postprocessing on three, in a TanStack Start app with Tailwind for UI chrome.
- One fixed full-screen canvas behind all five chambers — the DOM scrolls over a single scene.
- Camera choreography: position and look-at travel along two
THREE.CatmullRomCurve3paths, one viewpoint per chamber, damped each frame against shared scroll progress. - Material: drei's
MeshTransmissionMaterialwith chromatic dispersion and teal attenuation on procedural octahedral geometry — no purchased assets. - Light: a real Poly Haven HDRI plus three-point lighting that lerps violet → amber across the final descent.
- Atmosphere: two
Sparkleslayers, bloom tuned to catch only true speculars, faint vignette, subtle chromatic aberration.
Challenges we ran into
- The camera faced the wrong way. Our first rig slerped a plain
Object3Dquaternion toward the target — butObject3Dpoints +Z at a target while a camera looks down -Z, so we were aimed 180° away from the crystal. Fixed with a dampedcamera.lookAt()on a smoothed point along the look curve. - The crystal rendered invisible. Transmissive glass over a transparent canvas has nothing to refract. A large inverted void-colored backdrop sphere gave it depth.
- Headless WebGL debugging.
toDataURLkept returning stale or blank frames; we switched to Playwright screenshots and logged NDC coords to confirm framing. - Crisp vs. washed out. Raising reflectivity blew out highlights — we pulled ambient and fill back, dropped roughness, and raised the bloom threshold.
- Performance on real devices. Transmission + postprocessing is expensive, so a capability probe clamps DPR, thins particles, and drops postprocessing on mobile/low-power hardware.
Accomplishments that we're proud of
- One memorable image carries the whole visit, instead of five unrelated hero sections.
- Scroll is the only input — nothing is on a timeline; the site reacts to you rather than performing at you.
- It's actually accessible: reduced-motion gives a calm near-static version, every control has a visible focus ring, and all chamber content is keyboard-reachable — the 3D is enhancement, not a gate.
- It holds up on a phone, rare for a transmission-heavy WebGL scene.
- Everything is free or self-authored: procedural geometry, a Poly Haven HDRI, no copyrighted assets.
What we learned
- Scroll-driven 3D lives or dies on damping — raw scroll feels twitchy; everything needed smoothing to feel cinematic.
MeshTransmissionMaterialis only as good as what's behind it; environment and backdrop matter more than material settings.- Restraint reads as craft. Spending all the boldness on one crystal made the whole thing feel more considered.
- Building reduced-motion and low-power paths as you go beats retrofitting them.
What's next for LUCID
- Custom shaders so the crystal's healing from Threshold to Waking is geometric, not just lighting.
- Spatial audio shifting with depth — a low drone resolving into something warmer.
- Pointer parallax on top of scroll, so the scene breathes even when you're still.
- Shareable depth links — deep-link to a chamber and have the camera fly there.
- A WebGPU path for higher-sample transmission, with WebGL as fallback.
Built With
- drei
- react
- tailwind
- tanstack-start
- three-fibre
- three.js
- typescript
- webgl
Log in or sign up for Devpost to join the conversation.