# Spherical Harmonics Light Grid (SH Grid)

## Overview

The SH light grid is an extension to q3map2's lighting system that stores **L2 spherical harmonics** (9 RGB coefficients per grid point) alongside the legacy Quake 3 light grid. It encodes a smooth directional lighting function over the full sphere at each grid point, enabling dynamic objects to receive accurate directional lighting from all angles -- not just a single dominant direction as in the legacy grid.

The SH grid is stored in a new BSP lump (`LUMP_LIGHTGRID_SH`, index 18) that stock Quake 3 engines ignore, preserving full backward compatibility.

### Why SH over the Legacy Grid?

The legacy Q3 light grid stores per point:
- `ambient` (RGB byte) -- omnidirectional ambient term
- `directed` (RGB byte) -- directional light color
- `latLong` (2 bytes) -- single dominant light direction

The engine evaluates: `result = ambient + directed * max(0, dot(normal, decode(latLong)))`

This collapses all directional information into **one direction**. A point lit by a red light from the left and a blue light from the right produces a muddy average -- a dynamic object turning in place cannot see the color shift.

L2 SH encodes 9 directional basis functions that capture smooth angular variation across the entire sphere. A dynamic object rotating through the grid receives correct color shifts, soft gradients, and directional cues from all light sources.

### Comparison with Source Engine

Source Engine uses **ambient cubes** (6 axis-aligned RGB values, 18 bytes). SH L2 (9 RGB float coefficients) provides strictly higher angular resolution for comparable storage and is the industry standard used by UE4/5, Unity, Godot, and idTech 6/7.

---

## Building Maps with SH Grid

### Basic Usage

```bash
q3map2 -light -sh maps/mymap.bsp
```

The `-sh` flag enables SH grid computation alongside the standard light grid. Both are written to the BSP.

### Command-Line Options

| Flag | Arguments | Default | Description |
|------|-----------|---------|-------------|
| `-sh` | none | off | Enable SH light grid generation |
| `-gridscalesh` | `<float>` | `2.0` | Density multiplier relative to legacy grid. `2.0` means half the cell size = 2x the resolution per axis = 8x total points |
| `-gridsh` | `<X> <Y> <Z>` | derived | Explicit SH grid cell size in world units. Overrides `-gridscalesh` |

### Resolution Control

By default, the SH grid cell size is derived from the legacy grid:

```
gridSizeSH = gridSize / gridScaleSH
```

With default values: legacy grid = `64 64 128`, scale = `2.0`, so SH grid = `32 32 64`.

The minimum cell size is 8 units per axis (hard floor).

**Examples:**

```bash
# Default: 2x density (32x32x64 cells)
q3map2 -light -sh maps/mymap.bsp

# 4x density per axis (16x16x32 cells) -- high quality, large maps may be slow
q3map2 -light -sh -gridscalesh 4 maps/mymap.bsp

# Same density as legacy grid (for debugging/comparison)
q3map2 -light -sh -gridscalesh 1 maps/mymap.bsp

# Explicit cell size
q3map2 -light -sh -gridsh 24 24 48 maps/mymap.bsp
```

### With Radiosity Bounces

The SH grid receives bounced (radiosity) light when `-bouncegrid` is enabled:

```bash
q3map2 -light -sh -bounce 3 -bouncegrid maps/mymap.bsp
```

Each bounce pass re-traces the SH grid with the newly generated diffuse lights.

### With Other Features

The SH grid is independent of and compatible with all existing light features:

```bash
# Full quality build with SH grid, bounces, dirt, and deluxemaps
q3map2 -light -sh -bounce 2 -bouncegrid -dirty -deluxe -fast maps/mymap.bsp
```

### Worldspawn Entity Keys

The legacy grid size can be set per-map via the worldspawn entity:

```
"gridsize" "48 48 96"
```

If `-gridsh` is not explicitly given on the command line, the SH grid cell size is derived from this worldspawn value using the `-gridscalesh` multiplier.

---

## BSP Lump Format

### Lump Index

```
LUMP_LIGHTGRID_SH = 18
```

