Try it live About this project
The six panels are genuine WebGL geometry inside the scene — they have thickness and curvature, they occlude the character and are occluded by them, and their depth order stays correct from every angle. They are not DOM cards floating over a canvas.
The whole interaction rests on one division of labour: 3D is for discovery, 2D is for control. The panels handle browsing and selection, drifting around the character and turning with your gestures; actual playback control belongs to the stable screen-space interface at the bottom. Selecting a song is not a state switch but a continuous hand-over — the panel itself flies down to become the player, and when another track is chosen, the old one flies back to its original 3D slot.
Highlights
- Drag empty space and the music cards revolve around the character, releasing into inertia; drag the character and only they turn, camera and panels untouched. Both drift slowly in opposite directions — the opposed motion is what makes the cluster read as an orbiting body rather than a backdrop
- Click a panel and it breaks out of the 3D space, sweeps past the character and unfolds into a horizontal player at the bottom; pick another and two panels are airborne at once — the old one folds back into its exact original slot as the new one comes forward
- The wheel dollies the camera between 4.6 and 16 world units: push in and the foreground panels overflow the frame, pull back and the whole space gathers into one music constellation seen from above
- The custom cursor's four-corner frame locks onto each panel's real projected quad — the brackets land on true perspective corners and turn with the panel's tilt
Technical breakdown
3D assets · The untouched Tripo export
Artist.glb ships exactly as exported from Tripo: 59 MB, 1.02M vertices, an 8192² base colour map — nothing decimated, nothing resized, full authored detail. The cost is priced in (~350 MB VRAM once mipmapped, a million vertices redrawn per frame) and two decisions keep it manageable: the heavy mesh opts out of raycasting — a million triangles per pointer move would stall the main thread — with an invisible capsule sized from the model's own bounds standing in for it; and since the export has no animation clips, the character gets a group-level idle (a slow sway plus a millimetre of breathing) rather than an invented skeleton.
A meshopt pipeline in scripts/ takes the same file to 2.9 MB; the model is measured and normalised at load time, so swapping builds needs no code change.
Materials · Downgrading the volume material in Tripo exports
Tripo exports tag the mesh with KHR_materials_volume, which lands in three.js as a MeshPhysicalMaterial with a transmission/thickness path this scene neither wants nor can afford. At load every material is downgraded to MeshStandardMaterial with the full PBR map set kept — same look, cost gone.
Custom ShaderMaterials include colorspace_fragment explicitly: without it they write linear values straight into an sRGB framebuffer and render almost black.
Music panels · Real geometry on a curve
Each panel is four kinds of geometry — a face, a rim strip swept for real thickness, a translucent back plate, and invisible hit quads — all sharing one cached buffer set, only the group transform differing per card. Everything is authored flat and wrapped onto a large vertical cylinder: the progress groove and clock are baked onto the same curve rather than parented to it, and the progress fill is a UV cutoff injected into a MeshBasicMaterial — scaling a mesh would lift its far end off the curve.
Panel artwork splits across three canvases by update frequency: the 1320×680 face paints once, the time strip repaints when the displayed second changes, the icon only when playback flips — nothing expensive in the render loop. All six transparent panels skip depth writes and take banded renderOrder by camera distance every frame, so occlusion stays correct from any angle.
3D-to-2D relay · A card lands as the player
A WebGL mesh cannot become a DOM node, so continuity is assembled from three facts: a projector keeps every card's screen-space box current each frame — including hidden ones, because the return flight needs somewhere to land; the DOM panel's first frame is the very canvas bitmap already painted for the card's WebGL texture, not an HTML re-creation that would never quite line up; and the card hides on the same frame — two things in one place, one invisible.
Widening holds until the flight is ~35% done — stretching a 2:1 card into an 11:1 bar mid-flight turns the artwork to mush — and the path is a quadratic bezier bowed to the side, so a card docking from the upper left sweeps around the artist. The destination is re-read every frame: the constellation is still drifting, and a rect sampled at launch would land the panel beside its card.


