Light
The direct lights of a 3D scene: Light.sun() (a directional sun that casts shadows),
Light.point() (a lamp, a fire, a flash) and Light.spot() (a cone: a flashlight, a headlight).
Light extends Node; add it to a scene like any node. Ambient light is the scene's
image-based light (ibl, environmentIntensity on Scene), baked light is a
lightmap, and bloom or colour grading is post-processing.
- light a 3D scene with a sun and shadows
- make sun shadows softer, sharper or reach further
- add a lamp, a fire or a muzzle flash at a point
- add a flashlight or headlight that shines in one direction
- fade out, flash or recolour a light
- choose between ambient light, a sun, a point light and a spot light
Platforms: iOS, Android, desktop, the web player; the web (Web Lite) draws one sun shadow map
(no shadowDistance / cascades) and the 8 point / spot lights nearest the camera, without their
shadows, and plays no light tweens (nor do headless tests) — set intensity / color per frame
there.
At a glance
const scene = new Scene()
scene.add(Light.sun({ direction: [1, -2, -1], intensity: 80000, shadowsQuality: 2 }))
scene.add(Mesh.box({ material: Material.lit({ color: '#e33' }), position: [0, 0.5, 0] }))
scene.open()Sunlight: Light.sun()
Light.sun(options?: {
direction?: Vec3Like // where the light TRAVELS; default [0.548, -0.474, -0.689] (key-light angle)
intensity?: number // illuminance in lux; default 100000
color?: ColorInput // default white (0xffffff)
shadowsQuality?: 0 | 1 | 2 | 3 // default 1 — see table
shadowDistance?: number // metres from the camera the shadows reach; default 100
shadowCascades?: 1 | 2 | 3 | 4 // shadow-map cascades over that distance; default 1
}): Lightdirection and the shadow options are creation-time. intensity and color are
writable on any light (see below); to change the rest, destroy() the sun and create a new one.
What it was created with reads back — light.direction, light.shadowsQuality,
light.shadowDistance, light.color — and Light.lastSun is the most recently created sun, so a
graphics menu can rebuild it with one option changed.
Softer or sharper sun shadows: shadowsQuality
How soft the edges of the sun's shadows are, and what that costs, is shadowsQuality, set at creation:
shadowsQuality |
Result |
|---|---|
0 |
no shadows |
1 |
PCF: the cheap level — a 3×3 filter on desktop, a single tap on phones (harder edges) |
2 |
VSM: uniformly soft edges (a blurred variance map) |
3 |
PCSS: contact-hardening soft edges — sharp where the caster meets the receiver, softer further away |
Levels 1 and 2 draw into a 1024 map (2048 once the render is over 1200 px tall); level 3 into a
2048 map. Higher quality costs more GPU and memory — 3 is discrete-GPU territory. The engine logs
what it built: [shadows] sun: PCSS, map 2048, 1 cascade(s) over 100 m.
How far sun shadows reach: shadowDistance, shadowCascades
shadowDistance bounds both the casters the shadow pass draws and the area the map is spread
over, and shadows fade out towards it — so a shorter distance is crisper for the same quality and
puts fewer casters in the shadow pass; 30–40 m is plenty in first person. shadowCascades 2–4
split that distance into maps tight near the camera and coarser behind, fit to their texel grid so
the shadow does not swim as the camera moves (at least a 2048 map each). Each cascade is another
caster pass — worth it for forests, not for a lightmapped level where little casts.
Sun shadows on a lightmapped level
On a lightmapped level (a baked light grid is loaded) the variance / PCSS filters cannot be
used: they estimate the penumbra from what is under the pixel in the shadow map, and the baked
statics are kept out of the map — their shadow is already in the bake. There level 3 renders as
PCSS on the plain depth map (a blocker search + a variable-width filter: sharp at the contact, soft
further away — the same look, more samples per pixel), and level 2 renders as 1. The engine
logs [shadows] shadowsQuality N -> … when it does this.
On a level WITHOUT a bake, levels 2 and 3 draw every shadow receiver into the shadow map
(that is how the filters work), so castShadows: false holds there only together with
receiveShadows: false.
A lamp, a fire, a muzzle flash: Light.point()
Light.point(options?: {
intensity?: number // luminous POWER in lumens; default 1000
color?: ColorInput // default white (0xffffff)
range?: number // metres of influence; default 10
castShadows?: boolean // a cubemap shadow render per light; default false
baked?: boolean // lecodes lightmap bake bakes it; default true
bakeArea?: [width, height] // the bake's shape: an area light in metres (below)
}): LightPosition it like any node (light.position = [x, y, z]).
intensity is luminous power in lumens, the same physical scale as the sun's lux and
environmentIntensity, so a light keeps its look when the scene's exposure changes. Rough anchors:
a candle ≈ 12 lm, a bare 60 W bulb ≈ 800, a car headlight ≈ 1500, a street lamp ≈ 10 000. An
explosion is in the tens of millions — it has to out-shine whatever already lights the level, and
a 55 000-lux sun is a lot to out-shine.
range is the performance knob. Past it the light contributes nothing, and overlapping point
lights are the expensive case, so keep it as small as the look allows. A light that reaches the
whole level touches every froxel of every frame.
Lamps on a lightmapped level: baked, bakeArea
On a lightmapped level a point light is baked by default: it lights
the statics from the atlas and the movers from the level's light grid, and is held dark in real
time while that grid is loaded (intensity keeps its value; without a grid it keeps lighting the
movers live). A light that must reach the baked statics live — a flicker, a lamp the player can
shoot out, a muzzle flash — says baked: false: it is kept out of the bake and lights the statics
in real time. bakeArea: [w, h] makes the bake treat the lamp as a rectangle in its local XZ plane,
emitting along its local −Y (a ceiling panel): soft shadows, light from the whole panel (ignored
with baked: false). Real time it stays a point light.
A flash should not exist between flashes:
scene.remove()it when it burns out rather than leaving it parked atintensity = 0.
A flashlight or headlight: Light.spot()
Light.spot(options?: {
intensity?: number // luminous power in lumens; default 1000
color?: ColorInput // default white (0xffffff)
range?: number // metres of influence; default 10
angle?: number // the cone's FULL angle in degrees, up to 180; default 45
innerAngle?: number // the fully lit core, full angle in degrees; default 0.75 × angle
castShadows?: boolean // one 2D shadow map per light; default false
}): LightIt shines along the node's forward (−Z): position it like any node and aim it with lookAt, or
parent it to what carries it (a car, a player's camera).
const torch = Light.spot({ intensity: 1500, range: 20, angle: 30, castShadows: true })
torch.position = [0, 3, 0]
torch.lookAt([0, 0, -4])
scene.add(torch)
torch.angle = 15 // live: a flashlight's focusThe cone does not change the brightness: widen it and the same lumens light a larger patch at
the same level. On its axis a spot is 4× brighter than a point light of the same lumens (its light
is not spread over the whole sphere). The light fades from innerAngle out to angle; setting
angle scales innerAngle along. range is the performance knob, as for a point light. A spot's
shadow is one 2D map, cheaper than a point light's cubemap.
Real time only. The lightmap bake takes no spot lights: a spot lights the baked statics and the
movers live, the way Light.point({ baked: false }) does.
Web Lite draws the 8 point / spot lights nearest the camera, without shadows (its one shadow map is the sun's).
Fade, flash or recolour a light: intensity, color, animateTo
intensity (sun: lux, point / spot: lumens) and color are writable on every kind, and animateTo /
animateFrom tween them together with the transform — a muzzle flash or an explosion changes per
frame, and rebuilding the light would churn the engine's light list.
const flash = Light.point({ intensity: 2.4e7, color: '#ff8a2a', range: 26 })
flash.position = [0, 1, 0]
scene.add(flash)
flash.animateTo({ intensity: 0, duration: 400, easing: 'outCubic' }) // duration in ms
.onFinish(() => scene.remove(flash))
const sun = Light.sun()
scene.add(sun)
sun.intensity = 60000 // or set it directly
sun.color = '#ffd9a0'The tween returns the Animation handle (animate.md); several lights and
nodes on one clock are a Timeline.
after a color tween, light.color still reads the value it had before the tween (the
getter is not updated) — assign light.color yourself afterwards if code reads it back.
only meshes flagged castShadows throw shadows, and only receiveShadows surfaces
show them — see Mesh. A Material.shadow() plane makes a
transparent shadow-catcher (the AR staple).
Which light to use: ambient (IBL), sun, point or spot
- IBL (scene option) — soft omnidirectional ambient; on by default, strength
environmentIntensity(default 20000), disable withibl: false. No shadows. Light.sun()— directional key light; gives surfaces shading direction and casts shadows.Light.point()— a local source: lamps, fires, muzzle flashes. Falls off with distance and is bounded byrange.Light.spot()— a point light limited to a cone: flashlights, headlights, stage lights.
A typical lit scene uses both. With ibl: false and no sun, Material.lit() surfaces render
black — use Material.unlit() if you want no lighting at all.
See also
- Scene —
ibl/environmentIntensityoptions - Material — lit vs unlit, the shadow-catcher material
- Mesh —
castShadows/receiveShadowsper node - Lightmap — baked lamps, the light grid, shadows on a baked level
- Node — the inherited node surface (
destroy,animateTo, scene membership) - Scene files — a
light: { kind: 'sun', … }node takes the same options