A deep technical retrospective on building a Three.js/React Three Fiber experiments lab tied to the author's own music releases: a 3D Space Invaders game (FRONTIER), seven fly-through music visualizers (LXSTNGHT), and a CRT-wall installation (/fm). Covers hard-won rendering lessons: budgeting drawing-buffer pixels instead of device-pixel-ratio, avoiding mix-blend-mode over WebGL canvases, how Three.js bakes light count into shader compile keys causing mid-scene stalls, draw-call batching and frustum-culling pitfalls when merging geometry, HDR/bloom pipeline gotchas (8-bit composer buffers, LDR blend functions inverting colors, NaN from negative grade values), and choreographing visuals to pre-baked audio analysis instead of real-time FFT. Includes concrete GPU timing measurements (2ms frame time, p95 latency figures) and code snippets.
Table of contents
FRONTIER: the gameLXSTNGHT: one engine, seven worlds/fm: the record, playing on a wall of dying monitorsWhat keeps proving trueWhere to find itQuestions this post answers
Why does my custom ShaderMaterial in Three.js not respond to scene lights?
Three.js scene lights do not automatically affect a custom ShaderMaterial; you must feed light data into the shader yourself via uniforms. A hexagonal floor shader in a Three.js project stayed unlit by a moving spotlight for a week until the light position and intensity were passed in as a uniform array manually. Developers debugging custom shader lighting quirks in Three.js can find similar deep-dive writeups on daily.dev.
Why does bloom postprocessing turn bright emissive objects black instead of glowing in Three.js?
The pmndrs postprocessing library's Bloom effect defaults to BlendFunction.SCREEN, an LDR operator that inverts to negative when both the scene and bloom values exceed 1.0, producing black pixels with glowing rims. Setting blendFunction to BlendFunction.ADD fixes it, since additive blending can never invert to a negative result. Anyone chasing HDR bloom artifacts in a WebGL pipeline can track similar postprocessing fixes on daily.dev.
Why did merging geometries in Three.js break frustum culling and increase rendered triangles?
Merging many small meshes into one large geometry per material collapses them into a single bounding sphere spanning the whole merged shape, so it always intersects the camera frustum and gets submitted every frame regardless of visibility. Re-merging per material per smaller spatial segment (e.g. every 3 meters) restores culling and can cut submitted triangles 21-37%. Teams optimizing draw calls in Three.js scenes can follow more geometry-merging tradeoffs like this on daily.dev.