Implements a new SH light grid that runs alongside the legacy Q3 light grid, storing 9 RGB L2 spherical harmonic coefficients per grid point for accurate directional lighting of dynamic objects from all angles. BSP format: v47 with 19-lump header (160 bytes) when -sh is used, v46 with 17-lump header (144 bytes) otherwise. SH data stored in LUMP_LIGHTGRID_SH (index 18) with a header containing grid bounds/size/mins followed by the coefficient array. Stock Q3 engines read v46 lumps unchanged. New CLI flags: -sh (enable), -gridscalesh (density multiplier, default 2x), -gridsh (explicit cell size). SH grid receives bounced light with -bouncegrid. Also adds libjpeg-turbo as a proper build dependency with its own vcxproj, fixing the previous external engine path requirement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
351 lines
17 KiB
Markdown
351 lines
17 KiB
Markdown
# Source Engine 2013 (VRAD) vs Q3Map2 — Lighting System Analysis
|
|
|
|
## 1. VRAD Overview
|
|
|
|
VRAD (Valve RADiosity) is the final stage of the Source Engine map compilation pipeline (after VBSP and VVIS). It takes a compiled BSP file and embeds all precomputed lighting data into it.
|
|
|
|
### Pipeline Stages
|
|
|
|
1. **Initialization** — Load BSP, parse light entities, load `lights.rad` / `<mapname>.rad` texture-light definitions
|
|
2. **Patch Creation** — Create one patch per face, then recursively subdivide into a binary tree until patches are smaller than the "chop" size on all axes
|
|
3. **Macro Texture Setup** — Optional world-space TGA mask for lighting modulation
|
|
4. **Visibility Determination** — Per-face visibility using PVS data (or mark all visible in incremental mode)
|
|
5. **Direct Lighting** — Faces broken into triangles, partitioned into a KD-tree. For each luxel sample, ray-cast to light sources through the KD-tree. Apply attenuation (constant/linear/quadratic).
|
|
6. **Indirect Lighting (Radiosity)** — Compute form factors between patches (Fij), then use **progressive refinement** (shooting from brightest patch each iteration) rather than full matrix inversion. Default 100 bounces.
|
|
7. **Post-Processing** — Displacement lighting, detail prop lighting, leaf ambient cubes, static prop per-vertex lighting
|
|
8. **Export** — Write lightmaps to BSP lighting lump
|
|
|
|
### Core Radiosity Equation
|
|
|
|
```
|
|
Bi = Ei + Pi * SUM_j(Bj * Fij)
|
|
```
|
|
|
|
Where Bi = radiosity of patch i, Ei = emissivity, Pi = reflectivity, Fij = form factor (proportion of light from j reaching i).
|
|
|
|
VRAD uses **progressive refinement** for memory efficiency — it does not store a full NxN form factor matrix.
|
|
|
|
---
|
|
|
|
## 2. What Source Engine Bakes
|
|
|
|
### 2.1 Lightmaps
|
|
|
|
- **Luxels** (lighting pixels) form a grid on every brush face
|
|
- Resolution controlled by "Lightmap Scale" per face (default 16 units per luxel; can be 2, 4, 8, 16, 32, 64, 128)
|
|
- **Storage format**: `ColorRGBExp32` — 4 bytes per sample (R, G, B as bytes + signed exponent byte). Actual color = (R, G, B) * 2^exponent. This gives HDR range from the storage format itself.
|
|
- Lightmaps are packed into pages in the BSP lighting lump
|
|
- Up to **4 light styles** per face (each style gets its own lightmap layer)
|
|
- **Bumpmapped surfaces store 4x lightmap samples**: 3 directional basis lightmaps + 1 flat (non-bumped) fallback
|
|
|
|
### 2.2 Ambient Cubes (CompressedLightCube)
|
|
|
|
- Stored per **visleaf** (not per-face) — a sparse set of sample points in each leaf
|
|
- Each ambient cube = **6 ColorRGBExp32 values** (one per cardinal direction: +X, -X, +Y, -Y, +Z, -Z)
|
|
- Used to light **dynamic objects** (players, physics props, prop_dynamic) that don't have lightmaps
|
|
- Evaluation: weighted blend of 6 RGB values based on the world-space normal of the receiving surface
|
|
- VRAD distributes samples somewhat evenly across leaves; smaller leaves get denser coverage
|
|
|
|
### 2.3 Light Grid (comparison to Q3)
|
|
|
|
Source does NOT use a uniform 3D grid like Quake 3. Instead, it uses the **per-leaf ambient cube** system described above. This is more memory-efficient but less granular — dynamic objects in large leaves get coarser lighting approximation.
|
|
|
|
### 2.4 Static Prop Per-Vertex Lighting
|
|
|
|
- Enabled via `-StaticPropLighting` VRAD flag
|
|
- For each vertex of each `prop_static`, VRAD computes a color+brightness value
|
|
- The engine interpolates between vertices at runtime to create smooth gradients
|
|
- Without this flag, static props use **origin-based lighting** (single sample at the prop's origin, applied uniformly)
|
|
- Static props can also opt into **bounced lighting** via keyvalue "Enable Bounced Lighting"
|
|
- `-StaticPropPolys` treats prop triangles as occluders for shadow casting
|
|
|
|
### 2.5 Bounced/Indirect Lighting (Radiosity)
|
|
|
|
- Faces subdivided into **patches** (binary tree structure)
|
|
- `CPatch` stores: spatial bounds, winding polygon, normal (phong-interpolated), accumulated light (totallight, directlight, samplelight), reflectivity, area
|
|
- **Samples** (one per luxel) are distinct from patches — samples map to luxel positions, patches accumulate radiosity
|
|
- Form factors computed between patch pairs based on visibility and geometry
|
|
- Progressive refinement shoots light from highest-energy patch each iteration
|
|
- Default bounce count: 100
|
|
- `-final` flag increases quality by casting more rays for sky and indirect light
|
|
|
|
---
|
|
|
|
## 3. Light Entity Types in Source Engine
|
|
|
|
### 3.1 `light` (Point Light)
|
|
|
|
- Omnidirectional point source
|
|
- Static (baked into lightmaps by VRAD)
|
|
- Parameters: color, brightness, constant/linear/quadratic attenuation
|
|
- Default falloff: pure inverse-square (constant=0, linear=0, quadratic=1)
|
|
- Can be named (makes it switchable, adds second lightmap page)
|
|
|
|
### 3.2 `light_spot` (Spotlight)
|
|
|
|
- Directional cone light with **inner cone** (full brightness) and **outer cone** (falloff to zero)
|
|
- Same attenuation model as `light`
|
|
- Static, baked by VRAD
|
|
|
|
### 3.3 `light_environment` (Sun/Sky)
|
|
|
|
- Defines **two lighting components**:
|
|
- **Brightness**: Direct sunlight (directional, parallel rays, specific color/intensity)
|
|
- **Ambient**: Diffuse skylight (omnidirectional fill, separate color/intensity)
|
|
- Direction set by entity's pitch/yaw
|
|
- Only one per map (last one wins)
|
|
- Constant falloff (parallel rays, no distance attenuation)
|
|
- Sky light enters through `tools/toolsskybox` surfaces
|
|
|
|
### 3.4 `light_dynamic`
|
|
|
|
- Calculated at runtime, NOT baked
|
|
- Casts **dlights** on brushes, **elights** on models
|
|
- Can be toggled on/off, can track entities
|
|
- Limit: **3 dynamic lights per model** simultaneously
|
|
- Cannot cast shadows
|
|
- Higher performance cost than static lights
|
|
|
|
### 3.5 `env_projectedtexture`
|
|
|
|
- Projects a texture as a spotlight with FOV control
|
|
- Fully dynamic, can move
|
|
- Only **1 active projected texture** at a time (engine limitation in most branches)
|
|
- Used for flashlights, dynamic shadows
|
|
|
|
### 3.6 `env_cascade_light` (CS:GO+)
|
|
|
|
- Cascaded shadow maps from sky
|
|
- 3 detail levels across configurable distance
|
|
- Cannot coexist with projected textures
|
|
|
|
---
|
|
|
|
## 4. Advanced Features
|
|
|
|
### 4.1 HDR Lighting Pipeline
|
|
|
|
- VRAD compiles **separate LDR and HDR lighting data** (flag `-both`, `-hdr`, or `-ldr`)
|
|
- With `-both`, VRAD runs **twice** (once for each mode)
|
|
- `ColorRGBExp32` format inherently supports HDR via the exponent byte (range far exceeds 0-255)
|
|
- At runtime, the engine performs **tone mapping**: bloom on colors above 100% brightness, virtual camera aperture adjustment for over-exposure
|
|
- `mat_hdr_level` controls HDR mode at runtime
|
|
- Left 4 Dead branch and later: single compile pass for both modes
|
|
|
|
### 4.2 Bump-Mapped Lightmaps (Radiosity Normal Mapping)
|
|
|
|
This is one of Source's most distinctive features, and it is **entirely absent from q3map2**.
|
|
|
|
- For bumpmapped surfaces, VRAD stores **3 directional lightmaps** plus 1 flat fallback (4x memory)
|
|
- The 3 lightmaps correspond to the **HL2 basis vectors** in tangent space:
|
|
|
|
```
|
|
basis0 = ( 0.816497, 0.0, 0.57735)
|
|
basis1 = (-0.408248, 0.707107, 0.57735)
|
|
basis2 = (-0.408248, -0.707107, 0.57735)
|
|
```
|
|
|
|
- These 3 vectors point roughly 120 degrees apart in the tangent plane, tilted 30 degrees up from the surface
|
|
- At runtime, the shader combines the 3 lightmaps weighted by the normal map:
|
|
|
|
```hlsl
|
|
diffuse = normal.x * lightmap0 + normal.y * lightmap1 + normal.z * lightmap2
|
|
```
|
|
|
|
(where normal is transformed into the RNM basis)
|
|
|
|
- This allows **per-pixel lighting variation from baked lightmaps** — bumps catch light directionally
|
|
|
|
### 4.3 Self-Shadowed Bumpmaps
|
|
|
|
- Chris Green's SIGGRAPH 2007 paper: "Efficient Self-Shadowed Radiosity Normal Mapping"
|
|
- Adds **directional occlusion** to bump maps at no extra texture memory cost
|
|
- When bump data comes from height maps, standard RNM only shows orientation effects; self-shadowing adds occlusion cues where bumps block light
|
|
- Actually **faster** than the non-shadowing solution due to optimized shader paths
|
|
- The directional basis vectors encode both lighting AND shadowing information
|
|
|
|
### 4.4 Light Styles (Switchable/Animated Lights)
|
|
|
|
- A light gets a "style" by: giving it a **targetname**, setting a **style index**, or setting a **pattern string**
|
|
- Switchable lights compile **two lightmap layers** (on/off states) per affected face
|
|
- Engine blends between layers at runtime
|
|
- Hard limit: **4 light styles per face**, **32 total lightmap pages**
|
|
- Animated patterns use character strings where 'a'=dark, 'z'=bright (e.g., "mmmaaammmaaam" for flickering)
|
|
- Exceeding limits causes "Too many light styles on a face" warning
|
|
|
|
### 4.5 Texture Light Emission (Surface Lights)
|
|
|
|
- Defined in `lights.rad` or `<mapname>.rad` files
|
|
- Format: `<texture_name> <R> <G> <B> <brightness>`
|
|
- Every luxel of a brush face using that texture emits light during VRAD compilation
|
|
- The emitting surface is NOT self-lit by its own emission (needs `$selfillum` or `UnlitGeneric` shader separately)
|
|
- Map-specific `.rad` overrides global `lights.rad`
|
|
- Conceptually similar to q3map2's `q3map_surfacelight` shader directive
|
|
|
|
### 4.6 Lightmap Filtering and Anti-Aliasing
|
|
|
|
- VRAD applies filtering to lightmap edges
|
|
- Supersampling available via quality settings
|
|
- `-final` flag dramatically increases ray count for smoother indirect lighting
|
|
- No explicit `-samples` or `-filter` flags like q3map2; quality is controlled via `-extra` / `-final` presets
|
|
|
|
### 4.7 Macro Textures
|
|
|
|
- Optional world-space TGA file (matching BSP name) modulates lighting globally
|
|
- Maps world bounds onto texture coordinates
|
|
- Alpha channel controls per-luxel darkening (255 = no effect, 0 = full shadow)
|
|
- Used for large-scale ambient occlusion or artistic lighting control
|
|
|
|
### 4.8 Ambient Occlusion
|
|
|
|
- Source Engine does NOT have a dedicated AO pass in VRAD (unlike q3map2's `-dirty` flag)
|
|
- AO-like effects emerge naturally from radiosity bounces and the self-shadowed RNM system
|
|
- Some Source Engine branches add screen-space AO at runtime
|
|
|
|
---
|
|
|
|
## 5. Feature Comparison Table
|
|
|
|
### Features Source Has That Q3Map2 Doesn't
|
|
|
|
| Feature | Source Engine (VRAD) | Q3Map2 | Gap Severity |
|
|
|---------|---------------------|--------|--------------|
|
|
| **HDR lightmaps** | `ColorRGBExp32` (4 bytes, HDR via exponent) | 24-bit RGB, clamped 0-255 | **Major** |
|
|
| **Bump-mapped lightmaps (RNM)** | 3 directional basis lightmaps + 1 flat per bumped face | Single flat lightmap only | **Major** |
|
|
| **Self-shadowed bumpmaps** | Directional occlusion baked into RNM basis | None | **Major** |
|
|
| **Ambient cubes** | 6-axis `CompressedLightCube` per leaf for dynamic objects | Uniform 3D grid (direction + ambient) | Medium |
|
|
| **True progressive radiosity** | Form-factor based, patch-to-patch energy transfer | Fake radiosity (spawn point lights at hit surfaces) | Medium |
|
|
| **Static prop per-vertex lighting** | VRAD traces light to each vertex of static models | No model-aware lighting | Medium |
|
|
| **Hard-falloff / smoothstep lights** | Start/end distance with smoothstep fade | No equivalent | Small |
|
|
| **CLQ attenuation model** | Full Constant/Linear/Quadratic per light | Simpler falloff | Small |
|
|
| **Macro textures** | World-space TGA modulates lighting globally | None | Small |
|
|
|
|
### Features Q3Map2 Already Has That Match or Exceed Source
|
|
|
|
| Feature | Q3Map2 | Source |
|
|
|---------|--------|--------|
|
|
| **Ambient occlusion** | Explicit `-dirty` flag with configurable depth/samples | No dedicated AO — emerges from bounces |
|
|
| **Deluxe maps** | Stores average light direction per luxel | Similar concept via RNM basis |
|
|
| **Floodlight** | Fills dark areas with ambient fill | No equivalent |
|
|
| **Light styles** | Basic support exists | More mature (4 layers, runtime blending) |
|
|
| **Surface lights** | `q3map_surfacelight` shader directive | `lights.rad` file — functionally equivalent |
|
|
| **Sun/sky system** | `_sun` entity + sky shaders with deviance jitter | `light_environment` — similar capability |
|
|
|
|
---
|
|
|
|
## 6. The RNM Basis — Source's Key Innovation
|
|
|
|
Source stores **3 directional lightmaps** per bumpmapped face using these tangent-space basis vectors:
|
|
|
|
```
|
|
basis0 = ( 0.8165, 0.0000, 0.5774) // 120° apart in tangent plane,
|
|
basis1 = (-0.4082, 0.7071, 0.5774) // tilted 30° up from surface
|
|
basis2 = (-0.4082, -0.7071, 0.5774)
|
|
```
|
|
|
|
The shader recombines at runtime:
|
|
|
|
```hlsl
|
|
diffuse = dot(n, b0) * lm0 + dot(n, b1) * lm1 + dot(n, b2) * lm2
|
|
```
|
|
|
|
This gives **per-pixel lighting variation from baked data**. This is the single biggest visual quality difference between Source and Q3-engine games.
|
|
|
|
---
|
|
|
|
## 7. Q3Map2 Lighting System Architecture (Current State)
|
|
|
|
### 5 Source Files (~340K total)
|
|
|
|
| File | Size | Role |
|
|
|------|------|------|
|
|
| `light.cpp` | 82K | Entry point (`LightMain`), entity lights, core sample tracing |
|
|
| `light_ydnar.cpp` | 106K | Lightmap mapping, illumination, dirt, floodlight |
|
|
| `lightmaps_ydnar.cpp` | 89K | Lightmap allocation, packing, storage |
|
|
| `light_bounce.cpp` | 25K | Radiosity / bounce lighting |
|
|
| `light_trace.cpp` | 39K | Ray tracing through BSP (shadow tests) |
|
|
|
|
### Pipeline Flow (orchestrated by `LightMain()`)
|
|
|
|
```
|
|
1. CreateEntityLights() — parse light entities from BSP
|
|
2. CreateSurfaceLights() — create area/sun/sky lights from shaders
|
|
3. SetupEnvelopes() — compute bounding boxes for light culling
|
|
4. SetupGrid() + TraceGrid() — light the 3D sample grid [threaded]
|
|
5. MapRawLightmap() — project surfaces onto lightmap UV [threaded]
|
|
6. DirtyRawLightmap() — ambient occlusion / dirt pass [threaded]
|
|
7. FloodLightRawLightmap() — fill dark areas with floodlight [threaded]
|
|
8. IlluminateRawLightmap() — trace all lights to luxels [threaded]
|
|
9. IlluminateVertexes() — light non-lightmapped surfaces [threaded]
|
|
10. Bounce loop (if bounce > 0):
|
|
└── RadCreateDiffuseLights() → SetupEnvelopes() → re-illuminate
|
|
11. StoreSurfaceLightmaps() — pack luxels into final lightmap pages
|
|
```
|
|
|
|
### Key Data Structures (in `q3map2.h`)
|
|
|
|
- **`light_t`** — light source (type, origin, color, photons, envelope, flags)
|
|
- **`ELightType`** — Point, Area, Spotlight, Sun
|
|
- **`LightFlags`** — AttenLinear, AttenAngle, Negative, Dark, Grid, Surfaces, Fast...
|
|
- **`trace_t`** — ray trace state (origin, direction, hit info, accumulated color)
|
|
- **`rawLightmap_t`** — working lightmap with luxel arrays, floodlight, ambient
|
|
- **`outLightmap_t`** — final packed lightmap page
|
|
- **`sun_t`** / **`skylight_t`** — sun and sky light definitions
|
|
|
|
### Core Tracing Function
|
|
|
|
`LightContributionToSample(trace_t*)` at `light.cpp:777` — the inner loop that calculates one light's contribution to one sample point. Handles all light types, attenuation modes, and shadow rays via `TraceLine()`.
|
|
|
|
### Threading
|
|
|
|
Uses `std::thread` with work-stealing dispatch (`RunThreadsOnIndividual`). All heavy passes (grid, mapping, dirt, illumination, vertex, floodlight, radiosity) run multi-threaded.
|
|
|
|
### Post-Processing Pipeline
|
|
|
|
`ColorToBytes()` at `light_ydnar.cpp:62`: Brightness → Contrast → Gamma → Exposure → Saturation → Clamp
|
|
|
|
---
|
|
|
|
## 8. Proposed Feature Roadmap (Priority Order)
|
|
|
|
### Phase 1: HDR Lightmaps
|
|
|
|
Foundation for everything else. Switch from `byte[3]` to `ColorRGBExp32` or `half[3]` storage. Requires BSP format extension + renderer support.
|
|
|
|
### Phase 2: Radiosity Normal Mapping (Bump-mapped Lightmaps)
|
|
|
|
The biggest visual win. Requires:
|
|
- Computing tangent-space basis vectors per face
|
|
- Tracing 3 directional lightmaps per bumped surface
|
|
- 4x lightmap storage for bumped faces
|
|
- Shader-side recombination with normal maps
|
|
|
|
### Phase 3: True Progressive Refinement Radiosity
|
|
|
|
Replace q3map2's "fake bounce" (spawning point lights at hit surfaces) with proper patch-based energy transfer. More physically correct indirect lighting.
|
|
|
|
### Phase 4: Ambient Cubes / Enhanced Light Grid
|
|
|
|
Upgrade the 3D light grid to store 6-axis directional ambient (one color per cardinal direction) for better dynamic object lighting.
|
|
|
|
### Phase 5: Hard-Falloff Lights / CLQ Attenuation
|
|
|
|
Add smoothstep fade between start/end distances, and full constant/linear/quadratic attenuation control per light.
|
|
|
|
### Phase 6: Static Prop Per-Vertex Lighting
|
|
|
|
Trace light to model vertices for baked model illumination.
|
|
|
|
---
|
|
|
|
## 9. References
|
|
|
|
- [How VRAD Works (gpurad technical document)](https://github.com/x6herbius/gpurad/blob/master/How%20VRAD%20works.md)
|
|
- [Source Lighting Technical Analysis: Part One (Mapcore)](https://www.mapcore.org/articles/development/source-lighting-technical-analysis-part-one-r65/)
|
|
- [Source Lighting Technical Analysis: Part Two (Mapcore)](https://www.mapcore.org/articles/development/source-lighting-technical-analysis-part-two-r66/)
|
|
- [Shading in Valve's Source Engine (SIGGRAPH 2006 PDF)](https://cdn.cloudflare.steamstatic.com/apps/valve/2006/SIGGRAPH06_Course_ShadingInValvesSourceEngine.pdf)
|
|
- [Efficient Self-Shadowed Radiosity Normal Mapping (SIGGRAPH 2007, Chris Green, Valve)](https://cdn.fastly.steamstatic.com/apps/valve/2007/SIGGRAPH2007_EfficientSelfShadowedRadiosityNormalMapping.pdf)
|
|
- [VRAD - Valve Developer Community](https://developer.valvesoftware.com/wiki/VRAD)
|
|
- [HDR Lighting Basics - Valve Developer Community](https://developer.valvesoftware.com/wiki/HDR_Lighting_Basics)
|
|
- [Advanced Lighting - Valve Developer Community](https://developer.valvesoftware.com/wiki/Advanced_Lighting)
|
|
- [BSP (Source) format - Valve Developer Community](https://developer.valvesoftware.com/wiki/BSP_(Source))
|