Source: https://doc.babylonjs.com/lite/architecture/08-standard-material

# Module: Standard Material (Blinn-Phong)

> Package path: `packages/babylon-lite/src/material/standard/`
> Files: `standard-material.ts` (types), `create-standard-material.ts` (factory), `standard-group-builder.ts` (dynamic imports), `standard-template.ts` (shader template), `standard-pipeline.ts` (pipeline/binding/composed-shader caches), `standard-renderable.ts` (renderable builder and single-mesh rebuild closure), `standard-flags.ts` (feature flags and extension registry), `enable-standard-mesh-features.ts` (published opt-in API), `geometry-view.ts` / `standard-geometry-renderable.ts` / `standard-geometry-output-shader.ts` (geometry-pass reuse), `../geometry-deformation-hooks.ts` / `../geometry-deformation-velocity.ts` (shared opt-in previous-pose state), `no-color-view.ts` (pass-specific material view)

## Purpose

The StandardMaterial module implements a Blinn-Phong shading model with point/directional light support, optional fog (linear, exponential, exponential-squared), optional diffuse texture, optional emissive texture, optional bump/normal-map texture, optional specular texture, optional ambient/occlusion texture, optional lightmap texture, optional opacity/transparency texture, optional reflection texture (spherical and planar modes), UV2 support for select texture channels, thin instances with per-instance color, `disableLighting` mode, and optional ESM/PCF shadow receiving. Explicit opt-ins add Standard-only mesh deformation/state for skeletal skinning and RGBA vertex color. The shared `enableMaterialUvTransform(material)` opt-in enables independent per-texture scale, offset, and rotation for Standard materials without retaining the UV-transform fragment in unrelated bundles. It matches the output of `BABYLON.StandardMaterial` with the corresponding defines active.

Shaders are **dynamically composed** via the `ShaderFragment` / `ShaderComposer` system — no raw `.wgsl` files. A `ShaderTemplate` (`standard-template.ts`) provides the base WGSL with slot markers; optional `ShaderFragment` modules (in `fragments/`) inject code into those slots. Only the fragments needed for a given mesh's features are composed, minimizing bundle size per the Size Pillar. Fragment modules are **dynamically imported** at build time so unused features are tree-shaken. The old `standard-textured-material.ts` was merged into this unified system.

## ShaderFragment Composition System

Standard material shaders are built using the same `ShaderComposer` architecture as PBR (defined in `src/shader/shader-composer.ts`):

1. **`ShaderTemplate`** (`standard-template.ts` → `createStandardTemplate()`) — provides base vertex/fragment WGSL with slot markers (e.g. `/*AC*/`, `/*AD*/`, `/*AT*/`, `/*BC*/`, `/*BA*/`, `/*SV*/`, `/*VB*/`), base UBO fields, base vertex attributes, base varyings, and base bindings for lights/material/diffuse.

2. **`ShaderFragment`** — each optional feature (normal mapping, emissive texture, specular texture, ambient texture, lightmap, opacity, reflection, shadows) is a fragment object with:
    - `id` — unique string identifier
    - `fragmentSlots` / `vertexSlots` — WGSL snippets keyed by slot name
    - `bindings` — `BindingDecl[]` for textures/samplers
    - `varyings` — additional inter-stage varyings (shadows)
    - `helperFunctions` / `vertexHelperFunctions` — WGSL helper code

3. **`composeShader(template, fragments)`** — topologically sorts fragments, merges UBO fields, assigns binding indices, replaces slot markers, and returns a `ComposedShader` with final WGSL + bind group layout descriptors.

### Composition Flow (Standard)

```
standard-group-builder.ts:
  1. Scans meshes for needed features (bump, emissive, specular, etc.)
  2. Dynamically imports fog WGSL only when `scene.fog != null`
  3. Runs only mesh-feature dispatchers installed by explicit Standard enablers
  4. Dynamically imports only needed fragment modules
  5. Passes fragment factories plus the scene shader context to buildStandardMeshRenderables()

standard-renderable.ts (buildStandardMeshRenderables):
  1. Resolves MaterialOrView to source material state + render feature bits
  2. Lets registered mesh extensions contribute skeleton / 8-bone / vertex-color bits
  3. Per mesh: builds fragment list from material, mesh, shadow, morph, and instance features
  4. Calls composeStandardShader(features, meshFeatures, fragments, ..., sceneShader) → ComposedShader
  5. Calls getOrCreateStandardBindings() and getOrCreateStandardPipeline()
```

## Opt-in Standard Mesh Feature Contract

The deformation/vertex feature enablers are explicitly named exports from the root package entry:

```ts
import { enableStandardSkeleton, enableMaterialUvTransform, enableStandardVertexColors } from "@babylonjs/lite";
```

Call each enabler before `registerScene()` when the scene creates matching Standard meshes/material state. Enablers are idempotent and have no import-time registration side effects:

