Source: https://doc.babylonjs.com/lite/architecture/16-shadow-generator

# Module: Shadow Generator

> Package path: `packages/babylon-lite/src/shadow/`

## Purpose

Implements two shadow mapping techniques for different light types:

1. **Exponential Shadow Mapping (ESM)** with two-pass Gaussian blur for **directional lights** — produces a blurred shadow texture that the main material pass samples. Pipeline per-frame: (1) render shadow casters from the light's perspective into an ESM depth texture, (2) horizontal Gaussian blur, (3) vertical Gaussian blur.

2. **Percentage Closer Filtering (PCF)** for **spot and directional lights** — renders casters into a depth-only texture; the main-pass fragment shader samples with a hardware comparison sampler (5×5 bilinear PCF). No blur passes needed — saves 2 draw calls and 2 GPU textures vs ESM.

Both generators return the same `ShadowGenerator` interface, so the downstream render pipeline is shadow-technique-agnostic. Caster meshes are registered as scene-owned `ShadowTask` inputs, not stored on `ShadowGenerator`. Shadow maps are scheduled by the scene's frame graph: shadow scenes opt into the internal `ShadowTask` by calling `registerSceneWithShadowSupport()`, so ordinary `registerScene()` bundles do not retain shadow scheduling code. PCF and ESM casters use per-source-material Standard/PBR/Node shadow material views, preserving material/mesh vertex features.

### Babylon.js Configuration Equivalence

**ESM generator** matches:

- `useBlurExponentialShadowMap = true`, `useKernelBlur = true`, `blurKernel = 64`
- `mapSize = 1024`, `depthScale = 50`, `bias = 0.00005`

**PCF generator** matches:

- `usePercentageCloserFiltering = true` (SM_PCF / shadow5 quality)
- `mapSize = 512`, `bias = 0.00005`

---

## Public API Surface

### Runtime enablement (`shadow-enabled.ts`)

```typescript
export function setShadowGeneratorEnabled(generator: ShadowGenerator, enabled: boolean): void;
```

Runtime enablement preserves the generator attachment and every receiver bind group. Disabling
writes effective darkness `1` to the existing receiver UBO, returns fully lit before texture
sampling, and skips shadow-map rendering. Re-enabling restores the configured darkness and resumes
the existing generator before the scene pass. The feature state is installed only when this setter
is imported and called.

### Shared Base (`shadow-base.ts`)

```typescript
/** Write shadow generator state into a Float32Array(24) for UBO upload.
 *  Layout: [_lightMatrix(16), _depthValues.x, _depthValues.y, 0, 0, _shadowsInfo(4)] */
export function writeShadowUboFields(out: Float32Array, sg: { _lightMatrix: Float32Array; _depthValues: Float32Array; _shadowsInfo: Float32Array }): void;

export function buildLightViewMatrix(dirX: number, dirY: number, dirZ: number, px: number, py: number, pz: number): Float32Array;
export function multiply4x4(a: Float32Array, b: Float32Array): Float32Array;
export function createShadowParamsUBO(engine: EngineContextInternal, bias: number, depthScale: number): GPUBuffer;
export function createSharedShadowUBO(
    engine: EngineContextInternal,
    _lightMatrix: Float32Array,
    _depthValues: Float32Array,
    _shadowsInfo: Float32Array
): { ubo: GPUBuffer; data: Float32Array };
```

### ShadowTask Inputs (`frame-graph/shadow-inputs.ts`)

```typescript
export interface ShadowTaskInputs {
    readonly casterMeshes: readonly Mesh[];
}

export function setShadowTaskCasterMeshes(shadowGenerator: ShadowGenerator, casterMeshes: readonly Mesh[]): void;

export function setShadowCasterMaxCascade(mesh: Mesh, maxCascade: number): void;
```

Caster meshes are task inputs. Scene setup creates the filter-specific `ShadowGenerator`, assigns it to the light, calls `setShadowTaskCasterMeshes()` to provide the caster list, then calls `registerSceneWithShadowSupport()` instead of `registerScene()` to install the scene-owned shadow task.

`setShadowCasterMaxCascade()` is CSM-specific: it limits a caster to layers `0..maxCascade`, accepts a non-negative integer or `Infinity`, and is applied when a new caster-array instance is supplied. ESM and single-map PCF ignore it.

### Common ShadowGenerator Interface (`shadow-generator.ts`)

```typescript
export interface ShadowGenerator {
    /** Shadow technique: 'esm' (exponential) or 'pcf' (percentage closer filtering). */
    _shadowType: "esm" | "pcf";
    /** The light that owns this shadow generator. */
    _light: LightBase;
    _depthTexture: GPUTexture; // Receiver-facing map: ESM final blurred rgba16float, PCF depth32float
    _depthSampler: GPUSampler; // Receiver-facing sampler: ESM linear sampler, PCF comparison sampler
    _lightMatrix: Float32Array; // 16-element light view-projection
    _shadowsInfo: Float32Array; // ESM: [darkness, 0, depthScale, frustumEdgeFalloff]
    // PCF: [darkness, mapSize, 1/mapSize, 0]
    _depthValues: Float32Array; // Directional PCF/ESM: [0, 1]; spot PCF: [0, far]
    _shadowParamsUBO: GPUBuffer; // Shared shadow parameters UBO
    _shadowUBO: GPUBuffer; // Receiver-side shadow info UBO
    _config: ShadowGeneratorRuntimeConfig; // minimal normalized fields read by shadow task hooks
    /** Monotonically increasing version — bumped each time _lightMatrix changes.
     *  Consumers compare against a stashed version to skip redundant UBO uploads. */
    _version: number;
}
```

### ESM Factory Function (`esm-directional-shadow-generator.ts`)

```typescript
export interface EsmDirectionalShadowGeneratorConfig {
    mapSize?: number; // Default: 1024
    depthScale?: number; // Default: 50
    bias?: number; // Default: 0.00005
    blurScale?: number; // Default: 2
    darkness?: number; // Default: 0
    frustumEdgeFalloff?: number; // Default: 0
    orthoMinZ?: number; // Default: 1  — ortho projection near Z
    orthoMaxZ?: number; // Default: 10000 — ortho projection far Z
    forceRefreshEveryFrame?: boolean; // Default: false — regenerate every frame for GPU-driven/deforming casters
}

export function createEsmDirectionalShadowGenerator(engine: Engine, light: DirectionalLight, cfg?: EsmDirectionalShadowGeneratorConfig): ShadowGenerator;
```

