API

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), standard-geometry-feature-hooks.ts (lazy geometry factories), 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:

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; skeletal geometry velocity is a second lazy chunk loaded only by a velocity pass over a matching mesh.
  • 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

FlagBitConditionShader 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)

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)

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

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

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)

// 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)

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

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

Renderable (standard-renderable.ts)

/** 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

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

SlotAttributeFormatStrideShader Location
0
Position
float32x3
12 bytes
@location(0)
1
Normal
float32x3
12 bytes
@location(1)

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

AttributeFormatStrideStep ModeShader 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

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

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

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

BindingVisibilityTypeResource
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)FloatsWGSL TypeField
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)

OffsetWGSL TypeField
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)TypeField
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)TypeField
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)TypeField
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)TypeField
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):

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

SlotStagePurpose
/*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 LiteBabylon.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

TestDescription
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

FileSizePurpose
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/standard/standard-geometry-feature-hooks.ts
—
Scalar lazy-loader hook for opt-in skeletal geometry velocity
src/material/standard/standard-geometry-skeleton-velocity.ts
—
Double-buffered previous-bone textures loaded only by skeletal velocity passes
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