API

Module: Mesh opacity

Package path: packages/babylon-lite/src/mesh/opacity/

Purpose

Provide opt-in native per-mesh opacity for ordinary Standard and PBR meshes without changing or cloning their materials. The compat layer implements Mesh.visibility automatically through the same native API. Meshes sharing a material, including glTF primitive meshes, can have independent opacity.

Opacity is separate from mesh visibility, hierarchy, picking, bounds and visibility epochs. Thin instances share their owning mesh's opacity. Node/shader materials cannot fade below 1. MeshLoD instances are not ordinary Mesh objects and are not supported targets at any opacity. Per-instance opacity, submesh opacity and compat InstancedMesh.visibility are outside this module's contract.

Public API surface

Both functions are named exports from the sole package root:

function setMeshOpacity(mesh: Mesh, opacity: number): void;
function getMeshOpacity(mesh: Mesh): number;

The default is 1. The native setter clamps finite numbers to [0, 1]; the getter returns the stored clamped value, including after disposal.

The setter performs these operations in order:

  1. Reject non-number, NaN and infinite input with RangeError: setMeshOpacity: opacity must be a finite number, got <value>.
  2. Require an object with the existing _gpu Mesh discriminator. Otherwise throw: setMeshOpacity: "<name>" is not a Lite Mesh (for example a MeshLoD instance); per-mesh opacity is not supported on MeshLoD in this release. The getter performs the same target check with its own API-name prefix.
  3. Reject _disposed: Mesh "<name>" cannot set opacity: it was disposed when it left its last scene. Create a new mesh instead.
  4. Clamp the value.
  5. If below 1, validate a non-null material's _buildGroup._materialFamily. Only "standard" and "pbr" are accepted; otherwise throw: setMeshOpacity: mesh "<name>" uses a "<family>" material; mesh opacity supports only "standard" and "pbr" materials.
  6. Install the feature once. If the stored/default value already equals the new clamped value, return without storing, dirtying or writing.
  7. Store the value and synchronously set mesh._skipColorDraw = value === 0, enqueue a rebuild on crossing 1, and update live binding uniforms with every changed value, including 1.

A null material is permitted. An ordinary, non-disposed Mesh can reset to 1 regardless of material family. Invalid calls must not install hooks/extensions, store state, allocate/write buffers, enqueue swaps or mutate scenes.

Internal architecture

Mesh value and binding records

The feature uses a local intersection, not a member on the core Mesh interface:

type OpacityMesh = Mesh & { _opacity?: number };
interface OpacityBinding {
readonly _buffer: GPUBuffer;
readonly _device: GPUDevice;
readonly _engine: EngineContext;
readonly _disposers: (() => void)[];
}

mesh-opacity.ts owns validation, the scalar, installation, transitions and GPU records. A lazily created WeakMap<Mesh, Set<OpacityBinding>> tracks live binding-owned buffers. Neither the map nor its sets are allocated at import time. A reusable one-element Float32Array supplies live writes; a separate two-element array supplies binding initialization.

Each mesh's binding lifetime and device pair owns one 16-byte uniform buffer. Its first two f32 values are [opacity, policy], at byte offsets 0 and 4. Standard ignores the second value (initialized to 0); PBR uses it as an immutable per-binding OPAQUE policy. Live setters write only four bytes at offset 0, never the policy. Repeated binds with the same disposer-array identity and device reuse the existing buffer and record without another allocation, initial upload or disposer, preserving its creation-time policy even if a later bind supplies a different policy. Different lifetime arrays or devices create separate records; no material owns this buffer. Native shallow mesh clones copy the scalar and its corresponding skip flag but never the records.

Lazy installation and chained installers

The first successful setter call runs this once-guarded sequence:

  1. Register Standard extension "0-mesh-opacity".
  2. Register PBR extension "z-mesh-opacity".
  3. Install a mesh-feature encoder returning 1 << 22 iff opacity is below 1.
  4. Install a PBR binding-context augmenter that copies the binding's disposer array into context._disposers.
  5. Install material-transition validation using the mesh's live opacity.

Extension registration invalidates sorted-extension caches. No registration, map/set allocation or GPU work executes merely by importing the API.

The generic installers preserve earlier features in installation order:

InstallerComposition
_installMeshFeatureExtra
OR the previous encoder's result with the new encoder's result.
_installPbrBindingContext
Invoke the previous augmenter, then the new augmenter, with the same context, scalar id and disposers.
_installMeshMaterialValidator
Run the previous validator, then the new validator, before mutation.

