bloodrun-editor/analysis.md
serge_shubin 89b825ece5 Add L2 spherical harmonics light grid to q3map2
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>
2026-04-03 13:28:28 +08:00

17 KiB

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:
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:

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