Implement BSP leaf ambient cube lighting for entities #22

Closed
opened 2026-03-18 08:48:35 +00:00 by kit · 0 comments
Owner

Entities currently use a flat ambient + sun * NdotL lighting model (useSunLighting path in the uber-shader), which means they're always fully sunlit regardless of whether they're indoors, under a roof, or in shadow. The Source engine uses leaf ambient cubes — pre-computed 6-directional light samples baked into BSP leaves by VRAD — to properly light entities with occlusion.

How Source does it

  1. Each BSP leaf has one or more ambient light samples stored in lumps 55/56 (LUMP_LEAF_AMBIENT_LIGHTING) and indexed by lumps 51/52 (LUMP_LEAF_AMBIENT_INDEX)
  2. Each sample is a CompressedLightCube — 6 ColorRGBExp32 values for ±X, ±Y, ±Z directions — plus an xyz position within the leaf
  3. At runtime, the engine walks the BSP node tree to find which leaf an entity is in, picks the nearest ambient sample, and uploads the 6-color cube as shader uniforms
  4. The shader evaluates: nSquared.x * cube[±X] + nSquared.y * cube[±Y] + nSquared.z * cube[±Z] — a weighted blend based on the surface normal

Requirements

BSP parsing

  • Parse lump 10 (LUMP_LEAFS) for leaf bounding boxes
  • Parse lump 5 (LUMP_NODES) for the BSP node tree (plane splits + children)
  • Parse lumps 51/55 (HDR) or 52/56 (LDR) for leaf ambient index and lighting data
  • Decode ColorRGBExp32: channel * 2^exponent for each RGB component

Leaf lookup

  • Implement BSP node tree traversal: walk from root, test point against split plane, recurse into front/back child until reaching a leaf
  • Leaf indices in node children are encoded as ~leafIndex (bitwise NOT) when negative

Per-entity ambient sampling

  • For each entity, find its BSP leaf from its Source-space position
  • Look up leafAmbientIndex[leafIndex] to get the sample range
  • Find the nearest dleafambientlighting_t sample by converting fractional xyz coords to world space
  • Decode the CompressedLightCube into 6 linear RGB values

Shader changes

  • Replace the useSunLighting path with ambient cube evaluation
  • Add uniform vec3 ambientCube[6] to the uber-shader
  • Implement: nSq.x * cube[neg.x] + nSq.y * cube[2+neg.y] + nSq.z * cube[4+neg.z]
  • Coordinate system: Source cube order is +X, -X, +Y, -Y, +Z, -Z — must match Three.js world normals after coordinate conversion

Scene manager integration

  • When placing/updating entity positions, look up the leaf and sample ambient cube
  • Upload per-entity ambient cube uniforms to the entity's materials
  • Since Three.js Group.clone() shares materials, entities sharing a model template will need their own material instances (or a different uniform strategy)

Data structures

ColorRGBExp32 (4 bytes): r:u8, g:u8, b:u8, exponent:i8
CompressedLightCube (24 bytes): ColorRGBExp32[6]
dleafambientlighting_t (28 bytes): CompressedLightCube + x:u8, y:u8, z:u8, pad:u8
dleafambientindex_t (4 bytes): ambientSampleCount:u16, firstAmbientSample:u16

Context

  • Current entity lighting: client/src/bsp/material.js (uber-shader useSunLighting path)
  • BSP parser: client/src/bsp/parser.js
  • Entity placement: client/src/scene.js (_replaceWithModel, _updateEntity)
  • Relates to #20 (custom shader materials for entities)
Entities currently use a flat `ambient + sun * NdotL` lighting model (`useSunLighting` path in the uber-shader), which means they're always fully sunlit regardless of whether they're indoors, under a roof, or in shadow. The Source engine uses **leaf ambient cubes** — pre-computed 6-directional light samples baked into BSP leaves by VRAD — to properly light entities with occlusion. ## How Source does it 1. Each BSP leaf has one or more ambient light samples stored in lumps 55/56 (`LUMP_LEAF_AMBIENT_LIGHTING`) and indexed by lumps 51/52 (`LUMP_LEAF_AMBIENT_INDEX`) 2. Each sample is a `CompressedLightCube` — 6 `ColorRGBExp32` values for ±X, ±Y, ±Z directions — plus an xyz position within the leaf 3. At runtime, the engine walks the BSP node tree to find which leaf an entity is in, picks the nearest ambient sample, and uploads the 6-color cube as shader uniforms 4. The shader evaluates: `nSquared.x * cube[±X] + nSquared.y * cube[±Y] + nSquared.z * cube[±Z]` — a weighted blend based on the surface normal ## Requirements ### BSP parsing - Parse lump 10 (`LUMP_LEAFS`) for leaf bounding boxes - Parse lump 5 (`LUMP_NODES`) for the BSP node tree (plane splits + children) - Parse lumps 51/55 (HDR) or 52/56 (LDR) for leaf ambient index and lighting data - Decode `ColorRGBExp32`: `channel * 2^exponent` for each RGB component ### Leaf lookup - Implement BSP node tree traversal: walk from root, test point against split plane, recurse into front/back child until reaching a leaf - Leaf indices in node children are encoded as `~leafIndex` (bitwise NOT) when negative ### Per-entity ambient sampling - For each entity, find its BSP leaf from its Source-space position - Look up `leafAmbientIndex[leafIndex]` to get the sample range - Find the nearest `dleafambientlighting_t` sample by converting fractional xyz coords to world space - Decode the `CompressedLightCube` into 6 linear RGB values ### Shader changes - Replace the `useSunLighting` path with ambient cube evaluation - Add `uniform vec3 ambientCube[6]` to the uber-shader - Implement: `nSq.x * cube[neg.x] + nSq.y * cube[2+neg.y] + nSq.z * cube[4+neg.z]` - Coordinate system: Source cube order is +X, -X, +Y, -Y, +Z, -Z — must match Three.js world normals after coordinate conversion ### Scene manager integration - When placing/updating entity positions, look up the leaf and sample ambient cube - Upload per-entity ambient cube uniforms to the entity's materials - Since Three.js `Group.clone()` shares materials, entities sharing a model template will need their own material instances (or a different uniform strategy) ## Data structures ``` ColorRGBExp32 (4 bytes): r:u8, g:u8, b:u8, exponent:i8 CompressedLightCube (24 bytes): ColorRGBExp32[6] dleafambientlighting_t (28 bytes): CompressedLightCube + x:u8, y:u8, z:u8, pad:u8 dleafambientindex_t (4 bytes): ambientSampleCount:u16, firstAmbientSample:u16 ``` ## Context - Current entity lighting: `client/src/bsp/material.js` (uber-shader `useSunLighting` path) - BSP parser: `client/src/bsp/parser.js` - Entity placement: `client/src/scene.js` (`_replaceWithModel`, `_updateEntity`) - Relates to #20 (custom shader materials for entities)
kit closed this issue 2026-03-18 09:05:09 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kit/gmod-web-stream#22
No description provided.