This preserves mirrored-mesh encoding, rect-area binding context and indexed-indirect borrow validation in either install order. Features installing into a chained slot must guard their own one-time installation: opacity does so, and installRectAreaEmissionPbr does so before installing its lighting-id/disposer augmenter. Binding ids and disposer arrays remain per invocation, not captured from the first prepare.

Mesh-bit transport and Standard folding

material/mesh-features.ts reserves mesh bit 22 with a comment. The opacity encoder owns its meaning; it does not add a shared opacity constant or read opacity from core code. The installed encoder returns 0 at opacity 1 and sets bit 22 below 1. It does not encode material alpha policy.

PBR's opacity fragment declares the feature-local MESH_OPACITY_MESH_BIT = 1 << 22. OPAQUE alpha policy lives in the uniform, not a shader-variant bit.

Standard uses its existing extension hook:

_meshFeatures?(meshFeatures: number, material?: StandardMaterialProps): number;

The opacity Standard hook tests mesh bit 22 and the source material's cached NO_COLOR_OUTPUT | ESM_SHADOW_OUTPUT bits. For a fading colour mesh it returns the feature-local STD_MESH_OPACITY_BIT = 1 << 30 plus MATERIAL_ALPHA_BLEND; otherwise it returns 0. Bit 30 is reserved by a comment in standard-flags.ts. Both Standard forward and geometry fold this result into their effective material features.

Standard forward queue classification retains the live mat.alpha < 1 term and tests the final HAS_OPACITY_TEXTURE | MATERIAL_ALPHA_BLEND word. Queue and pipeline selection therefore use the opacity-folded blend bit without dropping the existing live material-alpha behavior.

PBR folding at three consumers

PBR has no per-mesh extension fold hook or scratch object. The existing generic transport is:

(meshFeatures >>> 16) & PBR_HAS_ALPHA_BLEND;

Mesh bit 22 maps to material alpha-blend bit 6. The feature bit remains in the mesh word for the opacity PBR fragment; the generic fold supplies routing, blend state and compose-key identity.

The three consumers are:

  1. pbr-renderable.ts forward rebuild. OR the transport into renderFeatures.features only when features2 contains neither PBR2_NO_COLOR_OUTPUT nor PBR2_ESM_SHADOW_OUTPUT. This effective word drives composition, binding/pipeline keys and transparent classification.
  2. pbr-geometry-renderable.ts geometry pipeline selection. Geometry's composition view strips the material blend branch, but its source blend decision ORs the transport into source._renderFeatures?.features ?? 0. This colour-output route therefore blends and disables depth writes for a fading mesh while the geometry shader still gates on effective alpha.
  3. pbr-built-variant.ts composed-variant lookup. Before _pbrComposeKey, fold the same transport into the source features with the same no-colour/ESM mask as forward. The lookup must ask for the variant the forward rebuild actually composes.

Shadow/no-colour/ESM outputs do not gain the alpha-blend bit or opacity fragment. Mesh bit 22 may still distinguish otherwise identical shadow variant keys; it does not change caster alpha.

_pbrMeshRequest(scene, mesh, noShadows) retains its four-item return:

readonly[(context, meshFeatures, lightMode, singleLightType)];

Forward generation metadata remains:

[geometryContext, meshFeatures, lightMode, singleLightType, renderFeatures];

There is no additional folded-material field in either tuple and no change to Renderable or geometry-view generation metadata.

Binding context and retirement transport

_PbrBindCtx._mesh permits Mesh | null. The opacity extension requires an actual mesh and its binding-owned disposer array.

_installPbrBindingContext also enables _pbrBindingIdentity, an identity function for the optional numeric binding id, and _createPbrBindingDisposers, a factory returning a fresh array. createPbrMeshBindGroup builds its ordinary context, invokes the installed augmenters, then runs the fragment bind hooks. Only feature-owned augmenters populate _disposers and _lightingId.

PBR geometry uses exactly:

const bindingDisposers = _createPbrBindingDisposers?.();

There is no ?? [] fallback. Without an installer this remains undefined and does not allocate an auxiliary lifetime collection. With opacity installed, the factory exists before geometry binds. The same array reaches the PBR context and geometry teardown. Standard passes its existing main/geometry binding-owned array through StdExt._bind.

Transparent and geometry loop skip