### PCF Spot Factory Function (`pcf-spotlight-shadow-generator.ts`)

```typescript
export interface PcfSpotlightShadowGeneratorConfig {
    mapSize?: number; // Default: 512
    bias?: number; // Default: 0.00005
    darkness?: number; // Default: 0
    normalBias?: number; // Default: 0
    near?: number; // Default: 1 (camera near)
    far?: number; // Default: light.range or 10000
    forceRefreshEveryFrame?: boolean; // Default: false — regenerate every frame for GPU-driven/deforming casters
}

export function createPcfSpotlightShadowGenerator(engine: Engine, light: SpotLight, cfg?: PcfSpotlightShadowGeneratorConfig): ShadowGenerator;
```

### PCF Directional Factory Function (`pcf-directional-shadow-generator.ts`)

```typescript
export interface PcfDirectionalShadowGeneratorConfig {
    mapSize?: number; // Default: 1024
    bias?: number; // Default: 0.00005
    darkness?: number; // Default: 0
    normalBias?: number; // Default: 0
    orthoMinZ?: number; // Default: 1
    orthoMaxZ?: number; // Default: 10000
    forceRefreshEveryFrame?: boolean; // Default: false — regenerate every frame for GPU-driven/deforming casters
}

export function createPcfDirectionalShadowGenerator(engine: Engine, light: DirectionalLight, cfg?: PcfDirectionalShadowGeneratorConfig): ShadowGenerator;
```

### Imports (ESM generator)

```typescript
import type { DirectionalLight } from "../light/directional-light.js";
import type { Engine, EngineInternal } from "../engine/engine.js";
import { getOrCreateSampler } from "../resource/gpu-pool.js";
import { createSharedShadowUBO, createShadowParamsUBO } from "./shadow-base.js";
import blurVertSrc from "../../shaders/shadow-blur.vertex.wgsl?raw";
import blurFragSrc from "../../shaders/shadow-blur.fragment.wgsl?raw";
```

### Imports (PCF generator)

```typescript
import type { SpotLight } from "../light/spot-light.js";
import type { Engine, EngineInternal } from "../engine/engine.js";
import type { ShadowGenerator } from "./shadow-generator.js";
import { createSharedShadowUBO, createShadowParamsUBO } from "./shadow-base.js";
import { ensurePcfShadowTaskState, preloadPcfShadowTaskState, renderPcfShadowMap } from "./pcf-shadow-task-hooks.js";
```

---

## Internal Architecture

### Shadow Base Shared Infrastructure (`shadow-base.ts`)

Shadow generators share math and UBO packing helpers only. Caster ownership lives in `ShadowTaskInputs`:

- **`setShadowTaskCasterMeshes()`** — registers the caster mesh list for a generator in lazy task-owned input state.
- **`setShadowCasterMaxCascade()`** — stores a lazy per-mesh CSM cascade cap; `Infinity` clears it.
- **`writeShadowUboFields()`** — packs a `ShadowGenerator`'s light matrix (16 floats), depth values (2 floats + 2 padding), and shadowsInfo (4 floats) into a 24-float array for downstream UBO upload.
- **`buildLightViewMatrix()` / `multiply4x4()`** — shared light-space matrix math for ESM and PCF task paths.
- **`createShadowParamsUBO()` / `createSharedShadowUBO()`** — shared GPU buffer setup for generator-owned receiver resources.

### Standard/PBR Receiver Code Generation

Standard and PBR prepare receiver code at their existing asynchronous material-build boundaries, then retain a scene-local synchronous fragment factory for pipeline composition and later per-mesh rebuilds:

1. The immutable scene shadow slots are classified before composition.
2. If any slot is CSM, CSM keeps precedence: only CSM slots are forwarded to the registered CSM receiver factory. The fallback loader is not called, preserving the existing unsupported mixed CSM+ESM/PCF behavior.
3. Otherwise the dynamically imported `shadow-fragment-builder.ts` uses `shadow-algorithms.ts` to load only the algorithms present in the slots:
    - ESM-only: builder + `shadow-fragment-esm.ts`
    - PCF-only: builder + `shadow-fragment-pcf.ts`
    - mixed ESM+PCF: builder + both algorithm modules
4. Standard stores the prepared factory in its immutable `StandardShadowContext`; PBR passes its prepared closure into `createPbrComposer`. Synchronous material swaps, geometry views, and pipeline-cache misses therefore never import code.

`shadow-fragment-builder.ts` prepares one-pass Standard/PBR assembly through the shared `shadow-algorithms.ts` loader. Each algorithm module supplies one readonly descriptor containing its immutable texture/sampler binding types plus only two methods: its sampling expression and its WGSL helper body. The builder owns all common binding/emission scaffolding, avoiding mutable append adapters and duplicated descriptor construction. The immutable binding-type objects are reused across slots. For each slot the builder appends varyings, algorithm-specific texture/sampler bindings, the shared shadow-info UBO binding, vertex calculations, sampling calls, and helpers. The immutable `shadowInfo_NUniforms` declaration string is created once per slot and reused in both vertex and fragment helper lists. Returned binding/varying order and emitted WGSL remain byte-identical to the previous three-pass generator.

`shadow-fragment-core.ts` remains the synchronous combined internal compatibility entry and deliberately imports both fallback algorithms. Material wrappers use the prepared loader instead, so the legacy entry cannot pull unused PCF into ESM scenes, unused ESM into PCF scenes, or either fallback into CSM-only scenes. PBR continues remapping the shared builder's `AD` fragment slot to `AS`; Standard retains `AD`.

PCF code generation shares the offset-vector formula between the two axes and generates the nine
tap statements in row-major order (row outer, column inner, each `0..2`). These loops run only
while preparing shader source: the emitted GPU shader remains fully unrolled, byte-identical to
the captured reference, with unchanged multiplication and accumulation order.

`shadow-algorithms.ts` supplies the identical clip-space conversion, UV conversion, and
outside-frustum rejection to both ESM and PCF helpers. The same preparation and sampling
implementations serve Node receivers without importing Standard/PBR fragment assembly.

