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

TypeScript
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()

TypeScript
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
}): Light
Note

direction 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()

TypeScript
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)
}): Light

Position 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 at intensity = 0.

A flashlight or headlight: Light.spot()

TypeScript
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
}): Light

It 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).

TypeScript
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 focus

The 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.

TypeScript
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.

Note

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.

Note

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 with ibl: 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 by range.
  • 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 / environmentIntensity options
  • Material — lit vs unlit, the shadow-catcher material
  • Mesh — castShadows / receiveShadows per 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