The Mesh interface declares an optional @internal _skipColorDraw?: boolean. It has no factory initializer and is absent from public declarations. Only setMeshOpacity writes it, immediately after storing the clamped scalar. It is true exactly at opacity 0 and false at every other assigned value. It lives with the mesh and owns no resources, so no additional disposal hook is needed.

The general render loops read this generic flag alongside their existing visible === false test. For this feature, a plain internal-field read is an explicitly approved exception to GUIDANCE's engine-installed-seam requirement: it costs about 16–25 B in scenes that never use opacity, less than the alternative opt-in per-renderable draw-wrapper hook, which measured up to 148 B in the same scenes. No renderer imports opacity or reads the opacity scalar.

LoopSkip conditionRouting
render-task-base.ts drawList
mesh && (mesh.visible === false || (transparent && mesh._skipColorDraw))
Fourth optional boolean is true only for the transparent list and transmission overlay. Opaque bundle recording and direct lists omit it.
transmission.ts transparent loop
mesh && (mesh.visible === false || mesh._skipColorDraw)
Checked at the existing position, after any transmissive texture update. The renderOnTop overlay uses drawList(..., true).
geometry-renderer-task.ts geometry loop
mesh.visible === false || mesh._skipColorDraw
Geometry bindings are colour/output bindings, never shadow casters.

Fading Standard/PBR colour bindings are forced into transparent lists by the existing feature folding. Shadow renderables are never transparent, so their opaque/direct lists ignore the skip flag and preserve full-strength casting. Opaque render-bundle recording and visibility epochs are unchanged.

The loops read the flag every frame. A 0.5 → 0 → 0.5 transition skips and resumes without rebuilding or caching a stale skip. Opaque twins replay the same bundle. Public visible, compat isVisible, picking and accessibility are independent and never changed by opacity.

This is CPU draw omission, not shader discard. Standard/PBR builders return their ordinary unwrapped draws; there is no mesh draw-hook module, wrapper or installer. Direct invocation of a raw binding bypasses task-level filtering. The diagnostic overdraw probe remains unchanged and may count opacity-0 transparent bindings.

Pipeline configuration

RouteOpacity 1Opacity in (0, 1)Opacity 0
Standard/PBR main and offscreen colour
Existing variant/bindings
Blend on, depth writes off, sorted transparent queue
No draw
PBR refraction
Existing variant
Same per-mesh factor; texture-group policy unchanged
No draw
Standard/PBR geometry output
Existing variant
Blend on, depth writes off; alpha reaches > 0.4 gate
No draw/outputs
No-colour/CSM/PCF/ESM shadows
Unchanged
Full-strength caster; no opacity fragment/binding
Full-strength caster
Picking, culling, bounds and visible
Unchanged
Unchanged
Unchanged
Transmission opaque capture/GPS depth
Existing classification
Transparent, excluded from opaque/depth capture
Transparent, no draw

Fading a mesh in a geometry task makes it a blended participant. Every colour target must be blendable: default VIEW_DEPTH (r32float) requires the device feature float32-blendable, or an r16float format override. Integer tag targets are never blended. This is the same requirement as a material's own BLEND policy. PBR, Standard, and Node geometry pipeline creation enforces the format contract through _geometryColorTarget. A blended 32-bit float target without the feature throws the existing error: Transparent geometry output format "${format}" requires the float32-blendable WebGPU feature or a blendable format override. Validation also rejects blended integer formats; the mesh-blending tag target remains explicitly unblended.

Faded cutouts discard geometry fragments whose post-opacity alpha <= 0.4, before any data or mesh-blending tag write. They therefore preserve underlying tags rather than writing a cutout's raw tag through a suppressed data sample. The trade-off is that these samples also stop contributing to an extra emit-colour slot. Forward cutout coverage is unchanged.

PBR main transparent order is 150; Standard main and both geometry routes use 200. Existing material-level alpha-to-coverage and :a2c pipeline identity remain; A2C and blending both see final alpha. No material alpha, alpha-test cutoff, alphaBlend flag or visible flag is mutated. Mirroring, VAT, thin instances and transmission remain independent.

Uniform layout

Each active opacity fragment contributes one structured declaration:

{
_name: "meshOpacity",
_type: { _kind: "uniform-buffer" },
_visibility: SS.FRAGMENT,
}