Node parsing awaits `prepareNodeShadowEmitter` before pipeline compilation. Its scene-local
synchronous emitter retains only the requested algorithms, preserves group-1 bindings,
deduplicated varyings, Node world-position injection, and the `_sf` dispatcher. Shared helper
generation preserves all shader tokens and arithmetic; only Node's former indentation changes.
`node-shadow.ts` is a combined synchronous compatibility entry, not a parser dependency.
Node emission assembles declarations, varying membership, bindings, and dispatch in one pass,
reusing each light's generated shadow-info symbol.

### ESM Generator — GPU Textures

| Label              | Size                | Format         | Usage                                |
| ------------------ | ------------------- | -------------- | ------------------------------------ |
| `shadow-esm`       | mapSize × mapSize   | `rgba16float`  | RENDER_ATTACHMENT \| TEXTURE_BINDING |
| `shadow-depth-buf` | mapSize × mapSize   | `depth32float` | RENDER_ATTACHMENT                    |
| `shadow-blur-h`    | blurSize × blurSize | `rgba16float`  | RENDER_ATTACHMENT \| TEXTURE_BINDING |
| `shadow-blur-v`    | blurSize × blurSize | `rgba16float`  | RENDER_ATTACHMENT \| TEXTURE_BINDING |

Where `blurSize = mapSize / blurScale` (default: 1024 / 2 = 512).

### PCF Generator — GPU Textures

| Label              | Size              | Format         | Usage                                |
| ------------------ | ----------------- | -------------- | ------------------------------------ |
| `shadow-pcf-depth` | mapSize × mapSize | `depth32float` | RENDER_ATTACHMENT \| TEXTURE_BINDING |

No color attachments, no blur textures. The depth texture is directly sampled in the main pass with a comparison sampler.

### Uniform Buffers (ESM)

#### Scene UBO (depthSceneUBO) — 64 bytes

| Offset | Size | Content                                    |
| ------ | ---- | ------------------------------------------ |
| 0      | 64B  | Light view-projection matrix (mat4x4&lt;f32&gt;) |

#### Shadow Params UBO (shadowParamsUBO) — 32 bytes

| Offset (bytes) | Float32 Index | Content                                  |
| -------------- | ------------- | ---------------------------------------- |
| 0              | [0]           | bias (default: 0.00005)                  |
| 4              | [1]           | _(unused)_                               |
| 8              | [2]           | depthScale (default: 50)                 |
| 12             | [3]           | _(unused)_                               |
| 16             | [4]           | depthMinZ = 0 (WebGPU directional light) |
| 20             | [5]           | depthMinZ + depthMaxZ = 1                |
| 24             | [6]           | _(unused)_                               |
| 28             | [7]           | _(unused)_                               |

#### Per-Caster Mesh UBO — 64 bytes

| Offset | Size | Content                    |
| ------ | ---- | -------------------------- |
| 0      | 64B  | World matrix (mat4x4&lt;f32&gt;) |

#### Blur H UBO — 16 bytes

| Offset | Content                               |
| ------ | ------------------------------------- |
| 0–7    | `delta = (1.0 / blurSize, 0)` as vec2 |
| 8–15   | padding (0, 0)                        |

#### Blur V UBO — 16 bytes

| Offset | Content                               |
| ------ | ------------------------------------- |
| 0–7    | `delta = (0, 1.0 / blurSize)` as vec2 |
| 8–15   | padding (0, 0)                        |

### Uniform Buffers (PCF)

#### Scene UBO (depthSceneUBO) — 64 bytes

Same as ESM: light view-projection matrix.

#### Shadow Params UBO — 32 bytes

| Offset (bytes) | Float32 Index | Content                                          |
| -------------- | ------------- | ------------------------------------------------ |
| 0              | [0]           | bias (default: 0.00005)                          |
| 4              | [1]           | _(unused)_                                       |
| 8              | [2]           | 1 / mapSize (texel size, reuses depthScale slot) |
| 12             | [3]           | _(unused)_                                       |
| 16             | [4]           | depthMinZ = 0                                    |
| 20             | [5]           | depthMinZ + depthMaxZ = 1                        |
| 24             | [6]           | _(unused)_                                       |
| 28             | [7]           | _(unused)_                                       |

### shadowsInfo Layout Differences

| Field Index | ESM                | PCF         |
| ----------- | ------------------ | ----------- |
| [0]         | darkness           | darkness    |
| [1]         | 0 (unused)         | mapSize     |
| [2]         | depthScale         | 1 / mapSize |
| [3]         | frustumEdgeFalloff | 0 (unused)  |

### Light View-Projection Matrix Computation

#### Directional shadows: matrix computation (orthographic)

```typescript
function _computeDirectionalLightMatrix(light: DirectionalLight, casterMeshes: Mesh[], orthoMinZ: number, orthoMaxZ: number): { viewProj: Float32Array; near: number; far: number };
```

**Algorithm:**

1. Transform the local direction by the light world matrix and normalize it: `dir = normalize(light.worldMatrix * vec4(light.direction, 0))`
2. Choose up vector: `(0, 1, 0)` unless `|dirY| > 0.99`, then `(0, 0, 1)`
3. Build orthonormal basis:
    - `right = normalize(cross(up, dir))`
    - `up' = cross(dir, right)`
4. Build view matrix (column-major, forward = dir):
    ```
    V = | rx  ux  dirX  0 |
        | ry  uy  dirY  0 |
        | rz  uz  dirZ  0 |
        | -dot(r,P) -dot(u,P) -dot(dir,P) 1 |
    ```
    Where `P = light.worldMatrix[12..14]`
5. Transform all 8 corners of each caster's local AABB (`mesh.boundMin`/`boundMax`, default unit cube) through `worldMatrix` then through `view` → compute X/Y bounds in light space
6. Expand bounds by 10% (`shadowOrthoScale = 0.1`): `lMinX -= (lMaxX - lMinX) * 0.1` etc.
7. Z bounds from `orthoMinZ`/`orthoMaxZ` (camera near/far)
8. Build orthographic projection (column-major, WebGPU z=[0,1]):
    ```
    P[0]  = 2 / (lMaxX - lMinX)
    P[5]  = 2 / (lMaxY - lMinY)
    P[10] = 1 / (far - near)
    P[12] = -(lMaxX + lMinX) / (lMaxX - lMinX)
    P[13] = -(lMaxY + lMinY) / (lMaxY - lMinY)
    P[14] = -near / (far - near)
    P[15] = 1
    ```
