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):
-
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. -
ShaderFragment— each optional feature (normal mapping, emissive texture, specular texture, ambient texture, lightmap, opacity, reflection, shadows) is a fragment object with:id— unique string identifierfragmentSlots/vertexSlots— WGSL snippets keyed by slot namebindings—BindingDecl[]for textures/samplersvaryings— additional inter-stage varyings (shadows)helperFunctions/vertexHelperFunctions— WGSL helper code
-
composeShader(template, fragments)— topologically sorts fragments, merges UBO fields, assigns binding indices, replaces slot markers, and returns aComposedShaderwith 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 beforeregisterScene(), then setuScale,vScale,uOffset,vOffset, oruAngon any boundTexture2D. The Standard group builder loadsstd-uv-transform-fragment.tsonly when a marked material is present.enableStandardUvOffset()remains the lightweight shared-material translation path formaterial.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)
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-uboexport { 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
| 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):
- Resolves each mesh material or material view to source material state plus render features.
- Applies enabled Standard mesh feature bits from the current mesh.
- Builds fragment lists from feature flags +
StdFragmentFactories. - Calls
composeStandardShader(features, meshFeatures, fragments, ..., sceneShader). - Creates/reuses sig-independent shader bindings and sig-specific pipelines.
- Creates one
Renderableper mesh (order =mesh.renderOrder ?? (isTransparent ? 200 : 100)). - Relies on
RenderTaskfor the group-0 scene UBO and scene-owned lights UBO refresh. - 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 onusesUV2)
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
- RGB mode (
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 viamat.reflectionCoordMode, samples reflection texture, writesreflectionColor * 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>Uniformsstruct, 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— writesshadowFactors[lightIndex]per light via ESM or PCF
Shader Logic
Vertex Shader (composed by template + fragments)
finalWorld = mesh.worldif 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 × worldPosif scene fog: vFogDistance = (scene.view × worldPos).xyzIf 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 × attenuationH = normalize(V + L)specComp = pow(max(0, dot(N, H)), max(1, glossiness))specular = specComp × lightSpecular × attenuationWhen vertex colors are enabled:
baseColor *= vColor.rgbalpha *= vColor.aESM Shadow (if RECEIVE_SHADOWS)
shadowPixelDepth = clamp(depthMetric, 0, 1)shadowMapSample = textureSampleLevel(shadowTex, shadowSampler, uv, 0).xesm = 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_TEXTUREshadow = computeShadowWithESM(...) // if RECEIVE_SHADOWS, else 1.0
finalDiffuse = clamp(diffuseBase × shadow × diffuseColor + emissiveColor + ambientColor, 0, 1) × baseColor.rgbfinalSpecular = specularBase × shadow × specularColorcolor = 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 buildingregisterScene(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 resourcesBabylon.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: ImportsTexture2Dfrom texture-2d,computeUboLayoutfrom ubo-layout,createStandardTemplatefrom standard-template.standard-template.ts: ImportsShaderTemplate,UboField,VertexAttribute,Varying,BindingDeclfrom fragment-types,WGSL_FOGfrom wgsl-helpers.standard-pipeline.ts: ImportscreateStandardTemplatefrom standard-template,composeShaderfrom shader-composer, types from standard-material, shadow generator types, lights UBO helpers.standard-renderable.ts: Imports pipeline functions from standard-pipeline,ShaderFragmentfrom 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: ImportscreateMaterialViewand Standard feature flags to create no-color material views without pulling the helper into ordinary Standard scenes.- Fragment modules (
fragments/): Each imports onlyShaderFragment(and optionallyBindingDecl,Varying) fromfragment-types.js. thin-instance-gpu.ts(src/mesh/): Conditionally imported bystandardGroupBuilderwhen 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/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 |