threejs-lighting
Three.js lighting - light types, shadows, environment lighting. Use when adding lights, configuring shadows, setting up IBL, or optimizing lighting performance.
By cloudai-x · 8,399 installs
npx skills add cloudai-x/threejs-skills --skill threejs-lighting
Source repository · Upstream listing
Three.js Lighting
Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.
Quick Start
Light Types and Units
Light Shadows Intensity Notes
AmbientLight no multiplier Flat fill from every direction
HemisphereLight no multiplier Sky color from above, ground color from below
DirectionalLight yes multiplier Parallel rays (sun); shines at its target
SunLight (addon) yes, cascaded multiplier Shadow frustum fitted to the view automatically
PointLight yes, six faces candela Inverse square falloff (decay 2)
SpotLight yes candela Cone; inverse square falloff (decay 2)
RectAreaLight no nits Only MeshStandardMaterial and MeshPhysicalMaterial
LightProbe no multiplier Spherical harmonics ambient, usually baked
PointLight and SpotLight intensity is candela with inverse square falloff ( decay 2 by default): a 1 candela light 5 units away contributes 1/25, so useful values depend on distance and typically run to the hundreds (the official docs' examples use 80 to 1000). light.power sets the same thing in lumens. light.distance is a smooth cutoff range (0 means no limit). Directional, ambient and hemisphere intensities are plain multipliers with no unit. RectAreaLight intensity is nits (candela per square metre).
Ambient and Hemisphere
DirectionalLight
SunLight: automatic cascaded shadows
SunLight ( three/addons/lights/SunLight.js ) is a parallel light with two cascaded shadow maps, suited to large scenes. It has no target: it shines from its position towards the origin, straight down by default. With WebGLRenderer it works out of the box; under WebGPURenderer register it first (see WebGPU and TSL).
The cascades are fitted automatically to slices of the camera's view frustum, up to sun.shadow.camera.far or the view camera's far plane, whichever is smaller. shadow.camera.left/right/top/bottom are ignored; the default mapSize is 1024 × 1024 per cascade. There is no shadow frustum to hand tighten.
PointLight and SpotLight
angle is the cone half angle in radians with an upper bound of Math.PI / 2; penumbra (0 to 1) softens the cone edge.
RectAreaLight
A rectangular emitter for soft window or strip style lighting. It lights only MeshStandardMaterial and MeshPhysicalMaterial, and it cannot cast shadows.
Under WebGPURenderer, register the LTC textures with RectAreaLightTexturesLib instead (see WebGPU and TSL).
Shadows
shadow.radius above 1 widens the PCF filter and the VSM blur; it has no effect with BasicShadowMap. VSM also exposes shadow.blurSamples (default 8), and PointLight shadows do not support VSM at all — WebGLRenderer warns and skips the light's shadow. PCFSoftShadowMap is deprecated: it warns and falls back to PCFShadowMap, which is already soft. With VSMShadowMap, every mesh that receives shadows also casts them.
Tuning a directional shadow
Point and spot shadow cameras are derived
The renderer rebuilds these cameras on every shadow pass: a spot light's shadow fov is 2 × angle × shadow.focus (in degrees) and its far comes from light.distance ; a point light's far also comes from light.distance . The fov is always overwritten, and a manual far survives only while distance is 0. Control the cone with angle and shadow.focus , the range with distance .
Visualize a directional or spot light's shadow camera with new THREE.CameraHelper(light.shadow.camera) added to the scene, and call the helper's update() after changing the camera.
Contact shadows (fake, cheap)
A shadow map per light is overkill for a grounded look; a radial gradient blob costs one textured quad.
For a blob that follows the object's silhouette, render depth from below into a render target and blur it — the official webgl shadow contact example does this with HorizontalBlurShader and VerticalBlurShader from three/addons/shaders/ .
Light Helpers
Every light helper copies the light's color only in update() , and DirectionalLightHelper, SpotLightHelper and HemisphereLightHelper also re aim only there: call update() after recoloring a light, and after moving or re aiming one of those three.
Environment Lighting (IBL)
Both renderers light MeshStandardMaterial and MeshPhysicalMaterial from scene.environment ; WebGLRenderer also lights MeshLambertMaterial and MeshPhongMaterial, WebGPURenderer does not. Keep PMREMGenerator for environments generated from a scene; on an equirect file it only repeats the prefiltering the renderer already does:
A cube texture works as an environment without any mapping line:
Light Probes
LightProbeGenerator.fromCubeTexture(cubeTexture) derives one synchronously from an already loaded cube texture. For position dependent ambient, bake a 3D grid instead: LightProbeGridWebGL with WebGLRenderer, LightProbeGrid with WebGPURenderer ( three/addons/lighting/ ). Under WebGPURenderer, build the cube render target with CubeRenderTarget from three/webgpu .
Common Setups
Animating Lights
Performance
Both renderers compile the set of lights into every lit material: adding or removing a light (an AmbientLight only under WebGPURenderer), or toggling castShadow , recompiles them. Remove lights you never use; switch a light on and off at runtime through its intensity . Under WebGPURenderer, DynamicLighting keeps materials compiled when ambient, hemisphere, directional, point or spot lights come and go, as long as they cast no shadow and no spot light has a map , and ClusteredLighting scales to many point lights (see WebGPU and TSL).
Every shadow casting light adds depth passes (six per point light). Leave castShadow off where nothing needs the shadow.
Static scenes: render a light's shadow on demand with light.shadow.autoUpdate = false; light.shadow.needsUpdate = true; and set light.shadow.needsUpdate = true again after something moves (both renderers; WebGLRenderer also has the global renderer.shadowMap.autoUpdate ).
Keep directional shadow frustums tight, or move to SunLight, which fits them to the view automatically.
Lights are not filtered per mesh by layers: a light's layers are tested only against the camera's. To light selected meshes only, use material.lightsNode under WebGPURenderer.
WebGPU and TSL
Light classes, shadow settings and the scene's environment properties above carry over to WebGPURenderer, except the WebGL only pieces this section replaces: RectAreaLightUniformsLib, WebGLCubeRenderTarget (use CubeRenderTarget ), LightProbeGridWebGL (use LightProbeGrid ) and the global renderer.shadowMap.autoUpdate . SunLight must be registered, and scene.environment does not light Lambert or Phong materials here.
Colored shadows: with renderer.shadowMap.transmitted = true , casters write color and alpha into the shadow map, from material.castShadowNode (a vec4) or the alpha of map and colorNode . material.receivedShadowNode (an Fn of the shadow value) changes how a receiver applies its shadows and works without the flag.
Common Mistakes
light.layers.set(1); → material.lightsNode = lights([keyLight, fillLight]); : lights are not filtered per mesh by layers — a light's layers are tested against the camera's, so this switches the light off for the whole scene; per material selection needs a node material, under WebGPURenderer or under WebGLRenderer through the WebGLNodesHandler addon.
new THREE.PointLight(0xffffff, 1, 100); → new THREE.PointLight(0xffffff, 100, 20); : point and spot intensity is candela with inverse square falloff, so 1 contributes almost nothing at range; typical values run to the hundreds.
renderer.useLegacyLights = true; → const bulb = new THREE.PointLight(0xffffff, 100, 20); : legacy light units no longer exist and neither useLegacyLights nor physicallyCorrectLights does anything, so retune point and spot lights in candela instead.
renderer.shadowMap.type = THREE.PCFSoftShadowMap; → renderer.shadowMap.type = THREE.PCFShadowMap; : PCFSoftShadowMap was deprecated in r182; both renderers warn and fall back to PCFShadowMap, which is already soft.
lightProbe.copy(LightProbeGenerator.fromCubeRenderTarget(renderer, rt)); → lightProbe.copy(await LightProbeGenerator.fromCubeRenderTarget(renderer, rt)); : the call is async and returns a Promise, which copy() cannot read.
new ContactShadows({ opacity: 0.5 }); → new THREE.Mesh(new THREE.PlaneGeometry(2, 2), new THREE.MeshBasicMaterial({ transparent: true, map: blobTexture })); : three.js has no ContactShadows class (it is a drei component); fake the contact shadow with a radial gradient plane.
scene.environment = pmremGenerator.fromEquirectangular(texture).texture; → texture.mapping = THREE.EquirectangularReflectionMapping; scene.environment = texture; : the renderer prefilters an equirect environment itself, so manual PMREM adds no quality; keep PMREMGenerator for fromScene.
scene.background = hdr; → hdr.mapping = THREE.EquirectangularReflectionMapping; scene.background = hdr; : HDRLoader returns a texture with UV mapping, so without the line the background draws as a flat screen space image.
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; → import { HDRLoader } from "three/addons/loaders/HDRLoader.js"; : RGBELoader was renamed HDRLoader in r180, and addons must be imported from three/addons/ for the import map to resolve.
spotLight.shadow.camera.fov = 30; → spotLight.shadow.focus = 0.9; : the shadow camera's fov is recomputed each pass as 2 × angle × focus in degrees, so control it through the light's angle and focus.
const lamp = new THREE.PointLight(0xffffff, 100, 20); lamp.shadow.camera.far = 50; → const lamp = new THREE.PointLight(0xffffff, 100, 50); : a non zero distance overwrites the shadow camera's far on every shadow pass, so set the shadow range through distance; a manual far is kept only while distance is 0.
material.envMapIntensity = 0.5; → scene.environmentIntensity = 0.5; : envMapIntensity scales only a material's own envMap, not scene.environment.
const clock = new THREE.Clock(); → const timer = new THREE.Timer(); timer.connect(document); : Clock was deprecated in r183; call timer.update() each frame before getDelta() or getElapsed().
RectAreaLightUniformsLib.init(); → THREE.RectAreaLightNode.setLTC(RectAreaLightTexturesLib.init()); : under WebGPURenderer the uniforms lib is never read, and the light's node dereferences a null LTC library until the textures are registered.
See Also
threejs materials — how materials respond to light (roughness, metalness, material.envMap).
threejs textures — loading environment maps and setting texture color spaces.
threejs shaders — custom GLSL effects such as fresnel rims, extending lit built in materials with onBeforeCompile, and lit node materials in TSL.
threejs postprocessing — bloom and other light adjacent effects.
threejs fundamentals — renderer setup, tone mapping and exposure for physically scaled lights and HDR environments.