API

Module: Lights

Package path: packages/babylon-lite/src/light/

Purpose

Provides plain-data light definitions for hemispheric, directional, point, and spot light types, plus a shared infrastructure layer (light-base.ts, types.ts) and a scene-owned lights UBO packing system (render/lights-ubo.ts, scene/scene-light-state.ts). Following Babylon Lite's "pillar 4b" principle, lights are stateless data objects with no scene references.

Factory functions create light objects with sensible defaults; callers add them to scenes or pass them to material setup functions. Each light carries:

  • Push-based dirty tracking via ObservableVec3 for positions/directions
  • World-matrix state with parent support (inherited from light-base.ts)
  • Shared UBO writer (_writeLightUbo) for the scene lights UBO system
  • Version tracking (_lightVersion plus worldMatrixVersion) so scalar and transform changes, including inherited parent transforms, refresh the shared light UBO

PBR no longer uses per-light extension registration or light fields in SceneUniforms. Standard, PBR, and NodeMaterial consume the scene-owned LightsUniforms UBO at @group(0) @binding(1). Per-mesh UBOs carry material-independent light selection (lc plus packed li: array<vec4<u32>, ceil(MAX_LIGHTS / 4)>) computed from LightBase.includedOnlyMeshes / excludedMeshes and includedOnlyMeshIds / excludedMeshIds (see Per-mesh light filtering); shaders index the scene lights array through those mesh indices. Exactly one eligible non-shadow PBR light uses material/pbr/fragments/singlelight-wgsl.ts; multiple lights or any shadow receiver use material/pbr/fragments/multilight-wgsl.ts.


Public API Surface

Shared Types (types.ts)

/** Shared base for all light types. Provides pipeline integration callbacks. */
export interface LightBase extends IWorldMatrixProvider, IParentable {
readonly lightType: string;
children: SceneNode[];
excludedMeshes?: ReadonlySet<Mesh>;
includedOnlyMeshes?: ReadonlySet<Mesh>;
excludedMeshIds?: ReadonlySet<string>;
includedOnlyMeshIds?: ReadonlySet<string>;
shadowGenerator?: ShadowGenerator;
parent: IWorldMatrixProvider | null;
readonly worldMatrix: Mat4;
readonly worldMatrixVersion: number;
}
/** @internal */
export interface LightBaseInternal extends LightBase {
readonly _writeLightUbo?: (data: Float32Array, offset: number) => void;
readonly _lightVersion: number;
}
export let MAX_LIGHTS = 16;
export function setMaxLights(n: number): void;
export const LIGHT_ENTRY_FLOATS = 16; // 4 × vec4 = 64 bytes per light

Runtime Intensity Updates (set-light-intensity.ts)

export function setLightIntensity(light: LightBase & { intensity: number }, intensity: number): void;
export function setLightDiffuseColor(light: LightBase & { diffuse: [number, number, number] }, color: readonly [number, number, number]): void;

Updates a finite scalar intensity and bumps the light-only version counter so the shared lights UBO is refreshed without invalidating the light's world matrix. Assigning an intensity during initial scene setup remains valid because the first UBO upload reads the final value; runtime changes use this setter. setLightDiffuseColor() provides the same invalidation for directional, point, and spot-light diffuse RGB changes.

Light Base (light-base.ts)

/** Create the SceneNode transform and version state shared by all light types. */
export function createLightBase(position: readonly [number, number, number]): {
node: SceneNode;
lvs: LightVersionState;
};
/** Add light-specific state to a SceneNode and return the same object. */
export function applyLightBase<R>(node: SceneNode, target: object, lvs?: LightVersionState): R;
export { ObservableVec3 } from "../math/observable-vec3.js";

Lights are full SceneNodes, with the same position, rotation, scaling, parent, children, and world-matrix behavior as meshes and transform nodes. The local direction is transformed by that world matrix, so direct quaternion/Euler writes orient the rendered light while preserving the existing direction API. setParent therefore uses the standard SceneNode path without a light-specific runtime branch. UBO writers normalize the world direction so parent scale cannot alter directional intensity or spotlight cone tests. _lightVersion includes the world matrix version, so ancestor motion refreshes GPU light data. Shadow builders likewise derive light position/direction from worldMatrix.