9. Multiply: `viewProj = proj * view` (standard 4×4 multiply)

ESM owns its task-facing directional matrix helper as internal `_computeDirectionalLightMatrix()` in `shadow/esm-directional-shadow-generator.ts`. PCF owns the same orthographic helper as internal `_computeDirectionalLightMatrix()` in `shadow/pcf-directional-shadow-generator.ts` so directional shadow generator math remains with the relevant directional resource setup without becoming public API.

#### Spot PCF: `_computeSpotLightMatrix` (perspective)

```typescript
function _computeSpotLightMatrix(light: SpotLight, near: number, far: number): { viewProj: Float32Array; near: number; far: number };
```

**Algorithm:**

1. Transform the local direction by the light world matrix and normalize it: `dir = normalize(light.worldMatrix * vec4(light.direction, 0))`
2. Choose up vector: `(0, 1, 0)` unless `|dirY| > 0.99`, then `(0, 0, 1)`
3. Build orthonormal basis (same as ESM): `right = cross(up, dir)`, `up' = cross(dir, right)`
4. Build view matrix (column-major) from `light.worldMatrix[12..14]`
5. Build **perspective** projection (column-major, WebGPU z=[0,1]):
    - FOV = `light.angle` (full cone angle in radians)
    - Aspect = 1:1 (square shadow map)
    ```
    f = 1 / tan(FOV * 0.5)
    P[0]  = f          // aspect = 1
    P[5]  = f
    P[10] = far / (far - near)
    P[11] = 1          // perspective divide
    P[14] = -(far * near) / (far - near)
    ```
6. Multiply: `viewProj = proj * view`

The spot matrix helper is exported as internal `_computeSpotLightMatrix()` from `shadow/pcf-spotlight-shadow-generator.ts`.

---

## Pipeline Configuration

### ESM Shadow Depth Pipeline

**Vertex buffers:**

| Slot | Stride | Attribute | Location | Format    |
| ---- | ------ | --------- | -------- | --------- |
| 0    | 12B    | position  | 0        | float32x3 |

**Bind group layouts:**

Group 0 — `shadow-depth-scene`:

| Binding | Visibility | Type    | Content                          |
| ------- | ---------- | ------- | -------------------------------- |
| 0       | VERTEX     | uniform | Light view-projection (64 bytes) |

Group 1 — `shadow-depth-mesh`:

| Binding | Visibility      | Type    | Content                  |
| ------- | --------------- | ------- | ------------------------ |
| 0       | VERTEX          | uniform | World matrix (64 bytes)  |
| 1       | VERTEX+FRAGMENT | uniform | Shadow params (32 bytes) |

**Pipeline state:**

- Primitive: `triangle-list`, cull: `back`, front: `ccw`
- Depth/stencil: `depth32float`, write: `true`, compare: `less-equal`
- Color target: `rgba16float` (1 target)
- No multisample

### ESM Blur Pipeline

**Vertex buffers:** None (fullscreen triangle from vertex_index)

**Bind group layout** — `shadow-blur`:

| Binding | Visibility      | Type    | Content                  |
| ------- | --------------- | ------- | ------------------------ |
| 0       | VERTEX+FRAGMENT | uniform | BlurParams (16 bytes)    |
| 1       | FRAGMENT        | texture | Source texture (float)   |
| 2       | FRAGMENT        | sampler | Linear filtering sampler |

**Pipeline state:**

- Primitive: `triangle-list`, cull: `none`
- No depth/stencil
- Color target: `rgba16float` (1 target)

**Blur sampler:**

- `minFilter: 'linear'`, `magFilter: 'linear'`
- `addressModeU: 'clamp-to-edge'`, `addressModeV: 'clamp-to-edge'`

The final vertical blur target is stored as `ShadowGenerator._depthTexture`; receivers bind `sg._depthTexture` and `sg._depthSampler` directly, the same as PCF.

### PCF Shadow Depth Pipeline

PCF depth rendering uses the regular material renderable path with a pass-specific no-color shadow material view. The active material family (Standard, PBR, or Node) supplies vertex buffers, mesh bind groups, and the pipeline; the view only changes render feature bits to select a variant whose fragment stage writes no color.

**Pipeline state:**

- Primitive/culling/vertex layout: inherited from the Standard/PBR material pipeline
- Depth/stencil: `depth32float`, write: `true`, compare: `less-equal`
- Bias handling: `ShadowTask` bakes Babylon-style clip-space linear bias (`bias * 0.5` for WebGPU half-Z depth) into the shadow pass view-projection matrix. PCF does **not** use WebGPU pipeline `depthBias` / `depthBiasSlopeScale`.
- **No color targets** (depth-only pass)
- **Fragment stage retained when needed** with a void entry point so material `discard` / alpha-test logic updates the depth attachment without writing color
- No multisample

**Caster material changes.** The PCF state snapshots each caster's terminal material, its `_csmGen` and its
`_renderFeatures`, and the caster's `_renderableGen`. A new caster array, a switched or re-pointed caster material, a
rebuilt one, a terminal material whose render features were recomputed, or a caster whose own renderable was rebuilt
makes the next ensure build a fresh task (the old one retires behind the frame fence). The features matter when the
terminal material is rebuilt only through meshes outside the caster set, for example an override that is also the
material of a mesh that casts no shadow: that rebuild moves only that mesh's generation, while the caster's view still
holds the features it was built from (alpha testing, culling). Features that were dropped and not recomputed yet do not
count, as for the CSM: a view built then would read none of them. A caster whose view cannot be built yet is left out of that task and kept in `_held`,
using the same `holdShadowCaster` check as the CSM reconcile: the no-colour view factory of its family is not imported
(the registered set is re-supplied, so the generator is parked once while it imports), or the material it casts through
itself has no completed group build in this scene. While the held casters stay held, an ensure returns the existing task;
once a hold lifts, or the change is reverted, the next ensure rebuilds the task with the caster. This applies to the first
build too, since a caster can switch family while the registration preload imports.

### PCF Main-Pass Integration

Standard, PBR, and Node shadow receivers use asynchronously prepared sampling algorithms. Node retains its own group-1 binding and dispatcher assembly. There is no generator-side PCF shader registration path; the generator only exposes receiver-facing texture/sampler/UBO resources.