Standard's helper is struct meshOpacityUniforms{value:f32}; PBR's is struct meshOpacityUniforms{value:f32,policy:f32}. The composer assigns the group-1 binding index from declarations; never parse emitted WGSL for an index. The bind hook pushes { binding, resource: { buffer } } and returns binding + 1. It reuses a record with the same lifetime array and device when present; otherwise it initializes the uniform from current mesh state, records the buffer and appends its disposer. Missing mesh/scene or lifetime transport throws a route-specific internal invariant error rather than silently allocating a fallback collection.

_bindMeshOpacity accepts a final optional numeric policy = 0 argument. Only creation initializes both floats; a reused record never rewrites them. PBR chooses the classifier with the same post-fold ctx._features & PBR_HAS_ALPHA_BLEND discriminator as its fragment slots. Forward bindings use the rendered binding material's own _renderFeatures: a public material view's declared flags, not its source's flags. Otherwise, geometry bindings use (material.source ?? material)._renderFeatures, because the geometry view strips the blend branch. Set policy 1 iff the chosen features exist and their pre-fold features contain neither PBR_HAS_ALPHA_BLEND nor PBR_HAS_ALPHA_TEST; otherwise set 0, including when features are absent. This uses only a type-only material-view import and plain fields, adding no runtime module dependency to Standard-only opacity scenes.

Overrides therefore use the override's own policy, not mesh.material. Material swaps and renderable rebuilds follow the new material/view policy through new lifetime-owned bindings. Rebinding the same lifetime/device retains its creation-time policy, consistent with its already-prepared pipeline variant.

Standard's leading "0-mesh-opacity" extension/fragment id aligns registry and composer ordering before "0-std-uv-transform". PBR uses id: "z-mesh-opacity", phase: "fragment"; its bind order follows the composed fragment key. Main, refraction and geometry each use the layout they composed.

Shader logic

Standard

Use slot BC:

alpha = alpha * meshOpacity.value;
color = vec4<f32>(color.rgb, color.a * meshOpacity.value);

The original AT alpha-test discard has already run; fog BA has not. Geometry compositions (features & GEOMETRY_OUTPUT) append if (mat.aCut > 0.0 && alpha <= 0.4) { discard; } to this BC. mat is in scope, and alpha is the same running value used by the geometry data gate. Forward BC remains exactly the two statements above. Colour returns color.a, while geometry gates on alpha > 0.4, so both alpha representations must be multiplied. No-colour/ESM templates return without this fragment. OPAQUE and surviving MASK fragments can therefore fade without changing their original discard threshold.

PBR

Return no fragment unless mesh bit 22 is set and features2 is neither no-colour nor ESM. Forward composition includes both policy normalization and the final opacity multiply; geometry combines them in one slot:

Composed featuresSlotStatement
PBR_HAS_ALPHA_BLEND set (forward)
BC
if (meshOpacity.policy > 0.5) { alpha = 1.0; }
Same forward composition
FA
finalAlpha = finalAlpha * meshOpacity.value;
Geometry with blend branch stripped
BC
alpha = select(alpha, 1.0, meshOpacity.policy > 0.5) * meshOpacity.value;

OPAQUE alpha policy: policy 1 treats source alpha as 1 even if the base-colour texture, base-colour factor or vertex colours carry alpha. Forward BC runs after decoding/vertex-colour alpha and before var finalAlpha=. The usual luminance fold saturates normalized alpha to 1; FA multiplies after that fold. Thus final alpha equals mesh opacity, not opacity plus luminance. Geometry normalizes and multiplies together before alpha > 0.4. Gamma-albedo and unlit paths declare alpha and use the same policy.

Own alpha policies: BLEND and material alpha below 1 retain the existing forward FA multiply after the normal output-alpha calculation: saturate(alpha * materialAlpha + luminanceOverAlpha²) * opacity. Geometry strips the blend branch and uses the combined BC with policy 0. MASK also uses policy 0 and keeps its forward FA / geometry BC behavior. Both occur after the original AT cutoff, so that cutoff tests the material's own alpha, not mesh opacity. Forward mesh opacity does not change which MASK fragments survive. In geometry only, PBR variants with PBR_HAS_ALPHA_TEST append if (alpha <= 0.4) { discard; } after the combined opacity BC, before the first data/tag write. Thus faded cutouts also obey the geometry data gate, while opacity-1 variants keep their original raw MASK tag and Standard aCut select. Non-cutout geometry and forward paths do not gain this discard. The opacity extension intentionally does not import geometry attachment state; a rejected sample also omits any extra emit-colour output.

