Optimize Multiple Dynamic Lights in Three.js
Rendering multiple dynamic lights in Three.js can drastically reduce frame rates due to increased draw calls, complex shader calculations, and shadow map overhead. This article outlines key strategies to optimize your render performance when your 3D scene requires multiple dynamic light sources, focusing on light culling, shadow optimization, material selection, and alternative rendering pipelines.
Limit the Range and Culled Lights
By default, Three.js calculates the influence of active lights on all materials within the scene graph. To prevent unnecessary calculations, you should actively manage which lights are enabled based on their proximity to the camera.
- Distance Culling: In your animation loop, calculate
the distance between each light and the camera. If a light is too far
away to contribute meaningfully to the visible scene, set
light.visible = false. - Define Light Decay: Use
PointLightorSpotLightwith physically correct decay (decay = 2). This ensures the light naturally fades to zero over a set distance, allowing you to safely disable the light when the camera or objects move out of its range.
Optimize Shadow Maps
Shadows are the most performance-intensive aspect of dynamic lighting because each shadow-casting light requires rendering the scene from its own perspective into a depth texture.
- Disable Unnecessary Shadows: Only allow a few key
lights to cast shadows (
light.castShadow = true). Keep minor, accent, or fill lights as non-shadow casters. - Reduce Map Resolution: Lower the resolution of your
shadow maps. Instead of the default size, use
light.shadow.mapSize.width = 512(or even 256) for secondary lights. - Tighten the Shadow Camera Frustum: Restrict the
shadow camera’s field of view and near/far clipping planes
(
light.shadow.camera.near,far,left,right,top,bottom) to tightly fit only the area where shadows are visible. - Choose Performance-Friendly Shadow Types: Set your
renderer’s shadow map type to
THREE.BasicShadowMaporTHREE.PCFShadowMap. AvoidTHREE.PCFSoftShadowMapas it requires significantly more GPU processing.
Use Cheaper Materials
The complexity of your materials determines how expensive the lighting calculations will be in the fragment shader.
- Downgrade Material Complexity:
MeshStandardMaterialandMeshPhysicalMaterialuse physically-based rendering (PBR) algorithms that are computationally heavy under multiple lights. For scenes where high-fidelity realism is not required, useMeshPhongMaterialorMeshToonMaterial, which use simpler, faster mathematical approximations for lighting. - Static Lightmaps: For lights that do not move, bake
their lighting into a texture (lightmap) using 3D modeling software like
Blender. You can then apply this static lightmap to a
MeshBasicMaterial, bypassing the runtime lighting engine entirely for those areas.
Leverage WebGL 2 and Clustered Forward Rendering
Standard Three.js forward rendering calculates lighting for every light source on every single fragment, which scales poorly.
- Use WebGPURenderer: Modern versions of Three.js
feature a
WebGPURenderer(with WebGL 2 fallbacks) that supports clustered forward shading. This technique divides the screen into a 3D grid (clusters) and only computes lighting for the specific lights that overlap with each cluster. Switch to WebGPU features where possible to handle dozens of dynamic lights with minimal performance loss.