**Receiver bind group entries for main pass:**

| Binding | Type                 | Content                                                |
| ------- | -------------------- | ------------------------------------------------------ |
| 0       | `texture_depth_2d`   | Shadow depth texture                                   |
| 1       | `sampler_comparison` | Comparison sampler (compare: `less`, linear filtering) |

**Receiver shader fragments:**

- **Declarations:** `@group(2) @binding(0) var shadowTex: texture_depth_2d; @group(2) @binding(1) var shadowCompSampler: sampler_comparison;`
- **Function:** `computeShadowWithPCF(posFromLight, depthMetric, darkness, mapSize, invMapSize) → f32`
- **Call site:** `shadow = computeShadowWithPCF(input.vPositionFromLight, input.vDepthMetric, shadowInfo.shadowsInfo.x, shadowInfo.shadowsInfo.y, shadowInfo.shadowsInfo.z);`

---

## Shader Logic

### ESM Material Shadow View Vertex Path

```wgsl
struct SceneUniforms { viewProjection: mat4x4<f32> };            // @group(0) @binding(0)
struct MeshUniforms  { world: mat4x4<f32> };                     // @group(1) @binding(0)
struct ShadowParams  {
  biasAndScale: vec4<f32>,  // x=bias, y=unused, z=depthScale, w=unused
  depthValues: vec4<f32>,   // Directional x=0,y=1; spot PCF receiver x=0,y=far
};                                                                // @group(1) @binding(1)

struct VertexOutput {
  @builtin(position) clipPos: vec4<f32>,
  @location(0) vDepthMetricSM: f32,
};

@vertex fn main(@location(0) position: vec3<f32>) -> VertexOutput {
  worldPos = mesh.world * vec4(position, 1.0)
  clipPos  = scene.viewProjection * worldPos
  vDepthMetricSM = (clipPos.z + depthValues.x) / depthValues.y + bias
}
```

**Depth metric formula:**

```
vDepthMetricSM = (clipPos.z + 0) / 1 + 0.00005
               = clipPos.z + bias
```

For WebGPU directional lights, `depthValues = (0, 1)`. For spot PCF receiver sampling, `depthValues = (0, far)`, matching Babylon.js `SpotLight.getDepthMinZ()/getDepthMaxZ()` in WebGPU half-Z mode.

### ESM Material Shadow View Fragment Output

The caster material's normal fragment logic runs first, including alpha test, `DiscardBlock`, and clip-plane discard. Like the no-color view, the ESM shadow view drops the shading-only normal-map and spec-gloss features and refraction (see [06-pbr-material.md](https://doc.babylonjs.com/lite/architecture/06-pbr-material#material-views-and-rebuild)). The ESM shadow view then replaces the final color output with the fixed ESM encoding:

```wgsl
@fragment fn main(@location(0) vDepthMetricSM: f32) -> @location(0) vec4<f32> {
  depthSM = clamp(exp(-min(87.0, depthScale * vDepthMetricSM)), 0.0, 1.0)
  return vec4(depthSM, 1.0, 1.0, 1.0)
}
```

**ESM encoding formula:**

```
output.r = clamp(exp(-min(87.0, 50.0 * depth)), 0, 1)
```

The `min(87.0, ...)` prevents float overflow in `exp()`. The result is stored in the red channel. Closer fragments (depth ≈ 0) produce values near 1.0; farther fragments produce values near 0.0.

### Shadow Depth Vertex Shader — PCF

PCF no longer owns a custom position-only depth vertex shader. `ShadowTask` renders casters through Standard/PBR/Node no-color shadow material views, so the normal material vertex path handles mesh features such as morph targets, skeletons, thin instances, UV-dependent alpha tests, and future material vertex extensions. No-color shadow variants emit a void fragment stage when discard logic is required; no color attachment or dummy texture is bound. Bias is baked into the task scene UBO's light-space view-projection matrix; PCF does not use WebGPU pipeline depth-bias state.

### PCF Sampling Function (injected into main pass fragment shader)

```wgsl
fn computeShadowWithPCF(
  posFromLight: vec4<f32>,
  depthMetric: f32,
  darkness: f32,
  mapSize: f32,
  invMapSize: f32,
) -> f32 {
  // Project to shadow UV
  let clipSpace = posFromLight.xyz / posFromLight.w;
  let uv = vec2(0.5 * clipSpace.x + 0.5, 0.5 - 0.5 * clipSpace.y);
  if (uv out of [0,1]) { return 1.0; }  // outside shadow map → fully lit

  let depthRef = clamp(clipSpace.z, 0.0, 1.0);

  // 5×5 optimised bilinear PCF (9 taps via 3×3 grid with fractional offsets)
  var tc = uv * mapSize + 0.5;
  let st = fract(tc);
  let base = (floor(tc) - 0.5) * invMapSize;

  // Bilinear weights from sub-texel position
  let uvw0 = 4.0 - 3.0 * st;
  let uvw1 = vec2(7.0);
  let uvw2 = 1.0 + 3.0 * st;

  // Optimised sample offsets (3 per axis)
  let u = vec3((3.0 - 2.0*st.x)/uvw0.x - 2.0, (3.0+st.x)/uvw1.x, st.x/uvw2.x + 2.0) * invMapSize;
  let v = vec3((3.0 - 2.0*st.y)/uvw0.y - 2.0, (3.0+st.y)/uvw1.y, st.y/uvw2.y + 2.0) * invMapSize;

  // 9 comparison samples (3×3 grid), weight sum = 144
  var sh = 0.0;
  for each (ui, vi) in 3×3 grid:
    sh += uvw_u * uvw_v * textureSampleCompareLevel(shadowTex, shadowCompSampler, base + vec2(u[ui], v[vi]), depthRef);
  sh /= 144.0;

  return mix(darkness, 1.0, sh);
}
```

### Shadow Blur Vertex Shader (`shadow-blur.vertex.wgsl`)

```wgsl
struct BlurParams {
  delta: vec2<f32>,   // texel step direction
  _pad: vec2<f32>,
};                    // @group(0) @binding(0)

@vertex fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
  // Fullscreen triangle (oversized, clipped by rasterizer)
  positions = [(-1, -1), (3, -1), (-1, 3)]
  clipPos = vec4(positions[vertexIndex], 0, 1)
  sampleCenter = p * vec2(0.5, -0.5) + 0.5   // clip → UV
}
```

