Neon Bloom
Three wireframe torus rings — magenta, cyan, and yellow — precess slowly around a floating low-poly icosahedron on a near-black backdrop, run through UnrealBloomPass so their bright, unlit wireframe pixels glow. The composer is built imperatively in onCreated and registered with root.frame(callback, 1): priority 1 is render takeover, the r3f-parity feature that hands gl.render off to your own callback for the rest of the root's life.
How it works
- Postprocessing enters through
extend():EffectComposer,RenderPass, andUnrealBloomPassall live underthree/addons/postprocessing/, outside the package's core namespace, so they're registered withextend({ EffectComposer, RenderPass, UnrealBloomPass })— seeextend()custom classes. None of the three ever become a scene tag here, though —EffectComposerisn't anObject3D, so it has no natural place inscene; it's built with plainnewinsideonCreatedinstead. - Render takeover via
root.frame(callback, priority):onCreatedcallsroot.frame((root, delta) => composer.render(delta), 1)directly, rather than going through a scene node'sonFrame/onFramePriorityprops — the composer has no node of its own to hang them on. Once any registered frame callback haspriority > 0, the root stops calling its owngl.render(scene, camera)every tick;composer.render()becomes the only thing that draws a pixel. See Priority / render takeover. - Resize handled with a bare
effect():root.sizeis a reactiveState<SizeState>— a plaineffect(() => { const { width, height } = root.size.get(); composer.setSize(width, height) })auto-tracks that read and re-runs on every resize, keepingUnrealBloomPass's internal render targets in step with the canvas, no manualResizeObserverneeded. Seeeffect()for the same auto-tracking idiom used by reactive props. - Unlit neon, lit everything else: the rings use
meshBasicMaterialwithwireframe: true— deliberately unaffected by scene lighting, so bloom always has clean, saturated pixels to work with regardless of the light rig. The icosahedron is a normally-litmeshStandardMaterial, and each ring carries its own coloredpointLightas a child (nested inside its ownmesharray) so the light rides the ring's transform and spills that hue across the icosahedron and the fog as the rings precess. - Precession, not just spin: each ring's
onFrameincrementsrotation.xandrotation.yby a different per-ring speed every frame — the ring's own tilt axis keeps drifting over time instead of settling into a flat, predictable spin, the same "wandering gyroscope" look real precessing rings have. root.glvs.THREE.WebGLRenderer:three()'sRootState.glis typed as the minimalRendererLikecontract, not the concreteWebGLRendererclassEffectComposer's constructor expects — the demo casts once at that boundary. The default renderer really is aWebGLRenderer, so it's safe at runtime; a customcreateRendererswapped in for something non-WebGL (WebGPU, a test stub) would need its own composer strategy.