# 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` / `.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 `.rad` files - Format: ` ` - 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))