- `enableStandardSkeleton()` installs a mesh predicate and lazy fragment loader. The fragment reuses the material-agnostic shared skeleton shader fragment used by PBR. A matching geometry velocity pass separately loads shared previous-skin/morph state; previous morph weights are applied before previous bones, as specified in [module 31](https://doc.babylonjs.com/lite/architecture/31-geometry-renderer).
- `enableStandardVertexColors()` installs RGBA vertex-color composition and draw-time color-buffer binding.
- `enableMaterialUvTransform(material)` marks a hand-built Standard material for independent texture transforms. Call it before `registerScene()`, then set `uScale`, `vScale`, `uOffset`, `vOffset`, or `uAng` on any bound `Texture2D`. The Standard group builder loads `std-uv-transform-fragment.ts` only when a marked material is present.
- `enableStandardUvOffset()` remains the lightweight shared-material translation path for `material.uvOffset`; absent offsets always resolve to `[0, 0]`.

The UV-transform fragment contributes one vertex-visible uniform buffer and only the varyings required by the material's active texture channels. Its fixed channel order is diffuse, emissive, bump, specular, ambient, lightmap, opacity. Each channel stores a 2x2 matrix plus translation; the matrix applies scale and `uAng` rotation around the UV origin, then translation. UV1 channels compose the existing `material.uvScale` / optional `material.uvOffset` first, while UV2 channels preserve the existing raw-UV2 behavior. `invertY` is folded into the channel transform. Texture transform fields are sampled when the renderable is built; later changes require `rebuildMaterial`.

The enablers register only scalar callbacks and lazy loaders. No module allocates a `Map`/`Set` or registers an extension at import time. If these root exports are unused, production scene bundles omit the deformation/offset chunks; a forward-only skeletal scene also does not load the geometry-velocity chunk.

## `_buildGroup` Pattern

`standard-material.ts` defines `standardGroupBuilder` (not exported), a `MeshGroupBuilder` function that dynamically imports `standard-renderable.js` and the needed fragment modules. This function is set as the `_buildGroup` field on every standard material created by `createStandardMaterial()`. At `startEngine()`, `scene.ts` groups meshes by builder identity so that all standard-material meshes are batched together for a single `buildStandardMeshRenderables()` call.

`standardGroupBuilder` detects which features are needed across all meshes and conditionally imports only the required fragment modules, plus `thin-instance-gpu.ts` when thin instances are present. Fog is a scene shader feature, not a material feature: the builder imports `std-fog-wgsl.ts` only for a fog-enabled scene and passes a `StandardSceneShaderContext` containing `STD_SCENE_FOG` plus the fog `ShaderFragment`. Mesh-feature fragment dispatch exists only after a published enabler installs it. This ensures zero bundle-size impact for unused features. The builder stores the `rebuildSingle` closure returned from `buildStandardMeshRenderables()` on `standardGroupBuilder._rebuildSingle` for material swaps, `rebuildMaterial()`, and per-pass material overrides.

Standard mesh vertex colors use a stricter opt-in seam because automatic color-buffer detection would add a dynamic-import predicate to every Standard scene. `enableStandardVertexColors()` installs the fragment factory before `registerScene()`. When the function is absent from a bundle, `_stdVertexColorFragment` is statically null and all color-buffer branches fold away, keeping non-feature scenes byte-identical.

## Dynamic Feature Flags

| Flag                         | Bit       | Condition                              | Shader effect                                                    |
| ---------------------------- | --------- | -------------------------------------- | ---------------------------------------------------------------- |
| `HAS_DIFFUSE_TEXTURE`        | `1 << 0`  | `material.diffuseTexture`              | Diffuse texture sampling                                         |
| `HAS_EMISSIVE_TEXTURE`       | `1 << 1`  | `material._emissiveTexture`            | Emissive texture sampling                                        |
| `HAS_BUMP_TEXTURE`           | `1 << 2`  | `material._bumpTexture`                | Cotangent-frame normal mapping                                   |
| `HAS_SPECULAR_TEXTURE`       | `1 << 3`  | `material._specularTexture`            | Specular texture replaces specularColor                          |
| `HAS_AMBIENT_TEXTURE`        | `1 << 4`  | `material._ambientTexture`             | Ambient occlusion multiply                                       |
| `HAS_LIGHTMAP_TEXTURE`       | `1 << 5`  | `material._lightmapTexture`            | Additive lightmap                                                |
| `HAS_OPACITY_TEXTURE`        | `1 << 6`  | `material._opacityTexture`             | Alpha/opacity texture                                            |
| `LIGHTMAP_USES_UV2`          | `1 << 7`  | Lightmap on UV2                        | UV2 attribute for lightmap                                       |
| `AMBIENT_USES_UV2`           | `1 << 8`  | Ambient on UV2                         | UV2 attribute for ambient                                        |
| `DOUBLE_SIDED`               | `1 << 9`  | `!material.backFaceCulling`            | `cullMode: 'none'`                                               |
| `DIFFUSE_USES_UV2`           | `1 << 10` | Diffuse on UV2                         | UV2 attribute for diffuse                                        |
| `SPECULAR_USES_UV2`          | `1 << 11` | Specular on UV2                        | UV2 attribute for specular                                       |
| `OPACITY_FROM_RGB`           | `1 << 12` | `material.opacityFromRGB`              | Opacity from RGB luminance                                       |
| `HAS_REFLECTION_TEXTURE`     | `1 << 13` | `material._reflectionTexture`          | Spherical/planar reflection                                      |
| `DISABLE_LIGHTING`           | `1 << 14` | `material.disableLighting`             | Skip light loop, emissive-only output                            |
| `MATERIAL_ALPHA_BLEND`       | `1 << 16` | `material.alpha < 1`                   | Alpha blend pipeline state                                       |
| `HAS_CUBE_REFLECTION`        | `1 << 17` | `material._reflectionCubeTexture`      | Cube reflection sampling                                         |
| `NO_COLOR_OUTPUT`            | `1 << 18` | No-color material view                 | Fragment stage runs discard/alpha-test logic and writes no color |
| `HAS_DEPTH_EMISSIVE_TEXTURE` | `1 << 19` | Emissive texture has depth sample type | Depth texture emissive preview                                   |
| `HAS_VERTEX_COLOR`           | `1 << 23` | Enabler active + mesh color buffer     | RGBA vertex color multiplies base color and alpha                |
| `HAS_SKELETON`               | `1 << 26` | Enabler active + mesh skeleton         | Bone-texture skinning of position and normal                     |
| `HAS_SKELETON_8`             | `1 << 27` | Skeleton has JOINTS_1 / WEIGHTS_1      | Adds the second four bone influences                             |
| `NEEDS_UV`                   | derived   | Any texture present                    | UV vertex attribute                                              |
| `NEEDS_UV2`                  | derived   | Any `*_USES_UV2` flag                  | UV2 vertex attribute                                             |

Thin-instance and shadow-receiver state are mesh feature bits in `material/mesh-features.ts`, separate from Standard material render features.

Vertex colors use `MSH_HAS_VERTEX_COLOR` plus the installed `_stdVertexColorFragment` factory rather than a Standard material bit. Once enabled via `enableStandardVertexColors()`, every Standard mesh with `mesh._gpu.colorBuffer` receives the vertex-color shader variant and buffer binding. RGB always multiplies the base color; the alpha channel is consumed (and folded into the alpha-test) only when the mesh opts in via `mesh.hasVertexAlpha` (Babylon `VERTEXALPHA`), which also sets `VERTEX_ALPHA | MATERIAL_ALPHA_BLEND` so the mesh source-over blends and sorts into the transparent phase. The dynamically loaded thin-instance fragment declares when per-instance RGBA is present, allowing the same `hasVertexAlpha` opt-in to set `MATERIAL_ALPHA_BLEND` without requiring a mesh vertex-color buffer. It deliberately does not set `VERTEX_ALPHA`, avoiding a redundant vertex-colour shader/cache variant when no such buffer or fragment exists. Standard geometry output remains vertex-color-only because thin-instance colors do not participate in that path's alpha classification.

`STD_SCENE_FOG` is a separate scene-feature bit. It is included in Standard binding/composed-shader cache keys but is never stored on a material.

Bindings and composed shaders are cached per `(features, meshFeatures, sceneFeatures, shadowVariant, stencilVariant)`. Pipelines are nested under those bindings and cached per complete render-target signature. A fog and non-fog scene sharing one `GPUDevice` therefore cannot share a composed shader, bind-group layout object, or pipeline even when all material and mesh feature bits match.

## Public API Surface

### Types (`standard-material.ts`)

```typescript
import type { MeshGroupBuilder } from "../../render/renderable.js";

/** StandardMaterial properties — plain data. */
export interface StandardMaterialProps extends Material {
    diffuseColor: [number, number, number];
    alpha: number;
    specularColor: [number, number, number];
    specularPower: number;
    emissiveColor: [number, number, number];
    ambientColor: [number, number, number];
    diffuseTexture: Texture2D | null;
    diffuseCoordIndex: 0 | 1;
    /** @internal Set via `setStandardEmissiveTexture()`. */
    _emissiveTexture?: Texture2D | null;
    /** @internal Set via `setStandardBumpTexture()`. */
    _bumpTexture?: Texture2D | null;
    bumpLevel: number;
    /** @internal Set via `setStandardSpecularTexture()`. */
    _specularTexture?: Texture2D | null;
    specularCoordIndex: 0 | 1;
    /** @internal Set via `setStandardAmbientTexture()`. */
    _ambientTexture?: Texture2D | null;
    ambientTexLevel: number;
    ambientCoordIndex: 0 | 1;
    /** @internal Set via `setStandardLightmapTexture()`. */
    _lightmapTexture?: Texture2D | null;
    lightmapLevel: number;
    lightmapCoordIndex: 0 | 1;
    /** @internal Set via `setStandardOpacityTexture()`. */
    _opacityTexture?: Texture2D | null;
    opacityLevel: number;
    opacityFromRGB: boolean;
    alphaCutOff: number;
    /** @internal Set via `setStandardReflectionTexture()`. */
    _reflectionTexture?: Texture2D | null;
    /** @internal Set via `setStandardReflectionCubeTexture()`. */
    _reflectionCubeTexture?: CubeTexture | null;
    reflectionLevel: number;
    reflectionCoordMode: 1 | 2;
    uvScale: [number, number];
    uvOffset?: [number, number];
    /** @internal True when enableMaterialUvTransform() enabled per-texture transforms. */
    _hasUvTx?: boolean;
    backFaceCulling: boolean;
    disableLighting: boolean;
}

/** Fog configuration — plain data. */
export interface FogConfig {
    mode: 0 | 1 | 2 | 3; // 0=off, 1=exp, 2=exp2, 3=linear
    density: number;
    start: number;
    end: number;
    color: [number, number, number];
}

/** Create StandardMaterial with Babylon defaults. Sets _buildGroup to standardGroupBuilder. */
export function createStandardMaterial(): StandardMaterialProps;

/** Collect all non-null textures for acquire/release tracking. */
export function collectStdBoundTextures(mat: StandardMaterialProps): Texture2D[];

/** Create a pass-specific no-color material view over a Standard source material. */
export function createStandardNoColorMaterialView(source: StandardMaterialProps): MaterialView;
```

### Vertex Colors (`enable-standard-vertex-colors.ts`)

```typescript
import { enableStandardVertexColors } from "babylon-lite";

/**
 * Enable RGBA mesh vertex colors for StandardMaterial.
 * Call once before registerScene().
 */
export function enableStandardVertexColors(): void;
```

The color buffer contract is four `f32` values per vertex. RGB always multiplies the Standard base color. Alpha is consumed only when the mesh opts in via `mesh.hasVertexAlpha` (Babylon `VERTEXALPHA`): the fragment then multiplies the running material alpha by `vColor.a`, folds `vColor.a` into the alpha-test cutoff, and the mesh source-over blends (depth-write off, transparent sort phase). Without the opt-in the vertex color is RGB-only. The same fragment participates in main-color, no-color/shadow, and Standard geometry-view shader composition. Per-thin-instance RGBA follows the same `hasVertexAlpha` opt-in for Standard forward transparency. Its lazy fragment marks the color as an alpha source, so the forward builder selects the transparent pipeline without enabling or binding the mesh vertex-color fragment; Standard geometry-view classification remains vertex-color-only.

### Optional texture setters

```typescript
export function setStandardEmissiveTexture(mat: StandardMaterialProps, texture: Texture2D | null): void;
export function setStandardBumpTexture(mat: StandardMaterialProps, texture: Texture2D | null): void;
export function setStandardSpecularTexture(mat: StandardMaterialProps, texture: Texture2D | null): void;
export function setStandardAmbientTexture(mat: StandardMaterialProps, texture: Texture2D | null): void;
export function setStandardLightmapTexture(mat: StandardMaterialProps, texture: Texture2D | null): void;
export function setStandardOpacityTexture(mat: StandardMaterialProps, texture: Texture2D | null): void;
export function setStandardReflectionTexture(mat: StandardMaterialProps, texture: Texture2D | null): void;
export function setStandardReflectionCubeTexture(mat: StandardMaterialProps, texture: CubeTexture | null): void;
```

Each setter lives in its own module and does exactly two things: stamp the `@internal` backing field and register its `StdExt`. **One setter per module is load-bearing** — a shared module's static imports would pull all eight shader fragments into every consumer and defeat the whole design.

Registration is synchronous and happens at call time; feature detection runs later, when the renderable is built, via the ext's `_detect(mat)`. Companion scalars (`bumpLevel`, `lightmapCoordIndex`, `opacityFromRGB`, …) can therefore be assigned in any order relative to the setter. Setting a texture after the material has already been built still requires `rebuildMaterial()`.

The backing fields are `@internal` (underscore-prefixed) so that a direct assignment — which would skip registration and silently render nothing — is a compile error. **The setter is the boundary:** every write to a backing field must go through it.

`_computeStandardMaterialFeatures` keeps only the always-present core bits (`HAS_DIFFUSE_TEXTURE`, `DIFFUSE_USES_UV2`, `DOUBLE_SIDED`, `DISABLE_LIGHTING`, `MATERIAL_ALPHA_BLEND`); every optional bit — including sub-bits such as `LIGHTMAP_USES_UV2`, `SPECULAR_USES_UV2`, `OPACITY_FROM_RGB`, and `HAS_DEPTH_EMISSIVE_TEXTURE` — is contributed by the owning extension's `_detect`. This mirrors `PbrExt.detect`.

`loadBabylon()` maps `.babylon` texture slots to these setters through per-slot dynamic imports, so a `.babylon` scene retains only the fragments its file actually references.

### Opt-in root exports

```typescript
export function enableStandardSkeleton(): void;
export function enableStandardUvOffset(): void;
export function enableMaterialUvTransform(material: PbrMaterialProps | StandardMaterialProps): boolean;
```

Skeletal skinning, UV translation, and RGBA vertex-color opt-ins are explicitly re-exported from the root `index.ts`. The source and emitted packages expose only the `"."` export; unused enablers and their lazy feature chunks are removed by tree-shaking.

### Pipeline (`standard-pipeline.ts`)

```typescript
// Feature flags (see Dynamic Feature Flags table above for full list)
export const HAS_DIFFUSE_TEXTURE = 1 << 0;
// ... (all flags as documented)

export function _computeStandardMaterialFeatures(mat: StandardMaterialProps): number;
export function getOrCreateStandardBindings(
    engine: EngineContextInternal,
    features: number,
    meshFeatures: number,
    fragments?: ShaderFragment[],
    shaderKey?: string,
    esmShadowDepthCode?: string,
    stencil?: StencilState | null,
    sceneShader?: StandardSceneShaderContext | null
): StandardShaderBindings;

export function getOrCreateStandardPipeline(engine: EngineContextInternal, sig: RenderTargetSignature, bindings: StandardShaderBindings): GPURenderPipeline;

export function clearStandardPipelineCache(): void;
export function releaseStandardPipelineVariant(variant: PipelineVariant): void;

// Re-exports from lights-ubo
export { LIGHTS_UBO_SIZE, getLightsUboSize, writeLightsUBO, refreshLightsUBO };
```

### Template (`standard-template.ts`)

```typescript
/** Configuration for standard shader template generation. */
export interface StandardTemplateConfig {
    _diffuse?: boolean;
    _needsUV: boolean;
    _needsUV2: boolean;
    _diffuseUsesUV2?: boolean;
    _disableLighting?: boolean;
    _noColorOutput?: boolean;
    _esmShadowOutput?: boolean;
    _hasMorph?: boolean;
}

/** Create a ShaderTemplate from standard material configuration. */
export function createStandardTemplate(config: StandardTemplateConfig, esmShadowDepthCode?: string): ShaderTemplate;
```

`StandardSceneShaderContext` is internal pure state:

```typescript
export interface StandardSceneShaderContext {
    readonly _features: number;
    readonly _fragments: readonly ShaderFragment[];
}
```

### Renderable (`standard-renderable.ts`)

```typescript
/** Fragment factories passed from standardGroupBuilder. */
export interface StdFragmentFactories {
    tiSync?: ThinInstanceSync;
    tiFragment?: ShaderFragment;
    bumpFragment?: ShaderFragment;
    shadowFragment?: (shadowLights: ShadowLightSlot[]) => ShaderFragment;
    emissiveFragment?: ShaderFragment;
    specularFragment?: (usesUV2: boolean) => ShaderFragment;
    ambientFragment?: (usesUV2: boolean) => ShaderFragment;
    lightmapFragment?: (usesUV2: boolean) => ShaderFragment;
    opacityFragment?: (fromRGB: boolean) => ShaderFragment;
    reflectionFragment?: ShaderFragment;
}

export function buildStandardMeshRenderables(scene: SceneContext, meshes: Mesh[], factories: StdFragmentFactories): MeshGroupBuildResult;
```

### Material Views and Rebuild

Standard renderables accept `MaterialOrView`. A plain material computes/stores `_renderFeatures = { features: _computeStandardMaterialFeatures(mat) }`. A view uses `view._renderFeatures` exactly while reading all uniform/texture state from `view.source`.

`createStandardNoColorMaterialView(source)` creates a view that ORs `NO_COLOR_OUTPUT` into the source material feature bits. This produces a Standard shader variant that runs discard/alpha-test code and writes no color, useful for passes that should execute the fragment stage without writing color.

The `rebuildSingle` closure returned from `buildStandardMeshRenderables()` is installed as `r` on the scene-local group and also cached on `standardGroupBuilder._rebuildSingle`. Material swaps, `rebuildMaterial()`, and per-pass overrides resolve through the scene-local group, never another scene's builder cache. Standard rebuilds require the passed scene's initialized `_standardRebuildContext`; they reject a missing context rather than falling back to the closure's original scene.

### Default Material Values

| Property                 | Default     |
| ------------------------ | ----------- |
| `diffuseColor`           | `[1, 1, 1]` |
| `alpha`                  | `1`         |
| `specularColor`          | `[1, 1, 1]` |
| `specularPower`          | `64`        |
| `emissiveColor`          | `[0, 0, 0]` |
| `ambientColor`           | `[0, 0, 0]` |
| `diffuseTexture`         | `null`      |
| `diffuseCoordIndex`      | `0`         |
| `bumpLevel`              | `1`         |
| `specularCoordIndex`     | `0`         |
| `ambientTexLevel`        | `1`         |
| `ambientCoordIndex`      | `0`         |
| `lightmapLevel`          | `1`         |
| `lightmapCoordIndex`     | `1`         |
| `useLightmapAsShadowmap` | `false`     |
| `opacityLevel`           | `1`         |
| `opacityFromRGB`         | `false`     |
| `alphaCutOff`            | `0`         |
| `reflectionLevel`        | `1`         |
| `reflectionCoordMode`    | `1`         |
| `uvScale`                | `[1, 1]`    |
| `backFaceCulling`        | `true`      |
| `disableLighting`        | `false`     |

The eight optional texture fields have **no default** — they are absent (`undefined`) until the corresponding `setStandardXTexture()` runs. Omitting the `null` initializers is what lets a scene that never imports a setter drop the field, its extension, and its shader fragment entirely.

## Pipeline Configuration

### Vertex Buffers (varies by features)

**Base (always present):**

| Slot | Attribute | Format      | Stride   | Shader Location |
| ---- | --------- | ----------- | -------- | --------------- |
| 0    | Position  | `float32x3` | 12 bytes | `@location(0)`  |
| 1    | Normal    | `float32x3` | 12 bytes | `@location(1)`  |

**Conditional (appended in order, slot numbers shift dynamically):**

| Attribute        | Format                   | Stride                    | Step Mode  | Shader Location(s)             | When                                           |
| ---------------- | ------------------------ | ------------------------- | ---------- | ------------------------------ | ---------------------------------------------- |
| UV               | `float32x2`              | 8 bytes                   | `vertex`   | `@location(2)`                 | `NEEDS_UV`                                     |
| UV2              | `float32x2`              | 8 bytes                   | `vertex`   | `@location(3)`                 | `NEEDS_UV2`                                    |
| Vertex color     | `float32x4`              | 16 bytes                  | `vertex`   | next free location             | Vertex colors enabled and color buffer present |
| Joints/weights   | `uint32x4` + `float32x4` | 32 bytes across 2 buffers | `vertex`   | next 2 locations               | `HAS_SKELETON`                                 |
| Joints1/weights1 | `uint32x4` + `float32x4` | 32 bytes across 2 buffers | `vertex`   | next 2 locations               | `HAS_SKELETON_8`                               |
| Instance matrix  | 4× `float32x4`           | 64 bytes                  | `instance` | `@location(N)..@location(N+3)` | `THIN_INSTANCES`                               |
| Instance color   | `float32x4`              | 16 bytes                  | `instance` | `@location(N+4)`               | `THIN_INSTANCE_COLOR`                          |

### Pipeline State

| Setting       | Value                                |
| ------------- | ------------------------------------ |
| Topology      | `triangle-list`                      |
| Cull mode     | `back` (or `none` if `DOUBLE_SIDED`) |
| Front face    | `ccw`                                |
| Depth format  | `depth24plus-stencil8`               |
| Depth compare | `greater-equal`                      |
| Depth write   | `true`                               |
| MSAA          | `count = msaaSamples`                |
| Color target  | Canvas preferred format, no blend    |

### Bind Group Layouts

**Group 0 — Scene**:

| Binding | Visibility         | Type                                            |
| ------- | ------------------ | ----------------------------------------------- |
| 0       | VERTEX \| FRAGMENT | Uniform buffer (canonical Scene UBO, 352 bytes) |
| 1       | FRAGMENT           | Uniform buffer (scene-owned `LightsUniforms`)   |

**Group 1 — Per-Mesh** (dynamic bindings based on features):

| Binding | Visibility         | Type                  | Resource                                                                   | When                                                            |
| ------- | ------------------ | --------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------- |
| 0       | VERTEX \| FRAGMENT | Uniform buffer        | Mesh UBO (`world` + per-mesh light selection)                              | Always                                                          |
| 1       | FRAGMENT           | Uniform buffer        | Material UBO (96B)                                                         | Always                                                          |
| 2       | FRAGMENT           | texture_2d            | Diffuse texture                                                            | HAS_DIFFUSE_TEXTURE                                             |
| 3       | FRAGMENT           | sampler               | Diffuse sampler                                                            | HAS_DIFFUSE_TEXTURE                                             |
| 4       | VERTEX+FRAGMENT    | Uniform buffer        | Shadow UBO (96B) or UV UBO (16B)                                           | RECEIVE_SHADOWS or NEEDS_UV                                     |
| next    | FRAGMENT           | texture/sampler pairs | Emissive, bump, specular, ambient, lightmap, opacity, reflection resources | Feature-dependent, assigned sequentially by the shader composer |
| next    | VERTEX             | texture_2d            | Skeleton bone matrix texture                                               | `HAS_SKELETON`                                                  |

**Group 2 — Shadow Map** (only when RECEIVE_SHADOWS):

| Binding | Visibility | Type       | Resource           |
| ------- | ---------- | ---------- | ------------------ |
| 0       | FRAGMENT   | texture_2d | Shadow map texture |
| 1       | FRAGMENT   | sampler    | Shadow map sampler |

## Internal Architecture

### Uniform Buffer Layouts

#### Scene UBO (Group 0, Binding 0) — 352 bytes (canonical `SceneUniforms`)

| Offset (bytes) | Floats | WGSL Type             | Field                                           |
| -------------- | ------ | --------------------- | ----------------------------------------------- |
| 0              | 0–15   | `mat4x4<f32>`         | `viewProjection`                                |
| 64             | 16–31  | `mat4x4<f32>`         | `view`                                          |
| 128            | 32–35  | `vec4<f32>`           | `vEyePosition` (xyz + pad)                      |
| 144            | 36–39  | Scalars/padding       | environment rotation/padding                    |
| 160–303        | 40–75  | 9 × SH vec3 + padding | environment irradiance                          |
| 304            | 76–79  | Scalars/padding       | exposure, contrast, LOD scale                   |
| 320            | 80–83  | `vec4<f32>`           | `vFogInfos` (x=mode, y=start, z=end, w=density) |
| 336            | 84–87  | `vec4<f32>`           | `vFogColor` (rgb + pad)                         |

#### Mesh UBO (Group 1, Binding 0)

| Offset | WGSL Type                                | Field                                              |
| ------ | ---------------------------------------- | -------------------------------------------------- |
| 0      | `mat4x4<f32>`                            | `world`                                            |
| 64     | `u32`                                    | `lc`                                               |
| 80..   | `array<vec4<u32>, ceil(MAX_LIGHTS / 4)>` | packed light indices into group-0 `LightsUniforms` |

#### Lights UBO (Group 0, Binding 1) — 16-byte header + `MAX_LIGHTS × 64` bytes

| Offset (bytes) | Type            | Field                                                                      |
| -------------- | --------------- | -------------------------------------------------------------------------- |
| 0–15           | `u32 + padding` | `count` header                                                             |
| 16 + N×64 + 0  | `vec4<f32>`     | `vLightData` — xyz=position/dir, w=type                                    |
| 16 + N×64 + 16 | `vec4<f32>`     | `vLightDiffuse` — rgb=diffuse×intensity, a=range                           |
| 16 + N×64 + 32 | `vec4<f32>`     | `vLightSpecular` — rgb=specular×intensity, a=spot exponent for spot lights |
| 16 + N×64 + 48 | `vec4<f32>`     | `vLightDirection` — direction/cos half-angle for spot lights               |

#### Material UBO (Group 1, Binding 1) — 96 bytes (24 floats)

| Offset (bytes) | Type        | Field                                            |
| -------------- | ----------- | ------------------------------------------------ |
| 0–15           | `vec4<f32>` | `vDiffuseColor` — rgb=diffuse, a=alpha           |
| 16–31          | `vec4<f32>` | `vSpecularColor` — rgb=specular, a=specularPower |
| 32–43          | `vec3<f32>` | `vEmissiveColor`                                 |
| 44–47          | `f32`       | `bumpScale` (1.0 / bumpLevel)                    |
| 48–59          | `vec3<f32>` | `vAmbientColor`                                  |
| 60–63          | `f32`       | `textureLevel` (1.0 when NEEDS_UV)               |
| 64–67          | `f32`       | `ambientTexLevel`                                |
| 68–71          | `f32`       | `lightmapLevel`                                  |
| 72–75          | `f32`       | `opacityLevel`                                   |
| 76–79          | `f32`       | `alphaCutOff`                                    |
| 80–83          | `f32`       | `reflectionLevel`                                |
| 84–87          | `f32`       | `reflectionCoordMode` (1=spherical, 2=planar)    |
| 88–95          | 2× `f32`    | padding                                          |

#### Shadow UBO (Group 1, Binding 5) — 96 bytes (if RECEIVE_SHADOWS)

| Offset (bytes) | Type          | Field                                                      |
| -------------- | ------------- | ---------------------------------------------------------- |
| 0–63           | `mat4x4<f32>` | `lightMatrix`                                              |
| 64–79          | `vec4<f32>`   | `depthValues` (x=near, y=far)                              |
| 80–95          | `vec4<f32>`   | `uvScaleOffset` (x=uScale, y=vScale, z=uOffset, w=vOffset) |

#### UV UBO (Group 1, Binding 5) — 16 bytes (if NEEDS_UV without shadow)

| Offset (bytes) | Type        | Field                                                                                                                                                            |
| -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0–15           | `vec4<f32>` | `uvScaleOffset` (x=uScale, y=vScale, z=uOffset, w=vOffset); offset defaults to `[0, 0]` even when the opt-in is globally enabled but a material omits `uvOffset` |

### Shader Template (`standard-template.ts`)

`createStandardTemplate(config, esmShadowDepthCode?)` builds a `ShaderTemplate` with slot markers for fragment injection. The template provides:

**Always-present WGSL blocks (embedded in template):**

| Block         | Contents                                                                                                              | Included when                              |
| ------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `LIGHTING_FN` | `computeLighting()` — Blinn-Phong over the mesh-selected subset of the scene-wide `MAX_LIGHTS` lights, shadow factors | Not `DISABLE_LIGHTING`                     |
| `FOG_FN`      | `calcFogFactor()` — linear/exp/exp2 from dynamically imported Standard fog WGSL                                       | `STD_SCENE_FOG` and a color-producing pass |

**Template slot markers** (injected by `ShaderComposer`):

| Slot     | Stage    | Purpose                                              |
| -------- | -------- | ---------------------------------------------------- |
| `/*AC*/` | Fragment | Normal perturbation (bump map)                       |
| `/*AD*/` | Fragment | Ambient/shadow/reflection contributions              |
| `/*AT*/` | Fragment | Emissive/specular/opacity texture sampling           |
| `/*BC*/` | Fragment | Post-lighting composition (lightmap, instance color) |
| `/*BA*/` | Fragment | Final alpha adjustments                              |
| `/*SV*/` | Fragment | Variable initialization                              |
| `/*VB*/` | Vertex   | Shadow light-space transforms                        |
| `/*VR*/` | Vertex   | Pre-transform modifications                          |
| `/*VW*/` | Vertex   | World matrix override (skinning)                     |

**`disableLighting` path:** When `DISABLE_LIGHTING` is set, the template omits the lighting function, light loop, shadow factors, ambient, reflection, and lightmap. Output becomes `clamp(emissiveContrib * diffuseColor, 0, 1) * baseColor`.

**Per-instance color:** When `THIN_INSTANCE_COLOR` is set, a `vInstanceColor` varying passes from vertex to fragment. Applied after main composition in the `BC` slot as `color.rgb *= vInstanceColor.rgb`.

### Pipeline Caching (`standard-pipeline.ts`)

`getOrCreateStandardPipeline` keeps a per-`StandardShaderBindings` `Map<targetSignatureKey(sig), GPURenderPipeline>`. BGLs are stable across signatures (only the pipeline depends on `sig`), so meshBGs validate against any pipeline produced for the same `(features, meshFeatures, sceneFeatures, variants)` bindings instance.

Composed shaders are also cached with that full shader key to avoid recomposition when only format/MSAA differs. Fog presence is mandatory in the key because fog changes emitted WGSL without changing material or mesh bits. The group-0 scene bind group is owned by `RenderTask`; Standard renderables bind only material/mesh/shadow groups.

Pipeline and composed shader caches are cleared on GPU device change.

### Renderable Builder (`standard-renderable.ts`)

`buildStandardMeshRenderables(scene, meshes, factories)`:

1. Resolves each mesh material or material view to source material state plus render features.
2. Applies enabled Standard mesh feature bits from the current mesh.
3. Builds fragment lists from feature flags + `StdFragmentFactories`.
4. Calls `composeStandardShader(features, meshFeatures, fragments, ..., sceneShader)`.
5. Creates/reuses sig-independent shader bindings and sig-specific pipelines.
6. Creates one `Renderable` per mesh (order = `mesh.renderOrder ?? (isTransparent ? 200 : 100)`).
7. Relies on `RenderTask` for the group-0 scene UBO and scene-owned lights UBO refresh.
8. Acquires textures for reference counting, registers cleanup disposables.

When thin instances are present, the draw function calls `tiSync(device, ti, pass, slot, hasInstanceColor)` to synchronize GPU buffers before each instanced draw, and uses `drawIndexed(indexCount, ti.count)` for instanced rendering.

### Single-Mesh Rebuild Closure

The `rebuildSingle(scene, mesh, materialOverride?)` closure returned from `buildStandardMeshRenderables()` rebuilds one mesh after a material swap or pass-specific override without rebuilding the entire scene. It accepts `MaterialOrView`, uses view render features with source material resources, computes material/mesh features and shader variant keys, creates/reuses shader bindings and pipelines, writes per-mesh light selections, builds optional shadow bind groups, and returns a `Renderable` that early-exits if the mesh material changed again unless it was built for an explicit override.

## Fragment Modules

All fragments live in `src/material/standard/fragments/` and export factory functions returning `ShaderFragment` objects.

### `normal-map-fragment.ts` — Bump/Normal Mapping

- **Factory**: `createNormalMapFragment(): ShaderFragment`
- **ID**: `"normal-map"`
- **Bindings**: `bumpTex` (texture2D), `bumpSampler` (sampler)
- **Helper WGSL**: `WGSL_PERTURB_NORMAL` — cotangent-frame normal perturbation from screen-space derivatives
- **Fragment slot**:
    - `AC` — `normalW = perturbNormal(input.vNormalW, input.vPositionW, input.vUV, mat.bumpScale)`

### `std-emissive-fragment.ts` — Emissive Texture

- **Factory**: `createStdEmissiveFragment(): ShaderFragment`
- **ID**: `"std-emissive"`
- **Bindings**: `emissiveTex` (texture2D), `emissiveSampler` (sampler)
- **Fragment slot**:
    - `AT` — `emissiveContrib = mat.vEmissiveColor * textureSample(emissiveTex, ..., input.vUV).rgb * mat.textureLevel`

### `std-specular-fragment.ts` — Specular Texture

- **Factory**: `createStdSpecularFragment(usesUV2: boolean): ShaderFragment`
- **ID**: `"std-specular"`
- **Bindings**: `specularTex` (texture2D), `specularSampler` (sampler)
- **Fragment slot**:
    - `AT` — `specularColor = textureSample(specularTex, ..., uv).rgb` (uses UV or UV2 based on `usesUV2`)

### `std-ambient-fragment.ts` — Ambient/Occlusion Texture

- **Factory**: `createStdAmbientFragment(usesUV2: boolean): ShaderFragment`
- **ID**: `"std-ambient"`
- **Bindings**: `ambientTex` (texture2D), `ambientSampler` (sampler)
- **Fragment slot**:
    - `AD` — `baseAmbientColor = textureSample(ambientTex, ..., uv).rgb * mat.ambientTexLevel`

### `std-lightmap-fragment.ts` — Lightmap Texture

- **Factory**: `createStdLightmapFragment(usesUV2: boolean): ShaderFragment`
- **ID**: `"std-lightmap"`
- **Bindings**: `lightmapTex` (texture2D), `lightmapSampler` (sampler)
- **Fragment slot**:
    - `BC` — additive lightmap: `color = vec4(color.rgb + textureSample(lightmapTex, ..., uv).rgb * mat.lightmapLevel, color.a)`

### `std-opacity-fragment.ts` — Opacity/Transparency Texture

- **Factory**: `createStdOpacityFragment(fromRGB: boolean): ShaderFragment`
- **ID**: `"std-opacity"`
- **Bindings**: `opacityTex` (texture2D), `opacitySampler` (sampler)
- **Fragment slot**:
    - `AT` — modulates alpha:
        - RGB mode (`fromRGB=true`): `alpha *= luminance(textureSample(...).rgb) * mat.opacityLevel`
        - Alpha mode: `alpha *= textureSample(...).a * mat.opacityLevel`

### `std-vertex-color-fragment.ts` — Mesh Vertex Colors

- **Factory**: `createStdVertexColorFragment(): ShaderFragment`
- **ID**: `"std-vertex-color"`
- **Vertex attribute**: `color: vec4<f32>` (`float32x4`, 16-byte stride)
- **Varying**: `vColor: vec4<f32>`
- **Vertex slot**:
    - `VB` — `out.vColor = color`
- **Fragment slot**:
    - `AT` — `baseColor *= input.vColor.rgb; alpha *= input.vColor.a`
- **Activation**: `enableStandardVertexColors()` installs the factory; only meshes with a color buffer use it.

### `std-reflection-fragment.ts` — Reflection Texture

- **Factory**: `createStdReflectionFragment(): ShaderFragment`
- **ID**: `"std-reflection"`
- **Bindings**: `reflectionTex` (texture2D), `reflectionSampler` (sampler)
- **Helper WGSL**: `computeSphericalCoords()`, `computePlanarCoords()`
- **Fragment slot**:
    - `AD` — chooses spherical vs planar coords via `mat.reflectionCoordMode`, samples reflection texture, writes `reflectionColor * mat.reflectionLevel`

### `std-shadow-fragment.ts` — Shadow Receiving

- **Factory**: `createStdShadowFragment(shadowLights: ShadowLightSlot[]): ShaderFragment`
- **ID**: `"std-shadow"`
- **Interface**: `ShadowLightSlot { lightIndex: number; shadowType: "esm" | "pcf" }`
- **Varyings**: per-light `vPosFromLight_<n>` (`vec4<f32>`), `vDepthMetric_<n>` (`f32`)
- **Bindings**: per-light shadow textures + samplers + `shadowInfo_<n>` uniform buffers (group `"shadow"`)
- **Helper WGSL**: per-light `shadowInfo_<n>Uniforms` struct, ESM (`computeShadowESM_<n>`, `computeFallOff_<n>`) and PCF (`computeShadowPCF_<n>`) functions
- **Vertex slot**:
    - `VB` — transforms world position into light space, computes depth metric
- **Fragment slot**:
    - `AD` — writes `shadowFactors[lightIndex]` per light via ESM or PCF

## Shader Logic

### Vertex Shader (composed by template + fragments)

```
finalWorld = mesh.world
if skeleton: finalWorld = mesh.world × weightedBoneMatrix(joints, weights[, joints1, weights1])
worldPos = finalWorld × vec4(positionOrMorphedPosition, 1.0)
normalWorld = mat3x3(finalWorld[0].xyz, finalWorld[1].xyz, finalWorld[2].xyz)
vNormalW = normalize(normalWorld × normal)
clipPos = scene.viewProjection × worldPos
if scene fog: vFogDistance = (scene.view × worldPos).xyz
```

If NEEDS_UV: `vDiffuseUV = uv × uvScaleOffset.xy + uvScaleOffset.zw`, where omitted `uvOffset` contributes `[0, 0]`.
If vertex colors are enabled and present: pass a `vec4<f32>` `vColor` varying; fragment RGB always multiplies `baseColor`. Under the `mesh.hasVertexAlpha` opt-in (VERTEX_ALPHA) alpha additionally multiplies the running `alpha` and the alpha-test comparison uses `vColor.a`; otherwise the vertex color is RGB-only.
If RECEIVE_SHADOWS: `vPositionFromLight = shadow.lightMatrix × worldPos`, `vDepthMetric = (lightClip.z + near) / far`
If CSM: derive cascade-selection view Z from `(scene.view × vec4(vp, 1)).z` in the fragment shader; do not depend on the fog-only `vFogDistance` varying.

### Fragment Shader (composed by template + fragments)

#### Blinn-Phong Lighting (`computeLighting`)

```
if lightData.w == 0:  // Point light
  direction = lightPos - fragmentPos
  attenuation = max(0, 1 - length(direction) / range)
  lightVectorW = normalize(direction)
else:                  // Directional light
  lightVectorW = normalize(-lightDir)
  attenuation = 1.0

NdotL = max(0, dot(N, L))
diffuse = NdotL × lightDiffuse × attenuation
H = normalize(V + L)
specComp = pow(max(0, dot(N, H)), max(1, glossiness))
specular = specComp × lightSpecular × attenuation
```

When vertex colors are enabled:

```
baseColor *= vColor.rgb
alpha *= vColor.a
```

#### ESM Shadow (if RECEIVE_SHADOWS)

```
shadowPixelDepth = clamp(depthMetric, 0, 1)
shadowMapSample = textureSampleLevel(shadowTex, shadowSampler, uv, 0).x
esm = 1 - clamp(exp(min(87, depthScale × shadowPixelDepth)) × shadowMapSample, 0, 1 - darkness)
shadow = computeFallOff(esm, clipSpace.xy, frustumEdgeFalloff)
```

#### Fog (`calcFogFactor`)

```
dist = length(vFogDistance)
mode 3 (linear):  fogCoeff = (end - dist) / (end - start)
mode 1 (exp):     fogCoeff = 1 / e^(dist × density)
mode 2 (exp2):    fogCoeff = 1 / e^(dist² × density²)
```

#### Final Composition

```
baseColor = textureSample(diffuseTex, ...) × textureLevel     // if HAS_DIFFUSE_TEXTURE, else vec4(1)
emissiveTex = textureSample(emissiveTex, ...).rgb              // if HAS_EMISSIVE_TEXTURE
shadow = computeShadowWithESM(...)                             // if RECEIVE_SHADOWS, else 1.0

finalDiffuse = clamp(diffuseBase × shadow × diffuseColor + emissiveColor + ambientColor, 0, 1) × baseColor.rgb
finalSpecular = specularBase × shadow × specularColor
color = vec4(finalDiffuse + finalSpecular, alpha)
color = max(color, 0)
if fogMode > 0: color.rgb = mix(fogColor, color.rgb, fogCoeff)
```

## State Machine / Lifecycle

```
addToScene(scene, mesh)            → mesh registered for deferred building
registerScene(scene)       → runs deferred builders and builds frame graph
  standardGroupBuilder()           → detects features, dynamically imports fragment modules
  buildStandardMeshRenderables()   → groups meshes by features, composes shaders
    composeStandardShader()        → createStandardTemplate() + composeShader(template, fragments)
    getOrCreatePipeline()          → cached pipeline + scene UBO
    createDynamicMeshGPU()         → per-mesh UBOs + bind groups
  → renderables + updater registered by buildScene
  render loop begins
    updater.update(engine)         → refreshes light/material state
    RenderTask updates each DrawBinding with its target dimensions
    DrawBinding.draw(pass, engine) → dispatches draw calls per mesh
  mesh.material = newMat           → triggers single-rebuild path
    buildSingleStandardRenderable()→ recomputes features, gets pipeline, creates mesh GPU resources
```

## Babylon.js Equivalence Map

| Babylon Lite                                   | Babylon.js                                                     |
| ---------------------------------------------- | -------------------------------------------------------------- |
| `createStandardMaterial()`                     | `new BABYLON.StandardMaterial("mat", scene)`                   |
| `HAS_DIFFUSE_TEXTURE`                          | `#define DIFFUSE`                                              |
| `HAS_EMISSIVE_TEXTURE`                         | `#define EMISSIVE`                                             |
| `RECEIVE_SHADOWS`                              | `#define SHADOW0`                                              |
| `HAS_BUMP_TEXTURE`                             | `#define BUMP`                                                 |
| `HAS_SPECULAR_TEXTURE`                         | `#define SPECULAR`                                             |
| `HAS_AMBIENT_TEXTURE`                          | `#define AMBIENT`                                              |
| `HAS_LIGHTMAP_TEXTURE`                         | `#define LIGHTMAP`                                             |
| `HAS_OPACITY_TEXTURE`                          | `#define OPACITY`                                              |
| `HAS_REFLECTION_TEXTURE`                       | `#define REFLECTION`                                           |
| `THIN_INSTANCES` / `THIN_INSTANCE_COLOR`       | `mesh.thinInstanceSetBuffer(...)`                              |
| `material.disableLighting`                     | `material.disableLighting`                                     |
| `computeFeatures()`                            | Internal define computation in `StandardMaterial._getEffect()` |
| `getOrCreatePipeline()`                        | Pipeline cache in StandardMaterial                             |
| `createStandardTemplate()` + `composeShader()` | GLSL shader generation from defines                            |
| `ShaderFragment` composition                   | `#include` / `#define` preprocessor                            |
| `DrawBinding.update(context)`                  | Per-mesh/material uniform refresh before draw                  |
| `buildSingleStandardRenderable()`              | `Material._markAllSubMeshesAsAllDirty()`                       |
| `computeLighting()` in shader                  | `computeLighting()` in Babylon standard shader                 |
| `calcFogFactor()`                              | `CalcFogFactor()` in Babylon                                   |
| `computeShadowWithESM()`                       | `computeShadowWithESM()` in Babylon                            |

## Dependencies

- **`standard-material.ts`**: Imports `Texture2D` from texture-2d, `computeUboLayout` from ubo-layout, `createStandardTemplate` from standard-template.
- **`standard-template.ts`**: Imports `ShaderTemplate`, `UboField`, `VertexAttribute`, `Varying`, `BindingDecl` from fragment-types, `WGSL_FOG` from wgsl-helpers.
- **`standard-pipeline.ts`**: Imports `createStandardTemplate` from standard-template, `composeShader` from shader-composer, types from standard-material, shadow generator types, lights UBO helpers.
- **`standard-renderable.ts`**: Imports pipeline functions from standard-pipeline, `ShaderFragment` from fragment-types, scene/engine/mesh/light types, material-view types, renderable interface, resource pool helpers, and returns the single-mesh rebuild closure.
- **`no-color-view.ts`**: Imports `createMaterialView` and Standard feature flags to create no-color material views without pulling the helper into ordinary Standard scenes.
- **Fragment modules** (`fragments/`): Each imports only `ShaderFragment` (and optionally `BindingDecl`, `Varying`) from `fragment-types.js`.
- **`thin-instance-gpu.ts`** (`src/mesh/`): Conditionally imported by `standardGroupBuilder` when thin instances are detected.
- **Depended on by**: Application code (via `createStandardMaterial`), mesh factories, skybox-cubemap (shares scene UBO layout).

## Test Specification

| Test                              | Description                                                                         |
| --------------------------------- | ----------------------------------------------------------------------------------- |
| `createStandardMaterial defaults` | All properties match documented defaults                                            |
| `pipeline cache hit`              | Same features+format+msaa → same variant object                                     |
| `pipeline cache miss on features` | Different features → different variant                                              |
| `simple shader (features=0)`      | No UV attribute, no texture bindings                                                |
| `textured shader (features=1)`    | UV attribute added, diffuse texture bound                                           |
| `shadow shader (features=4)`      | Shadow UBO, shadow map bind group created                                           |
| `full shader (features=7)`        | All bindings present                                                                |
| `mesh grouping`                   | Meshes with same features share pipeline                                            |
| `Blinn-Phong NdotL=0`             | Diffuse = 0, specular = 0                                                           |
| `minimal Standard excludes fog`   | No fog helper or blend WGSL without a scene fog context                             |
| `explicit fog composition`        | Fog helper + blend appear with `STD_SCENE_FOG`                                      |
| `fog cache separation`            | Fog/non-fog scenes sharing a device get distinct bindings/WGSL                      |
| `default UV offset`               | Global UV-offset opt-in + missing material offset writes zero                       |
| `Standard skeleton composition`   | Shared 4/8-bone fragment, bone binding, and vertex layouts                          |
| `Standard vertex-color alpha`     | RGBA modulation precedes alpha-test and geometry mask                               |
| `geometry deformation`            | Geometry shader/layout/bind/draw variants include morph, skeleton, and vertex color |
| `root feature exports`            | Root declaration exposes each enabler with no package subpaths                      |
| `single rebuild`                  | Material swap rebuilds one mesh without full scene teardown                         |
| `fragment composition`            | Bump fragment injects perturbNormal helper + AC slot code                           |
| `shadow fragment ESM`             | ESM shadow factor computation per light                                             |
| `shadow fragment PCF`             | PCF shadow factor computation per light                                             |
| `scene267-standard-vertex-colors` | Standard RGBA vertex-color interpolation matches BJS exactly                        |

## File Manifest

| File                                                           | Size       | Purpose                                                                                                                                       |
| -------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/material/standard/standard-material.ts`                   | ~299 lines | Types (StandardMaterialProps, FogConfig), factory, collectStdBoundTextures, standardGroupBuilder with dynamic fragment imports                |
| `src/material/standard/standard-template.ts`                   | —          | `StandardTemplateConfig` + `createStandardTemplate()` — builds the minimal Blinn-Phong `ShaderTemplate`; fog is a dynamically loaded fragment |
| `src/material/standard/standard-pipeline.ts`                   | —          | `composeStandardShader()`, full shader-context cache keys, mesh bind groups, UV transform writes, material UBO writes                         |
| `src/material/standard/standard-renderable.ts`                 | —          | `StdFragmentFactories`, effective mesh features, per-scene shader context, composed bindings/pipelines, draw-time deformation buffers         |
| `src/material/standard/enable-standard-vertex-colors.ts`       | —          | Canonical process-global opt-in that installs Standard RGBA vertex-color support while preserving byte-identical non-feature bundles          |
| `src/material/standard/enable-standard-mesh-features.ts`       | —          | Published idempotent skeleton and UV-offset enablers                                                                                          |
| `src/material/geometry-deformation-hooks.ts`                   | —          | Lazy geometry-only shared deformation-velocity feature                                                                                        |
| `src/material/geometry-deformation-velocity.ts`                | —          | Renderable-local previous bone/morph snapshots and shader code shared by PBR and Standard                                                     |
| `src/material/standard/fragments/std-skeleton-fragment.ts`     | —          | Standard wrapper around the shared skeleton fragment; bone texture + joints/weights binding                                                   |
| `src/material/standard/std-fog-wgsl.ts`                        | —          | Dynamically imported Standard fog fragment (varying, helper, and final blend)                                                                 |
| `src/shader/fragments/skeleton-fragment.ts`                    | —          | Material-agnostic 4/8-bone WGSL shared by PBR and Standard                                                                                    |
| `src/material/standard/standard-geometry-renderable.ts`        | —          | Geometry-pass Standard variant cache, layouts, bindings, updates, and draw buffers including deformation state                                |
| `src/material/standard/no-color-view.ts`                       | ~16 lines  | `createStandardNoColorMaterialView()` — pass-specific no-color material view helper                                                           |
| `src/material/standard/fragments/normal-map-fragment.ts`       | ~33 lines  | Cotangent-frame bump/normal mapping fragment (`AC` slot)                                                                                      |
| `src/material/standard/fragments/std-emissive-fragment.ts`     | ~17 lines  | Emissive texture sampling fragment (`AT` slot)                                                                                                |
| `src/material/standard/fragments/std-specular-fragment.ts`     | ~18 lines  | Specular texture sampling fragment (`AT` slot, UV/UV2 aware)                                                                                  |
| `src/material/standard/fragments/std-ambient-fragment.ts`      | ~18 lines  | Ambient/AO texture sampling fragment (`AD` slot, UV/UV2 aware)                                                                                |
| `src/material/standard/fragments/std-lightmap-fragment.ts`     | ~18 lines  | Additive lightmap fragment (`BC` slot, UV/UV2 aware)                                                                                          |
| `src/material/standard/fragments/std-opacity-fragment.ts`      | ~20 lines  | Opacity texture fragment (`AT` slot, RGB or alpha mode)                                                                                       |
| `src/material/standard/fragments/std-vertex-color-fragment.ts` | ~20 lines  | RGBA vertex attribute/varying and base-color/alpha multiplication (`VB` + `AT` slots)                                                         |
| `src/material/standard/fragments/std-reflection-fragment.ts`   | ~39 lines  | Spherical/planar reflection fragment (`AD` slot)                                                                                              |
| `src/material/standard/fragments/std-shadow-fragment.ts`       | ~155 lines | ESM/PCF shadow receiving fragment (per-light, `VB` + `AD` slots)                                                                              |
| `src/mesh/thin-instance-gpu.ts`                                | ~50 lines  | `syncThinInstanceBuffers()` — uploads instance matrix/color vertex buffers                                                                    |
| `src/shader/shader-composer.ts`                                | ~293 lines | `composeShader()` — topological sort, UBO merge, binding assignment, slot injection                                                           |