Directional Light (directional-light.ts)

export interface DirectionalLight extends LightBase {
readonly lightType: "directional";
direction: ObservableVec3;
position: ObservableVec3;
diffuse: [number, number, number];
specular: [number, number, number];
intensity: number;
}
export function createDirectionalLight(
direction: [number, number, number],
intensity?: number // Default: 1
): DirectionalLight;

Default values:

PropertyDefault
lightType
'directional'
direction
(parameter)
position
(0, 0, 0) via ObservableVec3
diffuse
[1, 1, 1]
specular
[1, 1, 1]
intensity
1

Point Light (point-light.ts)

export interface PointLight extends LightBase {
readonly lightType: "point";
position: ObservableVec3;
diffuse: [number, number, number];
specular: [number, number, number];
intensity: number;
range: number;
}
export function createPointLight(
position: [number, number, number],
intensity?: number // Default: 1.0
): PointLight;

Default values:

PropertyDefault
lightType
'point'
position
(parameter) via ObservableVec3
diffuse
[1, 1, 1]
specular
[1, 1, 1]
intensity
1.0
range
Number.MAX_VALUE

Local matrix: createTranslationMat4(position.x, position.y, position.z) — position only, no orientation.

Hemispheric Light (hemispheric.ts)

export interface HemisphericLight extends LightBase {
readonly lightType: "hemispheric";
direction: ObservableVec3;
intensity: number;
diffuseColor: [number, number, number];
specularColor: [number, number, number];
groundColor: [number, number, number];
}
export function createHemisphericLight(
direction?: [number, number, number], // Default: [0, 1, 0]
intensity?: number // Default: 1.0
): HemisphericLight;

Default values:

PropertyDefault
lightType
'hemispheric'
direction
(0, 1, 0) via ObservableVec3
intensity
1.0
diffuseColor
[1, 1, 1]
specularColor
[1, 1, 1]
groundColor
[0, 0, 0]

Spot Light (spot-light.ts)

export interface SpotLight extends LightBase {
readonly lightType: "spot";
position: ObservableVec3;
direction: ObservableVec3;
/** Full cone angle in radians. */
angle: number;
/** Falloff exponent — higher = sharper spotlight. */
exponent: number;
diffuse: [number, number, number];
specular: [number, number, number];
intensity: number;
range: number;
}
export function createSpotLight(
position: [number, number, number],
direction: [number, number, number],
angle: number,
exponent: number,
intensity?: number // Default: 1.0
): SpotLight;

Default values:

PropertyDefault
lightType
'spot'
position
(parameter) via ObservableVec3
direction
(parameter) via ObservableVec3
angle
(parameter)
exponent
(parameter)
diffuse
[1, 1, 1]
specular
[1, 1, 1]
intensity
1.0
range
Number.MAX_VALUE

Local matrix: Uses localMatrixFromDirection(direction, position) — both orientation and position.


Internal Architecture

Data Structures

All lights are plain JavaScript objects (POJOs) with Object.defineProperties-based world-matrix accessors — no classes, no GPU resources. The scene owns one GPU LightsUniforms buffer for scene.lights; materials only declare/read the fixed group-0 binding.

Light Base Infrastructure (light-base.ts)

Every light factory:

  1. Calls createLightBase(getLocalMatrix) which returns { wm, onDirty }:
    • wm: WorldMatrixAccessors — provides getWorldMatrix(), getWorldMatrixVersion(), markLocalDirty(), and parent get/set
    • onDirty: callback that calls wm.markLocalDirty() — passed to ObservableVec3 constructors
  2. Builds the light data object with an _writeLightUbo callback
  3. Calls applyWorldMatrixAccessors(target, wm) which uses Object.defineProperties to add parent, worldMatrix, and worldMatrixVersion as getters/setters

This pattern eliminates duplicated world-matrix boilerplate across all light types.

Light Type Discrimination (Standard Material)

Each light writes its type flag at data[offset + 3] in _writeLightUbo:

vLightData.wLight TypePosition/Direction Source
0
Point
xyz = world position (worldMatrix col 3)
1
Directional
xyz = world direction (worldMatrix col 2)
2
Spot
xyz = world position (worldMatrix col 3)
3
Hemispheric
xyz = world direction (worldMatrix col 2)

Lights UBO Layout (render/lights-ubo.ts)

Standard, PBR, and NodeMaterial pipelines use a shared scene lights UBO supporting up to MAX_LIGHTS = 16 packed scene lights by default. setMaxLights(n) may adjust the cap before pipelines/UBOs are created. Unlike Babylon.js's default maxSimultaneousLights = 4 per material, Babylon Lite's MAX_LIGHTS is the total scene-wide packed-light capacity.

The frame/pass bind group layout is:

GroupBindingOwnerContents
0
0
RenderTask
Per-pass SceneUniforms (camera, fog, image processing, environment)
0
1
SceneContextInternal via scene-light-state.ts
Scene-wide LightsUniforms packed from scene.lights

Material/mesh bind groups never contain light buffers. Mesh UBOs append:

lc: u32,
li: array<vec4<u32>, ceil(MAX_LIGHTS / 4)>,

Each packed index addresses lights.lights[index] in the scene-wide UBO (li[i / 4u][i % 4u]). render/mesh-light-selection.ts computes this list per mesh with lightAffectsMesh(light, mesh), skipping lights the mesh is filtered out of by either pair of lists (see Per-mesh light filtering).

Default total UBO size: getLightsUboSize() = 16 + MAX_LIGHTS × 64 bytes (1040 bytes when MAX_LIGHTS = 16)

Layout:

Offset (bytes)SizeContent
0–3
4B
count (u32) — number of active lights
4–15
12B
padding (3 × u32)
16–79
64B
Light 0 entry (4 × vec4)
80–143
64B
Light 1 entry (4 × vec4)
144–207
64B
Light 2 entry (4 × vec4)
208–271
64B
Light 3 entry (4 × vec4)

Per-light entry layout (LIGHT_ENTRY_FLOATS = 16, 64 bytes):

Float IndexDirectional (w=1)Point (w=0)Spot (w=2)Hemispheric (w=3)
[0–2]
direction (col2)
position (col3)
position (col3)
direction (col2)
[3]
1 (type flag)
0 (type flag)
2 (type flag)
3 (type flag)
[4–6]
diffuse × intensity
diffuse × intensity
diffuse × intensity
diffuseColor × intensity
[7]
MAX_VALUE (range)
range
range
(unused)
[8–10]
specular × intensity
specular × intensity
specular × intensity
specularColor × intensity
[11]
(unused)
(unused)
exponent
(unused)
[12–14]
(unused)
(unused)
direction (col2)
groundColor × intensity
[15]
(unused)
(unused)
cos(angle/2)
(unused)

UBO Functions (lights-ubo.ts)

/** Fill a Float32Array with standard light data. */
export function fillLightsData(data: Float32Array, lights: readonly LightBase[]): void;
/** Current lights UBO byte size for the active MAX_LIGHTS value. */
export function getLightsUboSize(): number;
/** Create a new lights UBO from all compatible lights. */
export function writeLightsUBO(engine: EngineContextInternal, lights: readonly LightBase[]): GPUBuffer;
/** Refresh an existing lights UBO with current light state. */
export function refreshLightsUBO(engine: EngineContextInternal, buffer: GPUBuffer, lights: readonly LightBase[], scratch: Float32Array): void;

fillLightsData algorithm:

  1. Zero the entire Float32Array
  2. Iterate lights, skip those without _writeLightUbo, stop at MAX_LIGHTS
  3. Call each light's _writeLightUbo(data, headerFloats + count * LIGHT_ENTRY_FLOATS)
  4. Write count into the first u32 slot via Uint32Array view

Per-mesh light filtering

Two pairs of lists on LightBase restrict which meshes a light reaches. render/mesh-light-selection.ts exports the single predicate both the packed selection and the material feature mirrors use:

export function lightAffectsMesh(light: LightBase, mesh: Mesh): boolean;

Object-identity lists. excludedMeshes and includedOnlyMeshes are ReadonlySet<Mesh>. Both apply: a non-empty includedOnlyMeshes restricts the light to those meshes, and excludedMeshes then subtracts from the result. This matches Babylon.js Light.canAffectMesh.