This extends the IBSP header from 18 to 19 lumps. Stock Q3 engines read lumps 0-17 by index and never access lump 18 -- the BSP remains fully compatible.

### Lump Layout

The lump contains a fixed-size header followed by a flat array of grid points:

```
[bspGridSHHeader_t] [bspGridPointSH_t × numPoints]
```

### Header: `bspGridSHHeader_t` (40 bytes)

```c
struct bspGridSHHeader_t
{
    float gridMins[3];      // World-space origin of the SH grid (12 bytes)
    float gridSize[3];      // Cell size per axis in world units   (12 bytes)
    int   gridBounds[3];    // Number of grid points per axis      (12 bytes)
    int   numPoints;        // Total grid points following header   (4 bytes)
};
```

| Offset | Size | Type | Field | Description |
|--------|------|------|-------|-------------|
| 0 | 12 | float[3] | `gridMins` | World-space position of grid point (0,0,0) |
| 12 | 12 | float[3] | `gridSize` | Cell dimensions in world units (e.g. 32, 32, 64) |
| 24 | 12 | int[3] | `gridBounds` | Grid dimensions in cells (e.g. 64, 48, 16) |
| 36 | 4 | int | `numPoints` | Total number of points. Must equal `gridBounds[0] * gridBounds[1] * gridBounds[2]` |

### Grid Point: `bspGridPointSH_t` (108 bytes)

```c
struct bspGridPointSH_t
{
    float coeffs[9][3];     // 9 SH coefficients, each an RGB triplet (108 bytes)
};
```

Each grid point stores 9 L2 SH coefficients as 32-bit floats, with 3 color channels (RGB) per coefficient. Memory layout is coefficient-major:

```
coeffs[0] = { R, G, B }    // Y0  (l=0, m=0)  -- DC / average
coeffs[1] = { R, G, B }    // Y1  (l=1, m=-1)
coeffs[2] = { R, G, B }    // Y2  (l=1, m=0)
coeffs[3] = { R, G, B }    // Y3  (l=1, m=1)
coeffs[4] = { R, G, B }    // Y4  (l=2, m=-2)
coeffs[5] = { R, G, B }    // Y5  (l=2, m=-1)
coeffs[6] = { R, G, B }    // Y6  (l=2, m=0)
coeffs[7] = { R, G, B }    // Y7  (l=2, m=1)
coeffs[8] = { R, G, B }    // Y8  (l=2, m=2)
```

Total lump size = 40 + (108 * numPoints) bytes.

### Grid Point Indexing

Grid points are stored in a flat array, indexed as:

```
index = x + y * gridBounds[0] + z * gridBounds[0] * gridBounds[1]
```

Where `x`, `y`, `z` are integer grid coordinates in range `[0, gridBounds[i])`.

The world-space position of grid point `(x, y, z)` is:

```
position = gridMins + float3(x, y, z) * gridSize
```

### Byte Order

All values are stored in the native byte order of the platform (little-endian on x86/x64). This matches the existing IBSP convention.

### Empty Lump

If the map was not compiled with `-sh`, the lump has zero length (offset and length both set to 0 in the header). Engines should check `lump.length > sizeof(bspGridSHHeader_t)` before reading.

---

## SH Basis Functions

The implementation uses real-valued, orthonormal L2 spherical harmonics in Cartesian form. Given a normalized direction vector `(x, y, z)`:

| Index | Band | Order | Basis Function | Constant |
|-------|------|-------|---------------|----------|
| 0 | l=0 | m=0 | `1` | 0.282095 |
| 1 | l=1 | m=-1 | `y` | 0.488603 |
| 2 | l=1 | m=0 | `z` | 0.488603 |
| 3 | l=1 | m=1 | `x` | 0.488603 |
| 4 | l=2 | m=-2 | `x*y` | 1.092548 |
| 5 | l=2 | m=-1 | `y*z` | 1.092548 |
| 6 | l=2 | m=0 | `3*z*z - 1` | 0.315392 |
| 7 | l=2 | m=1 | `x*z` | 1.092548 |
| 8 | l=2 | m=2 | `x*x - y*y` | 0.546274 |

