Optionaloptions: LitMaterialOptionsReadonlycolorStraight RGBA in 0…1; opaque white by default. The array instance is
readonly — write into it — for the reason UnlitMaterial.color
documents: a backend may keep a reference to it. Use
LitMaterial.setColor, or write components directly and call
Material.markDirty.
Whether fragments write colour at all (§57); true by default. false
draws the material into the depth buffer only — a depth-prepass or an
occluder, visible to the depth test and to nothing else.
Whether fragments are tested against the depth buffer (§57); true by
default, which is the fixed state the backend sets at initialization.
false draws the material over everything already in the buffer, whatever
its depth — the "always front" overlay case §49's depthMode will
generalise.
Whether fragments write depth (§57); true by default.
The usual setting for a transparent surface is depthWrite: false with
depthTest: true: it is occluded by opaque geometry in front of it but
does not occlude the transparent surfaces drawn after it, which is what
keeps a stack of blended quads from punching holes in each other.
ReadonlyidStable identity (§57 inherits §83's resource model), assigned at
construction from the family-wide monotonic counter and prefixed by the
subclass — material-7, lit-material-8, sprite-material-9. Unique
within a process, ascending in construction order, never reused.
ReadonlykindPipeline discriminant (§57, §64) — see UnlitMaterial.kind.
§57's optional stencil state (§67); undefined by default, which leaves
the stencil test disabled — GL's initial state, and the state every frame
this engine drew before R-7 ran in.
material.stencil = new StencilState({ func: "equal", ref: 1, writeMask: 0 });
A plain property, like the four booleans above and unlike
Material.opacity, and the F14 rule is satisfied a different way:
every field of a StencilState is validated by the class itself, on
construction and on every later write, and the class is nominal — its
fields are private, so an object literal is not assignable to it. A caller
therefore cannot reach the backend with an unchecked comparison name
without going through a validating constructor, and this property has
nothing left to check.
That is also what keeps it free. The import above is type-only, so a
bundle whose scenes never mask does not carry StencilState — masking
costs bytes only where it is used, which a validating accessor here would
have made impossible (measured 2026-08-11 over four example bundles:
0.62 kB gzip each, which is 42% of what the whole packet costs them).
Like the rest of §57's render state, assigning does not bump
Material.version: a backend re-reads it per draw and caches none of
it. Mutating the StencilState in place is therefore live too, with no
announcement needed — and two materials may share one record, which is how
the two passes of one mask stay in step.
A backend that cannot allocate a stencil buffer honours this by drawing
without the mask, never by failing the frame (§61) — see
@fourjs/render-webgl's RendererOptions.stencil.
Whether this material's fragments are blended with the colour buffer
(§57, §66); false by default.
Two consequences, both stated because a flag that quietly did one of them would be the defect this replaced:
false — the default — blending stays
off and the material's alpha is written to the buffer unblended, which
is exactly what every scene authored before this flag existed did;Transparency is a property of the material, not of its alpha: an alpha of
0.5 on an opaque material is a deliberate way to write a half-alpha pixel
into the framebuffer (for compositing the canvas with the page), and
inferring transparent from it would take that away and make an
animated alpha change the sort order mid-frame.
How blended fragments combine with the buffer (§57, §66); "normal" — the
straight-alpha blend — by default. Read only while
Material.transparent is true, except on pipelines that blend by
construction (sprites, particles), where it always applies.
A value outside BlendMode is rejected on assignment as well as at construction (F14, 2026-08-07): the type is gone at runtime, and a backend that meets an unknown mode can only guess. Assigning does not bump Material.version.
Whether Material.dispose has run.
The albedo texture this material samples with the geometry's uvs, or
null for an untextured surface (§53, §77; R-19, 2026-08-07).
The sampled texel multiplies the base colour before the lighting term, so a texture darkens and tints the surface and the lights then shade it:
base = color × texture(map, uv)
fragment.rgb = base.rgb × (ambient + lightColor × max(N·−L, 0))
fragment.a = base.a
One map, deliberately: §59's metallic-roughness workflow puts normal,
roughness, metalness, occlusion, and emissive maps on StandardMaterial,
and each of them needs a second texture unit, a sampler binding, and a
shader term that §59 — not this class — defines. LitMaterial is the
minimal lit member (see the module header); a base-colour map is the one
addition that needs nothing new from the shading model.
Assigning bumps Material.version. Ownership, disposal, and the
uv-less-geometry behaviour are exactly UnlitMaterial.map's — see it.
Uniform transparency multiplier over the material's own alpha (§57), nominally in 0…1 and 1 by default.
The alpha a backend uploads is color[3] × opacity (tint[3] × opacity
for a sprite), so the two compose rather than compete: a shared material's
authored alpha stays where the artist put it and opacity is the knob an
animation or a fade-out drives. At the default 1 the product is the
authored alpha unchanged, bit for bit.
Like the colour components, values outside 0…1 pass through rather than
clamp (the WP-3.3 decision UnlitMaterial records); non-finite values are
rejected (§85) — on every assignment, not only at construction (F14,
2026-08-07; see the module header). A rejected write leaves the previous
opacity in place, exactly as a rejected setColor leaves the previous
colour. Assigning does not bump Material.version: render state
is read per draw, never cached.
Opacity alone does not make a material blend — set Material.transparent too, or the alpha is written into the colour buffer without being blended against it.
Counter incremented on every announced mutation. Backends cache uniform
uploads and texture bindings against it; treat it as opaque and compare for
inequality, exactly like Transform.version. Monotonic, never wraps in a
realistic session.
It tracks colour and resource edits, not render state — see the module header for why the six §57 fields do not bump it.
Releases this material (§83). Idempotent.
There is nothing GPU-side to free from here — the backend owns the pipeline and uniform buffers built from this material and drops them when it sees the material disposed — so this marks Material.disposed and bumps the version, which is what invalidates those backend caches. Colour arrays and texture references are kept: clearing them would turn a use-after-dispose bug into a silently black frame instead of a diagnosable one (§83's "disposed resources still in use" warning).
It does remove this material from the process-wide §83 live-instance
total (liveMaterialCount), exactly once: the idempotence guard above is
what makes a double dispose() subtract once rather than twice.
Announces a mutation the material could not see — a direct write into a colour array. Bumps the version by one. Calling it after a setter that already bumped is harmless, only wasteful.
Writes the color and bumps Material.version once. Returns
this for chaining (§7b). alpha defaults to 1 rather than to the
current alpha, for the reason UnlitMaterial.setColor documents.
Optionalalpha: number
A material shaded by the scene's lights: Lambert diffuse plus the scene ambient term (§57, §68).
A
Renderablecarrying one produces a"lit"render item and draws through the lit pipeline; with no directional light in the scene it shades from the ambient term alone, and with a black (default) ambient it renders black — lights are part of the scene, not of the material. A geometry without anormalsattribute also shades ambient-only (seeBufferGeometry.normals).Materials are shared, not owned by nodes: any number of
Renderables may point at one, and disposing it is the job of whoever created it (§83).