ID lists. excludedMeshIds and includedOnlyMeshIds are ReadonlySet<string> matched against Mesh.id, which only the .babylon loader writes. Their precedence differs and is deliberate: a non-empty includedOnlyMeshIds wins outright and exclusions are not subtracted from it.

Precedence. The object lists are evaluated first. When includedOnlyMeshes is non-empty the ID lists are not consulted at all; otherwise an object exclusion is final, and only an unexcluded mesh falls through to the ID lists. A light with no list set takes an allocation-free path.

An ID cannot identify a mesh on its own, which is why the object lists exist: loading one asset twice yields distinct meshes carrying the same asset-local ID, and a clone is spread from its source, so it arrives with the source's ID. Compat clears id on a mesh clone for that reason.

Cloning. copyLightBase in light/light-clone.ts carries all four sets onto the clone, so a cloned light keeps its filter rather than becoming unrestricted. The sets are shared by reference, not copied.

Consistency with shader selection. The selection is baked when a renderable is built: into the shader's light-mode permutation, into the mesh UBO's packed indices, and into any material feature bits that mirror the selection (SceneLightingExtension._pbrFeatures(mesh), used by rectangular area lights). buildPbrRenderables therefore alternates scanning the selection and loading what is missing until a scan finds nothing missing, so the light WGSL it composes against is always a superset of what the final selection needs. The loop ends on a scan rather than on an await, which is what makes the guarantee hold against an edit landing inside the loading itself, and it terminates because loading only ever adds over a finite set of paths.

Lifecycle. The per-frame updater only revisits the selection when the light count or the mesh's world matrix changes, so an edit under a stable light count needs a renderable rebuild. Compat drives that:

  1. Light.excludedMeshes / includedOnlyMeshes hand out the light's own live array with push and splice hooked, the two methods Babylon.js hooks. One hook per array, reading a subscriber set, so an array shared between lights carries a single wrapper.
  2. Publishing is coalesced onto the microtask queue, and the rebuild is coalesced per scene. Empty lists publish undefined.
  3. Scene._hasStarted turns true several awaits before registerScene sets _built, and rebuildSceneRenderables is a documented no-op until then. An edit inside that window is held on the engine's late-work queue and applied once registration finishes, so it reaches groups already built.
  4. A light is released from an array when its list is replaced, when the light is disposed, and when its scene is disposed (through onSceneDispose), leaving other live consumers of that array subscribed. The scene registration is made synchronously when a list is handed out or edited, not by the publish queued behind it, so a scene disposed in the same tick still releases the light, and an array shared out of one light into another scene's light does not keep the first light's scene reachable. A list touched when the scene is already disposed releases immediately, since a disposal callback registered then would never run.
  5. Retiring a mesh splices it out of every scene light's two lists, as Babylon.js does in its own AbstractMesh.dispose. A dead mesh left in an inclusion would keep that restriction alive and leave every surviving mesh unlit by the light. The splice goes through the hook, so the republish and the rebuild follow the ordinary edit path and a list shared between lights notifies all of them. It hangs off wrapper retirement rather than node disposal, so a mesh an AssetContainer tears down prunes as well.
  6. The rebuild is skipped for a light held by a clustered container, which is outside the per-mesh light path. Babylon.js skips its own resync for the same reason.

PBR Light Shader Paths

PBR materials consume the same packed LightEntry layout as Standard materials. The PBR renderable builder decides which WGSL helper to import once per scene:

ConditionWGSL helperBehavior
Mesh has exactly one eligible light and no shadow receiver path
material/pbr/fragments/singlelight-wgsl.ts
Non-looping direct-light code specialized by that light's lightType; reads lights.lights[mli(0u)]
Mesh has multiple eligible lights, or any shadow receiver path
material/pbr/fragments/multilight-wgsl.ts
Generic computePbrLight() + loop over mesh.lc; shadow fragment writes per-scene-light shadow factors

Supported PBR light types are hemispheric, directional, point, and spot. PBR materials default to physical inverse-square point/spot falloff. Materials with usePhysicalLightFalloff: false use Babylon's Standard-style falloff instead: linear range attenuation plus spot cone exponent attenuation.