Full precision constants:

```c
#define SH_C0  0.282094791773878f   // 1 / (2*sqrt(pi))
#define SH_C1  0.488602511902920f   // sqrt(3) / (2*sqrt(pi))
#define SH_C2  1.092548430592079f   // sqrt(15) / (2*sqrt(pi))
#define SH_C3  0.315391565252520f   // sqrt(5) / (4*sqrt(pi))
#define SH_C4  0.546274215296040f   // sqrt(15) / (4*sqrt(pi))
```

### Coordinate System

The SH basis uses the same coordinate system as the BSP:
- **X** = forward/east
- **Y** = left/north
- **Z** = up

This matches Q3/idTech conventions. If your engine uses a different coordinate system, transform the direction vector before evaluating the SH basis.

---

## Engine Integration Guide

### Step 1: Loading the Lump

```c
// After loading the BSP file into memory:
typedef struct {
    float gridMins[3];
    float gridSize[3];
    int   gridBounds[3];
    int   numPoints;
} shGridHeader_t;

typedef struct {
    float coeffs[9][3];    // [coeff_index][rgb_channel]
} shGridPoint_t;

// Lump 18 in the BSP header
#define LUMP_LIGHTGRID_SH 18

static shGridHeader_t  shHeader;
static shGridPoint_t  *shGridPoints;
static qboolean        shGridAvailable;

void LoadSHGrid(const byte *bspData, const lump_t *lump) {
    if (lump->filelen <= sizeof(shGridHeader_t)) {
        shGridAvailable = qfalse;
        return;
    }

    const byte *data = bspData + lump->fileofs;

    // Read header
    memcpy(&shHeader, data, sizeof(shGridHeader_t));
    data += sizeof(shGridHeader_t);

    // Read points
    shGridPoints = malloc(shHeader.numPoints * sizeof(shGridPoint_t));
    memcpy(shGridPoints, data, shHeader.numPoints * sizeof(shGridPoint_t));

    shGridAvailable = qtrue;
}
```

### Step 2: Trilinear Sampling

Given a world-space position, sample the SH grid with trilinear interpolation:

```c
void SampleSHGrid(const float position[3], float outCoeffs[9][3]) {
    // Convert world position to grid-local floating-point coordinates
    float fx = (position[0] - shHeader.gridMins[0]) / shHeader.gridSize[0];
    float fy = (position[1] - shHeader.gridMins[1]) / shHeader.gridSize[1];
    float fz = (position[2] - shHeader.gridMins[2]) / shHeader.gridSize[2];

    // Integer grid coordinates and fractional parts
    int x0 = (int)floorf(fx);
    int y0 = (int)floorf(fy);
    int z0 = (int)floorf(fz);
    float dx = fx - x0;
    float dy = fy - y0;
    float dz = fz - z0;

    // Clamp to grid bounds
    int x1 = x0 + 1;
    int y1 = y0 + 1;
    int z1 = z0 + 1;
    x0 = CLAMP(x0, 0, shHeader.gridBounds[0] - 1);
    x1 = CLAMP(x1, 0, shHeader.gridBounds[0] - 1);
    y0 = CLAMP(y0, 0, shHeader.gridBounds[1] - 1);
    y1 = CLAMP(y1, 0, shHeader.gridBounds[1] - 1);
    z0 = CLAMP(z0, 0, shHeader.gridBounds[2] - 1);
    z1 = CLAMP(z1, 0, shHeader.gridBounds[2] - 1);

    // Fetch 8 corner grid points
    #define SH_INDEX(X, Y, Z) ((X) + (Y) * shHeader.gridBounds[0] + \
                               (Z) * shHeader.gridBounds[0] * shHeader.gridBounds[1])

    const shGridPoint_t *corners[8] = {
        &shGridPoints[SH_INDEX(x0, y0, z0)],
        &shGridPoints[SH_INDEX(x1, y0, z0)],
        &shGridPoints[SH_INDEX(x0, y1, z0)],
        &shGridPoints[SH_INDEX(x1, y1, z0)],
        &shGridPoints[SH_INDEX(x0, y0, z1)],
        &shGridPoints[SH_INDEX(x1, y0, z1)],
        &shGridPoints[SH_INDEX(x0, y1, z1)],
        &shGridPoints[SH_INDEX(x1, y1, z1)],
    };
    #undef SH_INDEX

    // Trilinear interpolation weights
    float weights[8] = {
        (1-dx) * (1-dy) * (1-dz),
           dx  * (1-dy) * (1-dz),
        (1-dx) *    dy  * (1-dz),
           dx  *    dy  * (1-dz),
        (1-dx) * (1-dy) *    dz,
           dx  * (1-dy) *    dz,
        (1-dx) *    dy  *    dz,
           dx  *    dy  *    dz,
    };

    // Blend SH coefficients
    memset(outCoeffs, 0, 9 * 3 * sizeof(float));
    for (int c = 0; c < 8; c++) {
        for (int i = 0; i < 9; i++) {
            outCoeffs[i][0] += corners[c]->coeffs[i][0] * weights[c];
            outCoeffs[i][1] += corners[c]->coeffs[i][1] * weights[c];
            outCoeffs[i][2] += corners[c]->coeffs[i][2] * weights[c];
        }
    }
}
```

