Documentation
    Preparing search index...

    Class StandardMaterial

    A physically based surface: metallic-roughness, shaded by the scene's lights (§57, §59, §68).

    const gold = new StandardMaterial({
    baseColor: [1, 0.77, 0.34, 1],
    metalness: 1,
    roughness: 0.25,
    });
    const mesh = new Renderable(sphereGeometry(), gold);

    A Renderable carrying one produces a "standard" render item and draws through the standard pipeline. Like LitMaterial it needs the scene to supply the light: with no directional light it shades from the ambient term alone, and a geometry without a normals attribute has no surface to shade against and falls back to that same ambient term (see @fourjs/render-webgl's standard fragment stage).

    StandardMaterial sits above LitMaterial in §57's family, not beside it: the lit tier is one Lambert lobe times a colour, this one is a diffuse and a specular lobe with a Fresnel between them. The two shade in the same space and blend by the same rule (see the module header), so mixing them in one scene is supported and cheap — the cost of a second surface family is one pipeline switch where the render list changes kind.

    Materials are shared, not owned by nodes (§83): any number of Renderables may point at one, and disposing it is the job of whoever created it.

    Hierarchy (View Summary)

    Index
    baseColor: ColorRGBA

    The surface's base colour — glTF's baseColorFactor — as straight RGBA in 0…1; opaque white by default.

    For a dielectric (metalness: 0) this is the diffuse albedo; for a metal (metalness: 1) it is the specular reflectance F0, and the diffuse lobe vanishes. That single field doing two jobs is the metallic-roughness workflow, and it is why §59 names one colour rather than a diffuse and a specular one.

    The array instance is readonly — write into it — for the reason UnlitMaterial.color documents: a backend may keep a reference to it. Use StandardMaterial.setBaseColor, or write components directly and call Material.markDirty.

    colorWrite: boolean

    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.

    depthTest: boolean

    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.

    depthWrite: boolean

    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.

    emissive: ColorRGB

    Light this surface emits regardless of the scene's lights (§59), straight RGB; black [0, 0, 0] by default, which is §59's own example value.

    Added to the shaded result after both lobes and unaffected by metalness, roughness, or the ambient term, so an emissive surface reads the same whatever is lighting it. It does not illuminate anything else — that is §68's environment lighting, and light emitted by geometry needs the global-illumination path §69/§68 stage.

    Components pass through unclamped (see the module header), which is how an HDR emissive is authored today: [4, 2, 1] is a value four times over white, waiting for the tone mapping §60a stages. The array instance is readonly for the same reason StandardMaterial.baseColor's is.

    The type is ColorRGB — @fourjs/math's alias, hoisted there 2026-08-08 by R-15's colour packet exactly as this note asked (the tuple was written out inline until then, because a second own-definition of the name is what the duplicate-symbol gate exists to refuse and @fourjs/scene's copy was unreachable across the frozen §3.1 matrix). @fourjs/scene's ColorRGB is now the same declaration, re-exported.

    id: string

    Stable 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.

    kind: "standard" = ...

    Pipeline discriminant (§57, §64, §66 sort key 3) — see UnlitMaterial.kind for why this is a literal string rather than an instanceof.

    stencil: StencilState | undefined

    §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.

    transparent: boolean

    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:

    1. the backend enables blending for the draw and applies Material.blendMode. With 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;
    2. §66's sort key 2 places transparent items after opaque ones inside their render layer, so a blended surface composites over what it overlaps instead of racing it in scene order.

    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.

    • get blendMode(): BlendMode

      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.

      Returns BlendMode

    • set blendMode(value: BlendMode): void

      Parameters

      Returns void

    • get metalness(): number

      How metallic the surface is (§59) — glTF's metallicFactor, nominally 0 (dielectric) to 1 (conductor); 0 by default.

      It moves two things at once, which is the whole of the metallic workflow: the diffuse lobe is scaled by 1 − metalness, and the specular reflectance F0 is interpolated from a dielectric's 4% toward StandardMaterial.baseColor. A metal is therefore a surface with no diffuse colour whose reflection is tinted.

      The default is 0, not glTF's file-format default of 1: new StandardMaterial() should be the surface the material one tier down (LitMaterial) already draws, made physical — not an untextured black mirror. A glTF importer reads both factors out of the file and assigns them explicitly, so it can never inherit this difference (decision, R-13).

      Values outside 0…1 pass through rather than clamp, and non-finite values are rejected on every assignment (§85, the F14 discipline) — see requireFiniteScalar. Assigning bumps Material.version: it changes what the surface is, exactly as writing its colour does.

      Returns number

    • set metalness(value: number): void

      Parameters

      • value: number

      Returns void

    • get opacity(): number

      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.

      Returns number

    • set opacity(value: number): void

      Parameters

      • value: number

      Returns void

    • get roughness(): number

      How rough the surface is (§59) — glTF's roughnessFactor, nominally 0 (a mirror) to 1 (fully diffuse); 1 by default.

      The GGX distribution is parameterized by α = roughness², the perceptually-linear remapping glTF and every metallic-roughness renderer use, so halving this value does not halve the highlight — it is authored to be dragged on a slider.

      The default is 1 for the reason StandardMaterial.metalness's 0 is: a fully rough dielectric is the closest a StandardMaterial gets to the LitMaterial it sits above, which makes new StandardMaterial() a legible starting point rather than a black mirror.

      Range, validation, and versioning are StandardMaterial.metalness's exactly.

      Returns number

    • set roughness(value: number): void

      Parameters

      • value: number

      Returns void

    • get version(): number

      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.

      Returns number

    • 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.

      Returns void

    • 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.

      Returns void

    • Writes StandardMaterial.baseColor 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. Every component is validated before the first write, so a rejected call leaves the colour exactly as it was (the 2026-08-04 torn-state rule, applied to every material in the family).

      Parameters

      • red: number
      • green: number
      • blue: number
      • alpha: number = 1

      Returns this