Shader Lighting Math (Standard Material)

(Defined in standard-textured.fragment.wgsl, consumed via lights UBO)

Point light attenuation:

direction = lightPosition - fragmentPosition
attenuation = max(0, 1 - length(direction) / range)
lightVector = normalize(direction)

Directional light:

lightVector = normalize(-lightDirection)
attenuation = 1.0

Spot light attenuation:

lightVector = normalize(lightPosition - fragmentPosition)
attenuation = max(0, 1 - length(lightPosition - fragmentPosition) / range)
cosAngle = dot(normalize(lightDirection), -lightVector)
spotFalloff = max(0, cosAngle - cosHalfAngle) ^ exponent
attenuation *= spotFalloff

Hemispheric light:

lightVector = normalize(lightDirection)
NdotL = dot(normal, lightVector) * 0.5 + 0.5 // wrapped diffuse
diffuse = mix(groundColor, diffuseColor, NdotL) * intensity

Diffuse (Lambertian):

ndl = max(0, dot(normal, lightVector))
diffuse = ndl * lightDiffuseColor * attenuation

Specular (Blinn-Phong):

halfVector = normalize(viewDir + lightVector)
specComp = pow(max(0, dot(normal, halfVector)), max(1, glossiness))
specular = specComp * lightSpecularColor * attenuation

Pipeline Configuration

Light modules do not create GPU pipelines. They produce plain data consumed by material pipelines.

The lights UBO (render/lights-ubo.ts) creates a single GPUBuffer with UNIFORM | COPY_DST usage. SceneContextInternal._lightGpuState stores it on the scene, refreshes it per-frame through refreshSceneLightsUBO(), and recreates it if the active MAX_LIGHTS size changes. The default size is 1040 bytes, but the size follows MAX_LIGHTS.


Shader Logic

Light modules do not contain shaders. Lighting computation lives in material shader modules: Standard and NodeMaterial loop over mesh.lc / mli(i) into the group-0 lights UBO, and PBR dynamically imports either singlelight-wgsl.ts or multilight-wgsl.ts from material/pbr/fragments/.


State Machine / Lifecycle

Light Creation

// Directional
const light = createDirectionalLight([0, -1, 0], 1.5);
light.position.set(10, 20, 10); // triggers onDirty → markLocalDirty
// Point
const point = createPointLight([5, 3, 0], 2.0);
point.range = 100;
// Hemispheric
const hemi = createHemisphericLight([0, 1, 0], 0.7);
hemi.groundColor = [0.1, 0.1, 0.1];
// Spot
const spot = createSpotLight([0, 10, 0], [0, -1, 0], Math.PI / 3, 2.0, 1.5);
spot.angle = Math.PI / 4;

Mutation & Dirty Tracking

  • Setting direction.x, direction.y, direction.z or calling direction.set(x,y,z) on any ObservableVec3 triggers onDirty() → wm.markLocalDirty() → increments worldMatrixVersion
  • worldMatrix getter lazily recomputes from getLocalMatrix() and parent chain when dirty
  • _writeLightUbo reads from worldMatrix (which auto-resolves parent transforms)

Lights UBO Lifecycle

  1. Creation: ensureSceneLightState(engine, scene) allocates a getLightsUboSize() UBO and stores it on SceneContextInternal._lightGpuState.
  2. Group-0 binding: every RenderTask binds its task-owned scene UBO at binding 0 and the scene-owned lights UBO at binding 1.
  3. Per-frame refresh: refreshSceneLightsUBO(engine, scene) compares the aggregate light version and light count, then writes the shared UBO only when needed.
  4. Light filtering: only lights with _writeLightUbo defined are packed; up to MAX_LIGHTS.
  5. Resize for cap changes: if MAX_LIGHTS changes and the UBO byte size changes, ensureSceneLightState() destroys/recreates the scene light buffer and render-pass tasks rebuild their group-0 bind group.
  6. Mesh selection: material renderables write per-mesh lc and packed li indices into the mesh UBO; this selection respects both the object-identity lists and the ID lists.