### Step 3: Evaluating Lighting from SH

Given the interpolated SH coefficients and a world-space surface normal, reconstruct the lighting color:

```c
void EvaluateSH(const float coeffs[9][3], const float normal[3], float outColor[3]) {
    float x = normal[0], y = normal[1], z = normal[2];

    // Evaluate SH basis functions
    float basis[9];
    basis[0] = 0.282095f;
    basis[1] = 0.488603f * y;
    basis[2] = 0.488603f * z;
    basis[3] = 0.488603f * x;
    basis[4] = 1.092548f * x * y;
    basis[5] = 1.092548f * y * z;
    basis[6] = 0.315392f * (3.0f * z * z - 1.0f);
    basis[7] = 1.092548f * x * z;
    basis[8] = 0.546274f * (x * x - y * y);

    // Dot product: sum(coeffs[i] * basis[i]) per channel
    outColor[0] = outColor[1] = outColor[2] = 0.0f;
    for (int i = 0; i < 9; i++) {
        outColor[0] += coeffs[i][0] * basis[i];
        outColor[1] += coeffs[i][1] * basis[i];
        outColor[2] += coeffs[i][2] * basis[i];
    }

    // Clamp negative values (SH ringing artifact)
    if (outColor[0] < 0.0f) outColor[0] = 0.0f;
    if (outColor[1] < 0.0f) outColor[1] = 0.0f;
    if (outColor[2] < 0.0f) outColor[2] = 0.0f;
}
```

### Step 4: GLSL Shader (GPU evaluation)

For GPU-side evaluation, pass the interpolated SH coefficients as a uniform or SSBO:

```glsl
// Vertex or fragment shader
// shCoeffs[9] are vec3 uniforms, interpolated per-object or per-vertex

vec3 evaluateSH(vec3 shCoeffs[9], vec3 normal) {
    float x = normal.x, y = normal.y, z = normal.z;

    vec3 color =
        shCoeffs[0] * 0.282095 +
        shCoeffs[1] * (0.488603 * y) +
        shCoeffs[2] * (0.488603 * z) +
        shCoeffs[3] * (0.488603 * x) +
        shCoeffs[4] * (1.092548 * x * y) +
        shCoeffs[5] * (1.092548 * y * z) +
        shCoeffs[6] * (0.315392 * (3.0 * z * z - 1.0)) +
        shCoeffs[7] * (1.092548 * x * z) +
        shCoeffs[8] * (0.546274 * (x * x - y * y));

    return max(color, vec3(0.0));
}
```

For per-vertex lighting, the engine can sample the SH grid per vertex on the CPU and pass per-vertex colors. For per-pixel lighting on dynamic objects, pass the 9 SH coefficients as uniforms (9 * vec3 = 9 * 12 = 108 bytes per object).