The shadow-only fragment owns and internally exports PBR2_HAS_SHADOW_ONLY = 1 << 30. The lazy opacity fragment imports it from fragments/shadow-only-fragment.ts, not shared flags or the public root. When that features2 bit is set, declare dependency ["shadow-only"]; otherwise omit the dependency. Shadow-only's own features force alpha blending, so its policy is 0. The policy BC runs after shadow-only's BC and leaves that mask intact; opacity FA runs after shadow-only's final-alpha write and scales the mask, without a missing-dependency error or luminance bleed.

Refraction opens its conditional in NI and closes it in BC. The "z-" id places the geometry alpha override or multiplier after that closing brace. Transmission/refraction write colour, not alpha. The selected opacity slot runs before output or the geometry gate.

Why not a variant bit? The blend fold makes faded OPAQUE and BLEND materials share a compose key when their other features match. A distinguishing key bit would require shared transport/key changes and add bytes to unrelated scenes. Policy derived from cached mesh/material features can also go stale or describe the wrong material for an override. The binding uniform follows the rendered material/view classifier and its lifetime instead; no shared shader-key or render-loop change is needed.

State machine and lifecycle

TransitionDirtyingExisting uniform writesBuffer lifecycle
1 → 0.6
Once per crossing; enqueue in every owning scene
Latest factor to any still-live records
New lifetime allocates
0.6 → 0.4 → 0 → 0.3
None
Four bytes per live binding per changed value
No setter allocation/destruction
0.3 → 1
Once per crossing; enqueue in every owning scene
Write 1 to any still-live records
Old buffers retire with old bindings
Equal clamped value
None
None
None
Unregistered mesh
No scene enqueue
No records unless already bound
First bind reads stored value

Dirtying uses markMeshRenderableDirty and existing per-scene swap-queue deduplication; it never bumps the visibility epoch. Boundary enqueueing and direct writes are independent.

Explicit render tasks

An autoMirror: false render task normally retains the renderables resolved by addMeshToTask. A crossing of 1 changes the opaque/transparent pipeline variant only when the task opts into enableRenderTaskMeshRefresh before recording, and the entry is a scene-owned mesh added without a per-task material override. As with material swaps, the owning scene first drains its rebuild queue; the enabled task follows the resulting renderable version on execute. It rebuilds its own auxiliary renderable and retires the old lifetime disposers after submitted GPU work drains. See Frame Graph — explicit-task mesh refresh.

const task = createRenderTask({ name: "fading", rt, autoMirror: false }, engine, scene);
enableRenderTaskMeshRefresh(task);
addMeshToTask(task, mesh);

Once a transparent binding is current, updates below 1 and the 0 skip are live; they require no task rebuild. A plain explicit task retains its old renderable across crossings, so callers must deliberately synchronize/re-record it if they do not enable refresh. Opaque lists do not check the opacity-0 flag: those lists also serve shadow tasks, where opacity must not remove casters, and opaque render bundles cache draws. Skipping there would break the shadow gate and cached-bundle replay contract. The flag is read only on transparent/transmission colour lists and geometry paths.

Unregistered meshes and retained bindings

A mesh registered in no scene but drawn through an explicit geometry-task meshes list picks up opacity threshold crossings (1 ↔ <1, selecting a pipeline/blend variant) only at that task's next renderable sync. This is the same contract as material/topology changes on such meshes; the setter does not invalidate off-scene geometry tasks. Value updates below 1 and the opacity-0 skip apply immediately. Returning to 1 also writes 1 to retained binding buffers, so they do not retain the old opacity while waiting for that sync.

Rapid double crossing: for 0.3 → 1 → 0.6 before asynchronous replacement settles, the retained transparent binding must receive 0.6, even though a rebuild was enqueued. A replacement binding initializes from the latest stored value. The intermediate transition to 1 writes 1 to still-live buffers. Zero/fraction changes require no rebuild because the loops read the synchronously updated skip flag.

For each direct write, require record._engine._device === record._device. Skip stale-device records without destroying them in the setter. The binding disposer removes its record before destroying the buffer and is idempotent.

Refraction rebinds reuse the opacity record and buffer associated with the same renderable lifetime array and device, even when other bind-group entries change. Each new lifetime or device gets a separate buffer. There is no new signature cache. Teardown of one scene removes only that scene's records when a mesh belongs to several scenes. The existing last-scene removal disposes mesh GPU resources and marks it disposed; opacity adds no mesh-disposal or retirement observer.

