Module: Material Stencil
Package path:
packages/babylon-lite/src/material/Files:material.ts(StencilState),stencil-state.ts,enable-material-stencil.ts, and the stencil seams inpbr-pipeline.ts,standard-pipeline.ts,shader-pipeline.ts,pbr-renderable.ts,standard-renderable.ts,engine/engine.ts.
Purpose
Optional per-material stencil test baked into a material's main-pass pipeline. Lets one material write the stencil buffer where it draws (a mask) and another discard fragments where the stencil was written — with no dynamic stencil reference. Typical use: portals, decals, and mirror/clip masks.
Shared by every concrete material kind (Standard, PBR, Shader) so none depends on another. Stencil takes effect only on a stencil-capable depth target (the main color pass) and is ignored on depth-only/shadow targets.
Public API Surface
interface StencilState { readonly compare?: GPUCompareFunction; // default "always" readonly passOp?: GPUStencilOperation; // default "keep" readonly failOp?: GPUStencilOperation; // default "keep" readonly depthFailOp?: GPUStencilOperation; // default "keep" readonly readMask?: number; // default 0xFF readonly writeMask?: number; // default 0xFF}
// Attach to a material (post-creation optional property on all three kinds):// StandardMaterialProps.stencil?: StencilState// PbrMaterialProps.stencil?: StencilState// ShaderMaterial.stencil?: StencilState
function enableMaterialStencil(): void; // opt-in, process-global, before registerSceneMask/test without a dynamic reference: a writer uses compare: "always" + passOp: "increment-clamp"
(stencil 0 → 1 where it draws); a tester uses compare: "equal", which passes only where the stencil is
still the render pass's default reference of 0 (i.e. NOT written).
Internal Architecture — Zero-Cost Opt-in
Stencil is an explicit opt-in so stencil-free scenes stay byte-identical to a build without it
(mirrors enableMaterialPlugins). The construction of the stencil descriptor lives in its own module
(stencil-state.ts), and each pipeline builder references it only through a module-local resolver hook
that the tree-shaker + minifier can fold away when the opt-in is absent.
- Each of
pbr-pipeline.ts,standard-pipeline.ts,shader-pipeline.tsdeclares a module-locallet _stencilResolver: ((s: StencilState) => ResolvedStencil) | null = nullplus a single exported setter (_installPbrStencilResolver/_installStandardStencilResolver/_installShaderStencilResolver). stencil-state.ts→_resolveStencil(stencil)returnsResolvedStencil { _desc, _key }:_desc: a partialGPUDepthStencilState(stencilFront/stencilBackshare one ops object,stencilReadMask/stencilWriteMask), default-filled here so the object literals never land in the always-fetched graph._key: a pipeline-cache-key suffix so two materials differing only in stencil never share a pipeline.
enable-material-stencil.ts→enableMaterialStencil()imports_resolveStencil+ the three setters and installs the resolver into all three pipelines. It is the only module that imports the heavy resolver, so when an application never calls it,stencil-state.tsandenable-material-stencil.tsare not in the graph, the three setters become unused exports and tree-shake, and each_stencilResolveris provably alwaysnull. The minifier then foldsstencil && _stencilResolver ? … : null→nulland every downstream_stencil/_descbranch disappears — the Standard/PBR/Shader chunks are byte-identical.- The renderables (
buildPbrRenderables,buildStandardMeshRenderables) pass the rawmat.stencil ?? nullintogetOrCreate{Pbr,Standard}Bindings, which resolve via the hook, fold_keyinto the bindings cache key, and store_desc.getOrCreate{Pbr,Standard}Pipelinespreadbindings._stencilinto the depth-stencil descriptor only whensig._depthStencilFormat.includes("stencil").shader-pipeline.tsresolves via its own hook ingetOrCreateShaderPipelineand spreads the resolved_desc.
Pipeline Configuration
depthStencil.{stencilFront,stencilBack} = { compare, passOp, failOp, depthFailOp };
depthStencil.{stencilReadMask,stencilWriteMask} = masks. Applied only on the main color pass
(*-stencil8 formats); the same material in a depth32float shadow/depth pass keeps plain depth state, so
there is no depth-stencil format mismatch.
Babylon.js Equivalence Map
| Babylon.js | Babylon Lite |
|---|---|
material.stencil.func / funcMask | StencilState.compare / readMask |
material.stencil.opStencilDepthPass | StencilState.passOp |
material.stencil.opStencilFail / opDepthFail | StencilState.failOp / depthFailOp |
material.stencil.mask | StencilState.writeMask |
| always-on stencil state | opt-in enableMaterialStencil() (zero bundle cost when unused) |
Dependencies
material/material.ts (StencilState), material/stencil-state.ts (_resolveStencil), and the three
material pipeline + renderable modules. No new runtime dependencies; the resolver is reachable only via the
opt-in.
File Manifest
material/material.ts—StencilStateinterface (public).material/stencil-state.ts—ResolvedStencil,_resolveStencil(internal; opt-in-linked).material/enable-material-stencil.ts—enableMaterialStencil()(public opt-in; installs the resolver).material/{pbr,standard,shader}/*pipeline.ts— module-local_stencilResolver+_install*StencilResolversetter + hook-gated stencil seam.material/{pbr,standard}/*renderable.ts— pass rawmat.stencilinto the bindings.