# 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))