### Shadow Blur Fragment Shader (`shadow-blur.fragment.wgsl`)

Implements a 33-tap Gaussian blur matching Babylon's `kernelBlur` post-process with `blurKernel = 64`.

**Tap structure:** 26 bilinear taps + 7 dependent taps = 33 total.

**Bilinear tap offsets (26 values, symmetric pairs):**

```
±31.43122487, ±29.43554136, ±27.43986765, ±25.44420309,
±23.44854704, ±21.45289886, ±19.45725789, ±17.46162348,
±15.46599496, ±13.47037167, ±11.47475294, ±9.4791381,
±7.48352647
```

**Bilinear tap weights (26 values, symmetric pairs):**

```
0.00096573, 0.00096573, 0.00164886, 0.00164886,
0.0027182,  0.0027182,  0.00432655, 0.00432655,
0.00664916, 0.00664916, 0.00986638, 0.00986638,
0.01413558, 0.01413558, 0.01955395, 0.01955395,
0.02611683, 0.02611683, 0.03367998, 0.03367998,
0.04193613, 0.04193613, 0.05041622, 0.05041622,
0.05852177, 0.05852177
```

**Dependent tap offsets (7 values, symmetric pairs + center):**

```
-5.48791739, 5.48791739, -3.49231018, 3.49231018,
-1.49670415, 1.49670415, 0.0
```

**Dependent tap weights (7 values):**

```
0.06558884, 0.06558884, 0.0709754, 0.0709754,
0.07415683, 0.07415683, 0.0374872
```

**Fragment algorithm:**

```wgsl
@fragment fn main(@location(0) sampleCenter: vec2<f32>) -> @location(0) vec4<f32> {
  var blend = vec4(0.0);

  // 26 bilinear taps
  for (i in 0..26) {
    coord = sampleCenter + params.delta * OFFSETS[i]
    blend += textureSample(srcTex, srcSampler, coord) * WEIGHTS[i]
  }

  // 7 dependent taps
  for (i in 0..7) {
    coord = sampleCenter + params.delta * DEP_OFFSETS[i]
    blend += textureSample(srcTex, srcSampler, coord) * DEP_WEIGHTS[i]
  }

  return blend;
}
```

---

## State Machine / Lifecycle

### Frame-Graph Scheduling

`registerSceneWithShadowSupport(scene)` installs an internal frame-graph `ShadowTask` before the default swapchain scene render task. Shadow textures are owned by each `ShadowGenerator`; shadow rendering is owned by `ShadowTask`. On every frame, `ShadowTask.execute()` reads `engine._currentEncoder`, loops `scene.lights`, and renders each light's shadow generator. The returned draw counts are summed by `FrameGraph.execute()` together with the scene render task.

ESM generators expose their depth/blur resources to `ShadowTask`. Caster meshes are registered as `ShadowTask` inputs. During `record()`, PCF creates a depth-only `RenderTask` over the generator's depth texture; ESM creates an ESM color+depth `RenderTask` over its task resources. Both paths create one shadow material view per unique caster material.

Each generator state owns an aggregate `_recordedVersion`. `ShadowTask` snapshots `scene._renderableVersion` before recording and publishes that snapshot only after every inner pass records successfully. A partial cascade failure therefore leaves the old value intact for retry, and a scene mutation during recording leaves a deliberately stale snapshot that forces the next frame to record again. Shadow camera facades similarly carry their installed cache version themselves, so PCF, ESM, and CSM updates advance `camera.worldMatrixVersion` directly instead of maintaining a second counter in task state.

The ESM horizontal and vertical blur passes share one internal recording helper. Each still
creates the current target view, clears to transparent black, stores the result, binds the same
blur pipeline and its axis-specific bind group, and draws three vertices. Horizontal output
feeds vertical input; the reported draw count remains the caster count plus two.

### ESM Initialization (once)

1. Create shadow params UBO (bias, depthScale, depthValues)
2. Create 4 GPU textures: ESM target, depth buffer, blur-H target, blur-V target
3. Create blur pipeline, blur UBOs, and blur bind groups
4. Create shared receiver shadow UBO
5. Store ESM-only task resources via the internal resource map
6. Return `ShadowGenerator` with `_shadowType: 'esm'`

Scene setup separately registers caster meshes as `ShadowTask` inputs via `setShadowTaskCasterMeshes()`.

### PCF Initialization

1. Create single depth-only texture (`depth32float`, RENDER_ATTACHMENT + TEXTURE_BINDING)
2. Create comparison sampler (`compare: 'less'`, linear filtering)
3. Create shadow params UBO (bias, texel size, depthValues)
4. Register caster meshes as `ShadowTask` inputs; the task wraps the generator's depth texture in a depth-only render target
5. Return `ShadowGenerator` with `shadowType: 'pcf'`

### Per-Frame Rendering — ESM (`ShadowTask`)

```
Recompute light matrix → update shared receiver shadow UBO if dirty

Pass 1: Shadow Depth
  ├─ Clear esmTexture to (0,0,0,0), depthBuf to 1.0
  ├─ Build/record a material-view RenderTask from ShadowTask caster inputs
  └─ Draw each caster through the material renderable path

Pass 2: Blur Horizontal
  ├─ Clear blurTexH to (0,0,0,0)
  ├─ Set blurPipeline
  ├─ Set group(0) = blurHBG (delta=(1/blurSize, 0), source=esmTexture)
  └─ draw(3)  // fullscreen triangle

Pass 3: Blur Vertical
  ├─ Clear blurTexV to (0,0,0,0)
  ├─ Set blurPipeline
  ├─ Set group(0) = blurVBG (delta=(0, 1/blurSize), source=blurTexH)
  └─ draw(3)  // fullscreen triangle

Returns: casters.length + 2  (depth draws + 2 blur passes)
```

### Per-Frame Rendering — PCF (`ShadowTask`)