### Step 5: Fallback

If the SH lump is not present (stock BSP, or compiled without `-sh`):

```c
if (!shGridAvailable) {
    // Fall back to legacy Q3 light grid (lump 15)
    R_LightGrid_Legacy(position, normal, outColor);
}
```

---

## Memory and Performance

### Storage Costs

| Grid Cell Size | Typical Map (4096x4096x1024) | Points | Legacy Size | SH Size |
|---------------|------------------------------|--------|-------------|---------|
| 64x64x128 (legacy default) | 64 x 64 x 8 | 32,768 | 262 KB | -- |
| 32x32x64 (SH default, 2x) | 128 x 128 x 16 | 262,144 | -- | 27 MB |
| 48x48x96 | 86 x 86 x 11 | 81,356 | -- | 8.4 MB |
| 16x16x32 (4x) | 256 x 256 x 32 | 2,097,152 | -- | 216 MB |

Each SH grid point is **108 bytes** (9 coefficients * 3 floats * 4 bytes). The 2x default is a good balance between quality and size.

For very large maps, consider reducing density with `-gridscalesh 1.5` or using explicit cell sizes.

### Compilation Time

SH grid tracing uses the same `LightContributionToPoint()` function as the legacy grid, tracing all lights per grid point. At 2x density (8x more points), expect roughly 8x the legacy grid computation time. This is typically a small fraction of total light compilation time (lightmap illumination dominates).

### Runtime Performance

SH evaluation is 9 multiply-adds per channel = 27 total operations per grid sample. Trilinear interpolation requires 8 grid lookups. Total cost per dynamic object is comparable to 8 texture fetches + a small ALU workload -- negligible on modern hardware.

---

## JSON Export/Import

The SH grid data is included in the JSON export/import pipeline:

```bash
# Export BSP to JSON (includes GridPointsSH.json if SH data present)
q3map2 -json -unpack maps/mymap.bsp

# Import JSON back to BSP
q3map2 -json -pack maps/mymap.bsp
```

### JSON Format

`GridPointsSH.json`:

```json
{
    "header": {
        "gridMins": [x, y, z],
        "gridSize": [sx, sy, sz],
        "gridBounds": [bx, by, bz]
    },
    "points": [
        [
            [r, g, b],   // coeffs[0] (Y0, DC)
            [r, g, b],   // coeffs[1] (Y1)
            [r, g, b],   // coeffs[2] (Y2)
            [r, g, b],   // coeffs[3] (Y3)
            [r, g, b],   // coeffs[4] (Y4)
            [r, g, b],   // coeffs[5] (Y5)
            [r, g, b],   // coeffs[6] (Y6)
            [r, g, b],   // coeffs[7] (Y7)
            [r, g, b]    // coeffs[8] (Y8)
        ],
        // ... one array of 9 RGB triplets per grid point
    ]
}
```

---

## Source Files

| File | Description |
|------|-------------|
| `q3map2/q3map2/sh.h` | SH math: basis evaluation, projection, reconstruction |
| `q3map2/q3map2/q3map2.h` | `bspGridSHHeader_t`, `bspGridPointSH_t` structs, SH grid globals |
| `q3map2/q3map2/light.cpp` | `SetupGridSH()`, `TraceGridSH()`, CLI parsing, pipeline integration |
| `q3map2/q3map2/bspfile_ibsp.cpp` | BSP lump read/write for `LUMP_LIGHTGRID_SH` |
| `q3map2/q3map2/bspfile_abstract.cpp` | BSP info stats for SH grid |
| `q3map2/q3map2/convert_json.cpp` | JSON export/import for SH grid data |

---

## References

- Peter-Pike Sloan, "Stupid Spherical Harmonics (SH) Tricks", Microsoft, 2008
- Ravi Ramamoorthi and Pat Hanrahan, "An Efficient Representation for Irradiance Environment Maps", SIGGRAPH 2001
- Robin Green, "Spherical Harmonic Lighting: The Gritty Details", GDC 2003
- Quake 3 BSP format specification (IBSP v46/v47)