Recovery's material-group rebuild computes mesh features even at opacity 1. The installed opacity feature encoder, when the binding map exists, retires records whose engine's current device differs from their captured device. The setter uses the same helper before writing live records. Retirement removes the record before destroying its lost-device buffer; a later old disposer sees that deletion already happened and does nothing. Recovery clears the scene's disposer map without invoking it, so encoder retirement is required even if opacity was reset to 1 and no replacement binding is created. Faded meshes rebuild bindings from the latest scalar; live updates reach only matched-device records. Opacity owns no device-scoped cache.

Material replacement and target exclusion

For a faded registered mesh, the chained validator rejects an unsupported material synchronously before mesh.material stores the value or queues work. Registration validates an unregistered mesh's pending raw assignment before enrollment. Previous validators retain their ordering and atomicity.

MeshLoD instances lack _gpu and cannot be passed to either opacity API. Their asset/instance enrollment paths do not accept an ordinary Mesh, so neither operation order puts an opacity-bearing Mesh into a MeshLoD batch. No MeshLoD hook, scan or metadata is added. Sharing a material with a fading ordinary Mesh does not mutate that material.

Babylon.js equivalence map

Compat Mesh.visibility statically imports the native API; there is no compat enabler. The wrapper stores the raw finite value but renders the clamped native value. Non-finite assignments throw. Family checks use the compat unsupported(...) helper.

Validate all owned primitives before writing any; publish the wrapper's raw value only after native writes succeed. A GPU wrapper owns itself. A transform-only glTF-node wrapper owns its immediate GPU primitive children, not descendants through child nodes. The synthetic root owns no geometry. _fromLite reads native opacity for an actual GPU wrapper; transform-only wrappers start at 1.

Raw getters belong to the wrapper assigned, not to an inherited parent factor. Owner fan-out changes primitive rendering factors without changing primitive wrappers' independent raw getters. Effective native opacity is also checked for material replacement. Pending NodeMaterial handles are rejected below 1 before their native material exists. Clone copies the raw value without a redundant setter. isVisible and _syncVisibility are unchanged.

BehaviorEquivalence/deviation
Programmatic Standard/PBR with no explicit BJS transparency mode
Visibility/blend parity
Native glTF OPAQUE fading
Source alpha normalized to 1; final alpha equals opacity
MASK / shadow-only fading
Preserve original cutoff / shadow mask, then scale opacity
Finite out-of-range values
Raw compat getter, clamped rendering
NaN/Infinity
RangeError deviation
Node/shader materials below 1
Explicit unsupported error
Shadows
Full strength even at zero
Picking, hierarchy and clones
Picking unchanged, no inheritance, scalar copied
InstancedMesh/submeshes
No new support

Dependencies

Use the existing Standard/PBR registries, structured shader composer, material family tags, scene validator/dirty registry, binding-owned lifetime arrays and clone/recovery paths. Runtime family code imports only generic seams, never this feature. Internal hooks and WebGPU handles do not leak through root declarations. The package still exposes one root export.

Core transport and draw call sites can survive across production chunk boundaries even when feature bodies are absent. Absence of opacity implementation markers is not proof of zero runtime-byte cost. Runtime-size checks count actually fetched JS with the repository's existing exclusions and gzip accounting, not artifact inventories.