```
ShadowTask.record()
  ├─ Create one Standard/PBR/Node no-color shadow MaterialView per unique caster material
  ├─ Create a depth-only RenderTask targeting the generator's depth texture
  └─ Add each caster mesh to the task with its no-color shadow material view

ShadowTask.execute()
  ├─ Recompute spot or directional light matrix if the light/caster state is dirty
  ├─ Update the shared receiver shadow UBO
  └─ Execute the depth-only RenderTask

Pass 1: Shadow Depth (single pass, depth-only)
  ├─ colorAttachments: []  (no color output)
  ├─ Clear depthTexture to 1.0
  ├─ Set group(0) = task scene UBO (biased light viewProjection)
  └─ Draw each caster through the material renderable path

Returns: casters.length  (depth draws only — no blur passes)
```

## Babylon.js Equivalence Map

| Babylon Lite                                                                                      | Babylon.js                                                              |
| ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `createEsmDirectionalShadowGenerator()`                                                           | `new ShadowGenerator(mapSize, light)` with directional ESM config       |
| `createPcfSpotlightShadowGenerator()`                                                             | `new ShadowGenerator(mapSize, light)` with spotlight PCF config         |
| `createPcfDirectionalShadowGenerator()`                                                           | `new ShadowGenerator(mapSize, light)` with directional PCF config       |
| `ShadowGenerator._shadowType === 'esm'`                                                           | `ShadowGenerator.useBlurExponentialShadowMap = true`                    |
| `ShadowGenerator._shadowType === 'pcf'`                                                           | `ShadowGenerator.usePercentageCloserFiltering = true`                   |
| `EsmDirectionalShadowGeneratorConfig.mapSize`                                                     | `ShadowGenerator.mapSize`                                               |
| `EsmDirectionalShadowGeneratorConfig.depthScale`                                                  | `ShadowGenerator.depthScale`                                            |
| `EsmDirectionalShadowGeneratorConfig.bias`                                                        | `ShadowGenerator.bias`                                                  |
| `EsmDirectionalShadowGeneratorConfig.blurScale`                                                   | `ShadowGenerator.blurScale`                                             |
| `EsmDirectionalShadowGeneratorConfig.darkness`                                                    | `ShadowGenerator.darkness`                                              |
| `EsmDirectionalShadowGeneratorConfig.frustumEdgeFalloff`                                          | `ShadowGenerator.frustumEdgeFalloff`                                    |
| `EsmDirectionalShadowGeneratorConfig.forceRefreshEveryFrame`                                      | Force shadow-map rendering for dynamic/deforming casters                |
| `PcfSpotlightShadowGeneratorConfig.normalBias` / `PcfDirectionalShadowGeneratorConfig.normalBias` | `ShadowGenerator.normalBias`                                            |
| ESM encoding (`exp(-depthScale*d)`)                                                               | `ShadowGenerator.useBlurExponentialShadowMap = true`                    |
| 33-tap Gaussian blur (kernel=64)                                                                  | `ShadowGenerator.useKernelBlur = true; .blurKernel = 64`                |
| PCF 5×5 bilinear (9 optimised taps)                                                               | `ShadowGenerator.filteringQuality = SM_PCF`                             |
| `_computeDirectionalLightMatrix()`                                                                | `DirectionalLight._setDefaultAutoExtendShadowProjectionMatrix()`        |
| `_computeSpotLightMatrix()`                                                                       | `SpotLight._setDefaultShadowProjectionMatrix()`                         |
| `shadowOrthoScale = 0.1` (expand)                                                                 | `DirectionalLight.shadowOrthoScale = 0.1`                               |
| Directional `depthValues = [0, 1]`                                                                | `DirectionalLight.getDepthMinZ()=0`, `getDepthMaxZ()=1` (WebGPU half-Z) |
| Spot PCF `depthValues = [0, far]`                                                                 | `SpotLight.getDepthMinZ()=0`, `getDepthMaxZ()=far` (WebGPU half-Z)      |
| `setShadowTaskCasterMeshes()`                                                                     | `ShadowGenerator.addShadowCaster(mesh)` render-list input equivalent    |
| `ShadowTask.execute()`                                                                            | Scene/frame orchestration before receiver rendering                     |
| ESM `ShadowGenerator._depthTexture`                                                               | `ShadowGenerator.getShadowMap()` (after blur passes)                    |
| `_lightMatrix`                                                                                    | `ShadowGenerator.getTransformMatrix()`                                  |
| `writeShadowUboFields()`                                                                          | Internal UBO packing in Babylon's shadow system                         |
| `ShadowGenerator._version`                                                                        | No direct equivalent — Lite uses version for dirty tracking             |

---

## Dependencies

### shadow-base.ts

- `../mesh/mesh.js` — `Mesh`, `MeshInternal` interfaces

### esm-directional-shadow-generator.ts (ESM)

- `../light/directional-light.js` — `DirectionalLight` interface
- `../engine/engine.js` — `Engine`, `EngineInternal`
- `./shadow-base.js` — shared shadow params and receiver UBO helpers
- `../../shaders/shadow-blur.vertex.wgsl` — blur vertex shader (raw import)

### pcf-spotlight-shadow-generator.ts (PCF)

- `../light/spot-light.js` — `SpotLight` interface
- `../engine/engine.js` — `Engine`, `EngineInternal`
- `./shadow-base.js` — shared light matrix, matrix multiply, shadow UBO/params helpers
- `./shadow-generator.js` — `ShadowGenerator` type
- `./pcf-shadow-task-hooks.js` — shared PCF task preload/state/render helpers

### pcf-directional-shadow-generator.ts (Directional PCF)

- `../light/directional-light.js` — `DirectionalLight` interface
- `../mesh/mesh.js` — `Mesh` interface for caster AABB fitting
- `../engine/engine.js` — `Engine`, `EngineInternal`
- `./shadow-base.js` — shared light matrix, matrix multiply, shadow UBO/params helpers
- `./shadow-generator.js` — `ShadowGenerator` type
- `./pcf-shadow-task-hooks.js` — shared PCF task preload/state/render helpers and PCF task state types

### pcf-shadow-task-hooks.ts

- `../frame-graph/render-task.js` — internal material-view render task used for PCF depth passes
- `./shadow-base.js` — shared caster-version, camera, render-target, and receiver UBO helpers
- `./shadow-generator.js` — `ShadowGenerator` and task-state contracts

### frame-graph/shadow-task.ts