No GPU resources are created by light modules. Light packing and scene-owned GPU state live in lights-ubo.ts; materials only own shaders, mesh/material UBOs, textures, and bind groups.


Babylon.js Equivalence Map

Babylon LiteBabylon.js
createDirectionalLight(dir, intensity)
new DirectionalLight(name, dir, scene)
DirectionalLight.direction (ObservableVec3)
DirectionalLight.direction (Vector3)
DirectionalLight.position (ObservableVec3)
DirectionalLight.position (Vector3)
DirectionalLight.diffuse
DirectionalLight.diffuse
DirectionalLight.specular
DirectionalLight.specular
DirectionalLight.intensity
DirectionalLight.intensity
createPointLight(pos, intensity)
new PointLight(name, pos, scene)
PointLight.range = Number.MAX_VALUE
PointLight.range (default very large)
createHemisphericLight(dir, intensity)
new HemisphericLight(name, dir, scene)
HemisphericLight.diffuseColor
HemisphericLight.diffuse
HemisphericLight.specularColor
HemisphericLight.specular
HemisphericLight.groundColor
HemisphericLight.groundColor
createSpotLight(pos, dir, angle, exp, int)
new SpotLight(name, pos, dir, angle, exp, scene)
SpotLight.angle
SpotLight.angle
SpotLight.exponent
SpotLight.exponent
SpotLight.range
SpotLight.range
LightBase.lightType string discriminator
Class hierarchy + getTypeID() (internal)
LightBase.excludedMeshes
Light.excludedMeshes
LightBase.includedOnlyMeshes
Light.includedOnlyMeshes
LightBase.excludedMeshIds
.babylon loader light lists (no Babylon.js equivalent)
LightBase.includedOnlyMeshIds
.babylon loader light lists (no Babylon.js equivalent)
LightBase.shadowGenerator
Light.getShadowGenerator()
LightBase.parent (IWorldMatrixProvider)
Light.parent (TransformNode)
Plain data objects (no scene ref)
Class instances with scene reference
ObservableVec3 with dirty callback
Vector3 + _markAsDirty() pattern
localMatrixFromDirection()
Light._buildUniformLayout() (internal)
Light type w flag (0/1/2/3)
Internal type system (LIGHTTYPEID_*)
MAX_LIGHTS = 16 scene-wide cap
maxSimultaneousLights (default 4 per material)
fillLightsData() / writeLightsUBO()
Babylon's internal light UBO building
refreshLightsUBO()
Per-frame light uniform update
singlelight-wgsl.ts / multilight-wgsl.ts
PBR shader includes (pbrDirectLightingSetupFunctions, etc.)

Key Differences from Babylon.js

  1. No scene reference — Babylon Lite lights are POJOs with world-matrix accessors; Babylon.js lights are class instances that register with a Scene.
  2. lightType string discriminator — Instead of class hierarchy, each light has a lightType string literal ('directional', 'point', 'spot', 'hemispheric').
  3. ObservableVec3 — Direction/position use observable vectors with dirty callbacks, replacing Babylon's Vector3 + manual dirty tracking.
  4. Shared lights UBO — Standard and PBR pack up to MAX_LIGHTS scene lights into one UBO; Babylon.js uses material-scoped light defines/uniforms and defaults to 4 simultaneous lights per material.
  5. Tree-shakable PBR light code — PBR imports the one-light or multi-light WGSL helper only when needed; Babylon.js includes broad light shader support.
  6. No shadow caster list on lights or generators — Shadow casters are scene/frame-graph ShadowTask inputs registered for a generator. Lights only reference their shadow generator via shadowGenerator property.

Dependencies

types.ts

  • ../math/types.js — Mat4
  • ../scene/parentable.js — IWorldMatrixProvider, IParentable
  • ../shadow/shadow-generator.js — ShadowGenerator (type-only import)

light-base.ts

  • ../math/types.js — Mat4
  • ../scene/parentable.js — IWorldMatrixProvider
  • ../scene/world-matrix-state.js — createWorldMatrixState, WorldMatrixAccessors
  • ../math/observable-vec3.js — ObservableVec3 (re-exported)

