Real-Time Dynamic Environment Maps in Three.js: The Holy Donut Technique Explained
One of the most beautiful techniques in Bruno Simon’s Three.js Journey — a glowing donut that lights and reflects on everything around it in real time.
This lesson blew my mind. Here’s a complete, clear breakdown with code, why each part matters, and pro tips you won’t find everywhere.
What Is a Real-Time Environment Map?
Normal environment maps are static (one fixed HDRI).
A real-time one updates every frame: you render your scene into a texture that then becomes the lighting + reflections for the whole scene.
Result? Moving objects instantly change how everything else looks. Pure magic.
The Analogy That Makes It Click
Imagine a room with a spinning glowing Holy Donut (Bruno’s name for it).
- Without real-time map → You take one photo of the room and paste it on the walls. Donut spins → walls stay frozen.
- With real-time map → The walls are smart mirrors refreshing 60× per second. Every shiny surface instantly sees the donut’s new position.
The Complete Code Breakdown
1. Background (Visuals Only)
const environmentMap = textureLoader.load('/textures/environmentMaps/cozy-wood-cabin.jpg');
environmentMap.mapping = THREE.EquirectangularReflectionMapping;
environmentMap.colorSpace = THREE.SRGBColorSpace;
scene.background = environmentMap; // Only background, not lighting
2. The Holy Donut (Your Dynamic Light)
const holyDonut = new THREE.Mesh(
new THREE.TorusGeometry(8, 0.5, 32, 64),
new THREE.MeshBasicMaterial({
color: new THREE.Color(10, 4, 2) // Super bright!
})
);
holyDonut.position.y = 3.5;
holyDonut.layers.enable(1); // Crucial
scene.add(holyDonut);
3. Cube Render Target (The Storage)
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, {
type: THREE.FloatType // Allows colors brighter than 1.0
});
scene.environment = cubeRenderTarget.texture;
4. CubeCamera (The 6 Smart Mirrors)
const cubeCamera = new THREE.CubeCamera(0.1, 100, cubeRenderTarget);
cubeCamera.layers.set(1); // Only sees the donut
5. Animation Loop – The Magic Line
const tick = () => {
const elapsedTime = clock.getElapsedTime();
holyDonut.rotation.x = Math.sin(elapsedTime) * 2;
cubeCamera.update(renderer, scene); // ← This makes it real-time
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
How It Works (Every Single Frame)
| Step | What Happens |
|---|---|
| 1 | Holy Donut rotates |
| 2 | CubeCamera renders 6 faces (only layer 1) |
| 3 | Textures saved into cubeRenderTarget |
| 4 |
scene.environment updates |
| 5 | All PBR materials instantly reflect/lit by new donut position |
Key Technical Details Explained
Why FloatType?
Normal textures clamp colors at 1.0 → your bright donut (10, 4, 2) becomes boring white. FloatType keeps the glow.
Why Layers?
Main camera sees everything. CubeCamera only sees layer 1 → clean reflections without the helmet baking into itself.
Pro Tips & Gotchas (Added Value)
- Performance: 256px is the sweet spot. Try 128 for mobile. You can update the camera every 2–3 frames instead of every frame.
-
Common mistake: Forgetting
layers.set(1)→ your whole scene reflects itself (ugly). - When to use this: Hero objects, fire, moving cars, disco balls.
-
Alternative:
THREE.ReflectionProbefor simpler cases orPMREMGenerator+ HDRIs for mostly static scenes.
Another dynamic glowing example from the Three.js community
The Final Result
Your scene now feels alive:
- Dynamic lighting from moving objects
- Real-time reflections on every shiny surface
- Clean & performant
This is the kind of effect that makes people stop scrolling.
Huge thanks to Bruno Simon for the brilliant “Holy Donut” example and his incredible teaching in Three.js Journey. If you haven’t bought the course yet — do it. It’s worth every penny.
Top comments (0)