- `../engine/engine.js` — `EngineContext`, `EngineContextInternal`
- `../scene/scene-core.js` — `SceneContext`, `SceneContextInternal`
- `./task.js` — `Task`
- `./shadow-inputs.js` — task-owned caster mesh input accessors

---

## Test Specification

1. **ESM light matrix computation** — Given a directional light at direction `(-1, -3, 2)` with caster meshes, verify the computed view-projection matrix produces correct NDC coordinates for known world points.
2. **ESM encoding** — For depth value 0.5 and depthScale 50: `exp(-min(87, 50 * 0.5)) = exp(-25) ≈ 1.389e-11 ≈ 0` (clamped). For depth 0.01: `exp(-0.5) ≈ 0.6065`.
3. **Blur weight normalization** — Sum of all 33 weights (26 bilinear + 7 dependent) should equal ≈ 1.0.
4. **ESM render pass count** — `ShadowTask` should encode exactly 3 ESM render passes; return value = `casters.length + 2`.
5. **PCF render pass count** — `ShadowTask` should execute exactly 1 depth-only render task per dirty PCF shadow map; return value = `casters.length`.
6. **Texture dimensions** — ESM with `mapSize=1024, blurScale=2`: ESM is 1024², blur textures are 512². PCF with `mapSize=512`: depth texture is 512².
7. **UBO sizes** — Scene: 64B, shadow params: 32B, mesh: 64B, blur: 16B.
8. **ESM default config** — Verify: `mapSize=1024, depthScale=50, bias=0.00005, blurScale=2, darkness=0, frustumEdgeFalloff=0, orthoMinZ=1, orthoMaxZ=10000, forceRefreshEveryFrame=false`.
9. **PCF default config** — Verify: `mapSize=512, bias=0.00005, darkness=0, normalBias=0, near=1, far=10000 or light.range, forceRefreshEveryFrame=false`.
10. **PCF spot light matrix** — Verify perspective projection uses `light.angle` as FOV, 1:1 aspect, correct near/far.
11. **PCF shadowsInfo packing** — `[darkness, mapSize, 1/mapSize, 0]`.
12. **Caster dirty tracking** — After mutating a mesh's worldMatrixVersion, `ShadowTask` detects the task-owned caster input versions and re-executes the material-view render task.
    12b. **Forced refresh** — With `forceRefreshEveryFrame=true`, `ShadowTask` re-executes the PCF material-view render task even if light and caster world matrix versions are unchanged, covering morph targets and other GPU-driven deformations.
13. **writeShadowUboFields** — Output Float32Array(24) matches expected layout: [lightMatrix×16, depthValues.x, depthValues.y, 0, 0, shadowsInfo×4].
14. **Receiver output stability** — Empty, ESM-only, PCF-only, mixed, and non-contiguous Standard/PBR slot lists reproduce byte-exact fragment objects, including binding order and PBR `AD` → `AS` remapping.
15. **Receiver feature isolation** — Built Standard/PBR wrappers have no static fallback-core or algorithm import; ESM and PCF helper modules contain only their own WGSL codegen.
16. **Prepared algorithm guard** — An ESM-only prepared factory rejects PCF slots and a PCF-only factory rejects ESM slots rather than silently emitting an invalid receiver.
17. **CSM precedence** — A list containing CSM forwards only CSM light indices to the registered receiver factory and does not prepare fallback algorithms.

---

## File Manifest

| File                                                     | Role                                                                                                                                                                       |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/shadow/shadow-base.ts`                              | Shared shadow math, params UBO, receiver UBO, and `writeShadowUboFields()` helpers                                                                                         |
| `src/shadow/shadow-generator.ts`                         | Shared `ShadowGenerator` contract                                                                                                                                          |
| `src/shadow/esm-directional-shadow-generator.ts`         | Directional ESM generator — shadow params UBO, ESM/depth/blur textures, blur pipeline, receiver UBO, task resource accessors, and directional AABB-fit matrix helper       |
| `src/shadow/pcf-spotlight-shadow-generator.ts`           | Spot PCF shadow generator — factory function, spot light matrix helper, depth texture/comparison sampler ownership, and spot receiver depth values                         |
| `src/shadow/pcf-directional-shadow-generator.ts`         | Directional PCF shadow generator — factory function, directional AABB-fit matrix helper, depth texture/comparison sampler ownership, and directional receiver depth values |
| `src/frame-graph/shadow-inputs.ts`                       | Public caster-mesh input registration for shadow tasks                                                                                                                     |
| `src/frame-graph/shadow-task.ts`                         | Internal frame-graph task that schedules shadows before receiver rendering and renders PCF/ESM casters via Standard/PBR/Node shadow material views                         |
| `src/shadow/pcf-shadow-task-hooks.ts`                    | Shared PCF preload/state/render hooks used by spot and directional PCF generators                                                                                          |
| `src/shader/fragments/shadow-fragment-core.ts`           | Combined synchronous compatibility entry for direct internal callers                                                                                                       |
| `src/shader/fragments/shadow-algorithms.ts`              | Shared algorithm preparation, descriptor types, and projection source for all material families                                                                            |
| `src/shader/fragments/shadow-fragment-builder.ts`        | Algorithm-neutral one-pass Standard/PBR receiver fragment assembly                                                                                                         |
| `src/shader/fragments/shadow-fragment-esm.ts`            | ESM-only receiver bindings, sampling call, and WGSL helpers                                                                                                                |
| `src/shader/fragments/shadow-fragment-pcf.ts`            | PCF-only receiver bindings, sampling call, and WGSL helpers                                                                                                                |
| `src/material/node/node-shadow-emitter.ts`               | Prepared Node group-1 declarations, varyings, bindings, and shadow-factor dispatch                                                                                         |
| `src/material/node/node-shadow.ts`                       | Combined synchronous Node compatibility entry                                                                                                                              |
| `src/material/standard/fragments/std-shadow-fragment.ts` | Standard receiver preparation, CSM precedence, scene-local caching, and fragment ID                                                                                        |
| `src/material/pbr/fragments/pbr-shadow-fragment.ts`      | PBR receiver preparation, CSM precedence, fragment ID, and `AD` → `AS` remapping                                                                                           |
| `shaders/shadow-blur.vertex.wgsl`                        | Blur vertex shader: fullscreen triangle generation + UV computation                                                                                                        |

The spot matrix helper lives in `shadow/pcf-spotlight-shadow-generator.ts`.