light-matrix.ts

  • ../math/types.js — Mat4

directional-light.ts

  • ./types.js — LightBase
  • ./light-base.js — createLightBase, applyWorldMatrixAccessors, ObservableVec3
  • ./light-matrix.js — localMatrixFromDirection

point-light.ts

  • ./types.js — LightBase
  • ../math/mat4.js — createTranslationMat4
  • ./light-base.js — createLightBase, applyWorldMatrixAccessors, ObservableVec3

hemispheric.ts

  • ./types.js — LightBase
  • ./light-base.js — createLightBase, applyWorldMatrixAccessors, ObservableVec3
  • ./light-matrix.js — localMatrixFromDirection

spot-light.ts

  • ./types.js — LightBase
  • ./light-base.js — createLightBase, applyWorldMatrixAccessors, ObservableVec3
  • ./light-matrix.js — localMatrixFromDirection

lights-ubo.ts

  • ../light/types.js — LightBase, LightBaseInternal, MAX_LIGHTS, LIGHT_ENTRY_FLOATS

Test Specification

  1. Directional light defaults — createDirectionalLight([0, -1, 0]) returns lightType: 'directional', direction (0,-1,0), position (0,0,0), diffuse [1,1,1], specular [1,1,1], intensity 1.
  2. Point light defaults — createPointLight([5, 3, 0]) returns lightType: 'point', position (5,3,0), diffuse [1,1,1], specular [1,1,1], intensity 1, range Number.MAX_VALUE.
  3. Hemispheric light defaults — createHemisphericLight() returns lightType: 'hemispheric', direction (0,1,0), intensity 1, diffuseColor [1,1,1], specularColor [1,1,1], groundColor [0,0,0].
  4. Spot light defaults — createSpotLight([0,10,0], [0,-1,0], PI/3, 2) returns lightType: 'spot', diffuse [1,1,1], specular [1,1,1], intensity 1, range Number.MAX_VALUE.
  5. Custom intensity — createDirectionalLight([1,0,0], 2.5).intensity should be 2.5.
  6. Mutability — All properties should be directly assignable. ObservableVec3 properties support .x, .y, .z setters and .set(x,y,z).
  7. Dirty tracking — Setting direction.x = 5 should increment worldMatrixVersion.
  8. World transform — Directional light's local direction should be normalized after transformation by its world matrix.
  9. Parent support — Setting light.parent should affect worldMatrix computation.
  10. Light type flags — _writeLightUbo should set w=0 (point), w=1 (directional), w=2 (spot), w=3 (hemispheric).
  11. Spot UBO packing — Spot light writes exponent at [11], direction at [12–14], cos(angle/2) at [15].
  12. Light UBO packing — For directional light: lightData.w = 1, colors premultiplied by intensity.
  13. Light UBO packing — For point light: lightData.w = 0, lightDiffuse.a = range.
  14. Lights UBO size — getLightsUboSize() = 272 bytes by default (16 header + 4 × 64).
  15. fillLightsData count — With 6 lights, only first 4 with _writeLightUbo are packed.
  16. PBR single-light selection — One non-shadow light imports singlelight-wgsl.ts, not the generic multi-light loop.
  17. PBR multi-light selection — Multiple lights or shadow receivers import multilight-wgsl.ts.

File Manifest

FileRole
src/light/types.ts
LightBase, LightBaseInternal interfaces; MAX_LIGHTS, setMaxLights(), LIGHT_ENTRY_FLOATS
src/light/light-base.ts
createLightBase(), applyWorldMatrixAccessors() — shared world-matrix state factory; re-exports ObservableVec3
src/light/light-matrix.ts
localMatrixFromDirection() — builds local 4×4 matrix from direction + position
src/light/directional-light.ts
DirectionalLight interface + createDirectionalLight() factory
src/light/point-light.ts
PointLight interface + createPointLight() factory
src/light/hemispheric.ts
HemisphericLight interface + createHemisphericLight() factory
src/light/spot-light.ts
SpotLight interface + createSpotLight() factory
src/render/lights-ubo.ts
Shared lights UBO system — getLightsUboSize(), scene light GPU state, mesh light selection; packs up to MAX_LIGHTS scene lights