Test specification

  • Native input/error atomicity, finite clamping, disposal, clone isolation, unchanged material/picking/visibility, MeshLoD rejection and null-material policy.
  • All boundary/non-boundary transition counts, multi-scene records, idempotent disposal, recovery/device mismatch and rapid-double-crossing buffer freshness.
  • Off-scene retained geometry bindings receive 1 on reset. Repeated binds on the same device and lifetime allocate one buffer/disposer/record and write once per update; different lifetime arrays or devices remain independent.
  • Atomic unsupported material swaps and both installation orders with the real indexed-indirect borrow validator; previous-first feature/context chains.
  • PBR per-binding OPAQUE policy with alpha-bearing texture/factor/vertex colours, gamma/unlit forward/geometry matrix, large luminance and coherent compose keys. Explicit overrides in both directions use the rendered binding's policy: forward public views use their own flags when adding or stripping blend, while geometry views use the source's flags. Plain OPAQUE forward bindings retain policy 1; OPAQUE/BLEND/OPAQUE/Standard swaps create new bindings. Live writes preserve the policy while changing only four bytes at offset 0; same-lifetime reuse preserves creation-time policy. Shadow-only initializes [opacity, 0] and retains its mask. BLEND/material-alpha multiplication and MASK forward FA / geometry BC remain after the original cutoff. Standard output alpha ignores diffuse-texture alpha. Verify shadow-only dependencies, refraction closing braces, geometry > 0.4 gates and composer/bind-index equality.
  • Transparent and geometry task loops skip colour draws at zero and resume live when the flag clears. Opaque/direct lists ignore it, preserving no-colour/ESM casters. Transmission and renderOnTop checks preserve texture-update ordering. Raw bindings remain unwrapped. Twin-bundle tests replay the same opaque bundle and preserve the visibility epoch while only the faded transparent draw changes.
  • Scene-owned non-override explicit entries follow opacity crossings only with mesh refresh. Drain the real scene rebuild queue and execute the real task: 1 → 0.5 selects transparent, 0.5 → 0 skips live, and 0.5 → 1 restores opaque and retires auxiliary lifetimes. Plain explicit tasks retain their renderables.
  • Flag tests cover zero-to-fraction/one, invalid-call atomicity, clone isolation, unchanged public visibility/pickability and accessibility projection.
  • Preserve Standard's live material-alpha classification, A2C plus blending, mirroring/VAT bindings, PBR generation identity and composed-key consistency.
  • Repeated rect-area emission installer calls register one context augmenter; that augmenter forwards each bind's current identity and lifetime array.
  • Compat raw/clamped values, independent isVisible, atomic glTF fan-out, no inheritance, clone/_fromLite and unsupported material assignments.
  • Build probes: unused entries contain no feature body markers; opted-in entries retain them. Root declaration trimming and root-only exports hold; MeshLoDInstance is a compile-time-invalid opacity target.
  • Scene354 supplies static Standard/PBR 1/0.5/0 and opaque twins, normalized OPAQUE/MASK cases, full-strength shadows and A2C. Its BJS golden is immutable; pixel/MAD coverage belongs to the existing scene parity infrastructure.

File manifest

src/ below means packages/babylon-lite/src/. Listed generic files contain transport or integration, not feature-owned opacity comparisons.

AreaFiles
Native API/feature
src/mesh/opacity/mesh-opacity.ts, mesh-opacity-standard.ts, mesh-opacity-pbr.ts; root exports in src/index.ts
Mesh transport
src/material/mesh-features.ts
Standard integration
src/material/standard/standard-flags.ts, standard-renderable.ts, standard-geometry-renderable.ts
PBR integration
src/material/pbr/pbr-flags.ts, pbr-pipeline.ts, pbr-renderable.ts, pbr-geometry-renderable.ts, pbr-built-variant.ts
Shadow-only predicate
src/material/pbr/fragments/shadow-only-fragment.ts
Draw filtering / validation
src/mesh/mesh.ts, src/frame-graph/render-task-base.ts, transmission.ts, geometry-renderer-task.ts, src/scene/mesh-scene-registry.ts
Rect-area installer guard
src/material/pbr/fragments/rect-area-light-emission-fragment.ts
Compat
packages/babylon-lite-compat/src/meshes/meshes.ts, README.md, COMPAT-STATUS.md
Architecture index
docs/lite/architecture/00-overview.md
Native unit tests
tests/lite/unit/mesh-opacity.test.ts, mesh-opacity-shader.test.ts, render-pass-task.test.ts, geometry-renderer-task.test.ts, pbr-forward-generation.test.ts
Rect-area regression
tests/lite/unit/area-light-emission-opt-in.test.ts
Build tests
tests/lite/build/mesh-opacity-treeshake.test.ts, public-api-types.test.ts
Compat tests
packages/babylon-lite-compat/tests/mesh-visibility.test.ts, mesh-clone.test.ts
Fixture
lab/lite/scene354.html, bundle-scene354.html, babylon-ref-scene354.html, src/lite/scene354.ts, src/bjs/scene354.ts, src/shared/scene354-opacity.ts
Registration
scene-config.json; HTML inputs, gallery and bundle tests auto-discover entries
Parity/assets
tests/lite/parity/scenes/scene354-mesh-opacity.spec.ts, reference/lite/scene354-mesh-opacity/babylon-ref-golden.png, lab/public/thumbnails/scene354.jpg