Rendering

Lights & shadows

The Light component supports directional, spot, point, and ambient types; LightSystem wires them into Three.js and the Forward+ cluster; shadows use variance shadow maps with frustum-fitted cameras.

All light data lives on the Light ECS component. LightSystem reads Light + Transform pairs and maintains the corresponding Three.js light objects and, for point and spot lights, entries in the Forward+ LightManager. This page covers the component API, the four light types, and how shadow cameras are configured.

The Light component

import { Light }     from "@woosh/meep-engine/src/engine/graphics/ecs/light/Light.js";
import { LightType } from "@woosh/meep-engine/src/engine/graphics/ecs/light/LightType.js";
import { Transform } from "@woosh/meep-engine/src/engine/ecs/transform/Transform.js";
import { Entity }    from "@woosh/meep-engine/src/engine/ecs/Entity.js";

const light = new Light();
light.type.set(LightType.POINT);
light.color.setRGB(1, 0.9, 0.7);
light.intensity.set(2.0);
light.distance.set(8);         // radius of influence (POINT and SPOT)
light.castShadow.set(false);

const t = new Transform();
t.position.set(0, 4, 0);

new Entity().add(t).add(light).build(ecd);

All fields are observed values — changes propagate reactively to the underlying Three.js and Forward+ objects without any manual refresh.

Properties

PropertyTypeDescription
typeObservedEnumOne of the LightType constants below
colorColorRGB light color, defaults to white
intensityVector1Multiplier on the emitted energy
distanceVector1Max influence radius for POINT and SPOT lights
angleVector1Half-angle of the cone for SPOT lights, in radians
penumbraVector1Fraction of the cone that is soft-edge for SPOT lights (0 = hard, 1 = fully soft)
castShadowObservedBooleanWhether this light produces a shadow map
maxShadowDistanceVector1For DIRECTION lights: caps the depth of the view frustum fitted for shadows (Infinity = full frustum)

Light types

import { LightType } from "@woosh/meep-engine/src/engine/graphics/ecs/light/LightType.js";
ConstantValueBehavior
LightType.DIRECTION0Parallel rays from a direction derived from the entity’s Transform rotation. Affects the whole scene. Shadow-eligible.
LightType.SPOT1Cone emanating from the entity’s position in its forward direction. angle and penumbra shape the cone. Registered in the Forward+ cluster.
LightType.POINT2Sphere of influence centered on the entity’s position. distance sets the radius. Registered in the Forward+ cluster.
LightType.AMBIENT3Uniform contribution added to all surfaces regardless of direction. Not cluster-registered; cannot cast shadows.

How Forward+ handles POINT lights

When LightSystem links a Light of type POINT, it creates a PointLight render object in the Forward+ LightManager. The object tracks position, radius, color, and intensity — the same data that is packed into the GPU’s light-data texture each frame. Directional and ambient lights are handled by Three.js’s own path and are not cluster-distributed.

Registering LightSystem

import { LightSystem } from "@woosh/meep-engine/src/engine/graphics/ecs/light/LightSystem.js";

em.addSystem(new LightSystem(engine));

Optional pre-allocation avoids resize churn if you know your light budget up front:

em.addSystem(new LightSystem(engine, {
    preAllocateLightsPoint:     32,
    preAllocateLightsSpot:       8,
    preAllocateLightsDirection:  1,
    preAllocateLightsAmbient:    1,
}));

Variance shadow maps

Meep sets THREE.VSMShadowMap on the renderer at startup. Three.js generates the initial depth render, then runs a separable Gaussian blur (8 samples per pass) to produce the mean and mean-squared depth values that VSM requires. The resulting soft shadows have no contact hardening, but they are cheap and largely free of the acne and peter-panning that require large bias values in PCF shadows.

A small bias of −0.0001 is applied automatically when the shadow camera is fitted.

Shadow camera fitting for directional lights

With a single shadow cascade, naively fitting the orthographic shadow camera to the full view frustum spreads the shadow map over hundreds of world units, making shadows look pixelated. Meep works around this with a per-frame fit:

  1. The view frustum is optionally truncated to light.maxShadowDistance before fitting, so the shadow map covers only the nearest N metres of the view.
  2. The (truncated) frustum corners are transformed into the directional light’s local space.
  3. The AABB of those corners gives left/right/top/bottom/near/far for the orthographic projection.
  4. Both the frustum size and the camera position are snapped to shadow-map-texel boundaries so the shadow does not swim as the camera translates.
  5. If a RenderLayerManager is provided, the engine walks render-layer BVHs to find geometry above the receiver frustum (casters outside the view) and pulls the shadow camera near back to include them.
// Cap shadow distance to 40 m for a sharper map at the cost of visible range.
const light = new Light();
light.type.set(LightType.DIRECTION);
light.castShadow.set(true);
light.maxShadowDistance.set(40);

Setting maxShadowDistance to Infinity (the default) fits the entire camera frustum. For a typical 60 m far plane that may still be acceptable; at 500 m or more you almost always want a finite value.

Where to go next