API Reference · Materials and textures

Material

Namespace: Ava3D

public sealed class Material

The surface appearance of a MeshNode: metallic-roughness PBR, the same model glTF 2.0 defines, so a loaded model looks the way its author intended rather than the way a viewer guessed.

Every slot has a scalar and an optional texture, and the texture multiplies the scalar. A material with no textures at all is still a complete PBR material — Material.Metallic and Material.Roughness alone are enough to describe brushed steel or matte plastic, which is why the demo's material scenes need no image assets.

Two of these are shading-model choices worth knowing about. Material.Metallic is a blend, not a flag: at 0 the surface is a dielectric with a white 4% specular reflection and its own diffuse colour, at 1 it has no diffuse at all and reflects Material.BaseColor tinted. Values in between are physically meaningless for a pure substance but are how authored assets fake rust, wear and dust, so the renderers interpolate rather than threshold. And Material.Roughness is perceptual — it is squared before it reaches the GGX distribution, which is what makes the low end of the slider useful instead of collapsing to a mirror.

Properties

MemberDescription
float AlphaCutoff { get; set; }

Fragments whose base-colour alpha falls below this are discarded. Zero means the material is opaque and nothing is discarded. Foliage and cut-out details need it; without it they draw as solid cards.

Vector4 BaseColor { get; set; }

Linear RGBA, multiplied with Material.BaseColorTexture when there is one.

For a metal this is the reflectance tint — gold is roughly (1.00, 0.77, 0.34). For a dielectric it is the diffuse albedo.

TextureMapping BaseColorMapping { get; set; }

UV set and transform for Material.BaseColorTexture. Applied before shared material/node UV settings.

TextureProgram BaseColorProgram { get; set; }

A program that computes Material.BaseColorTexture rather than it being stored.

Assigning one points the map at the program's own texture, and everything downstream — the caches, the uploads, the shaders — then treats it as the ordinary texture it is. Changing the program's TextureProgram.Size afterwards moves the map to the texture that replaces it, and raises Material.Changed. Clearing it takes the map away again. Neither touches the map if something else has been put there in the meantime.

See TextureProgram for what a program can and cannot do; the short version is that it computes an image and never reaches the lighting.

Texture BaseColorTexture { get; set; }

Base colour map. Treated as sRGB-encoded and linearised by the renderer.

BlendMode Blend { get; set; }

How this material's fragments combine with what is already in the frame. BlendMode.Opaque by default.

Ordering is per object, not per triangle: within one Node.RenderOrder every opaque draw happens before every blended one, and blended draws are sorted back to front by the distance from the camera to their node's origin. That is enough for shells, glows and overlays, and it is not enough for two transparent surfaces that interpenetrate — which is a limit worth designing a scene around rather than discovering.

float BumpScale { get; set; }

How far Material.BumpTexture tilts the normal. Zero disables it.

Texture BumpTexture { get; set; }

A height field that perturbs the shading normal by its screen-space gradient.

Unlike Material.NormalTexture this needs no tangents, which makes it the better choice for a sphere — UV-sphere tangents degenerate at the poles — and for anything whose source data is elevation rather than a baked normal map. Only the red channel is read.

CullMode Cull { get; set; }

Which faces are rasterised. CullMode.None by default, which is what this control has always done and what unreliable winding needs.

The two cases that want otherwise are a sphere seen from inside — a sky dome, where only the far hemisphere should draw, so CullMode.Front — and a thin additive shell around a solid, where drawing the far side as well doubles the glow, so CullMode.Back.

The default is for materials built in code. A material loaded from a glTF file gets this from the file's own doubleSided flag instead, which is a statement about which faces exist and whose own default is single-sided — so most loaded geometry arrives at CullMode.Back.

float DepthBias { get; set; }

A constant nudge to this surface's depth, in the smallest step the depth buffer can represent. Positive pushes it away from the camera, negative pulls it forward. Zero, the default, is exactly what this control has always done.

What it is for: geometry that is coplanar with something else and has to lose, or win, consistently. The case that produces it is a hull drawn as triangles with its panel seams drawn as a LineNode over the top, where the line endpoints are hull vertices rather than vertices offset from the hull. The depth test cannot decide between two surfaces at the same depth, so the answer varies per pixel and per frame, and the seams break into dashes that crawl as the object rotates. Push the hull back by (1, 1) and the lines win everywhere.

Two things a caller reaches for first, both worse. Turning the lines' Material.DepthTest off draws them through the far side of the hull, so a ship's rear seams show through its nose. Moving the line vertices along the surface normal by a small epsilon works at one distance and detaches visibly at another, because the depth buffer's resolution varies with distance and a fixed offset cannot.

The unit is the depth buffer's own, which is why this pairs with Material.DepthBiasSlope rather than standing alone — see there.

float DepthBiasSlope { get; set; }

The same nudge, scaled by how steeply the surface recedes from the camera. Zero by default.

The reason both exist: a polygon seen edge-on spans many depth values within a single pixel, so the error a constant bias has to cover grows with the slope. The slope-scaled term measures that and the constant term covers the rest, which is why the fixed-function offset every GPU implements takes exactly these two numbers and why the pair is the whole of the feature.

DepthBiasSlope = 1, DepthBias = 1 is the setting three.js spells polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1, and it is the one to start from: it is a step or two of depth on a flat surface and grows to whatever an oblique one needs.

Where it applies: filled triangles, which is what the offset is defined for on every backend, the CPU fallback included. It has no effect on LineNode, PointsNode or SpriteNode — GL ES and WebGL guarantee the fill case only, and the fallback follows them rather than being quietly more capable than the platform it stands in for. Bias the surface the overlay sits on, not the overlay.

The browser can do this now. It could not before: glPolygonOffset resolves to a real address under Emscripten and used to end the process when called, so the renderer refused it and a coplanar overlay z-fought instead. The fault was a missing interop trampoline in a build that was assembled rather than published, and a published one has it. RenderInfo.Features reports the feature present. PlatformSupport.ForceBrowserFallbacks puts the old refusal back if you ever need it, and there the answer is the old one: give the overlay its own geometry a small distance off the surface.

bool DepthTest { get; set; }

Whether fragments are tested against the depth buffer. Off draws over everything already there.

bool DepthWrite { get; set; }

Whether fragments write depth. On by default, which is right for opaque geometry and wrong for almost anything blended: a transparent surface that writes depth occludes whatever is drawn after it, including the parts of the scene meant to show through.

float DetailFade { get; set; }

How far away the detail layer fades out, in metres. Zero is automatic, which is the useful setting.

Automatic means by footprint rather than by distance, and the difference matters. What makes a detail layer sparkle is its texels falling below the size of a pixel, and that depends on the window, the field of view and how obliquely the surface is seen — not only on how far away it is. A fade tuned in metres at one resolution sparkles at another. So by default the layer fades on its own texel footprint, which is the quantity that actually governs the fault.

A non-zero value is an additional artistic limit on top of that, for a caller who wants the layer gone before it would have faded anyway.

float DetailNormal { get; set; }

How far the detail field perturbs this surface's normal. Zero is off, and off is the default.

The fine end of the surface: what a tiling map cannot carry at any resolution anybody can afford. One is a strong, obviously-textured surface; a tenth is the amount that stops something reading as plastic without anybody noticing why.

float DetailRough { get; set; }

How far the detail field perturbs this surface's roughness. Zero is off.

The answer to the third of the three complaints. "Blurry, tiled, not PBR" is three faults, and the third one is almost always a constant roughness: a surface that is equally rough everywhere reads as plastic under any lighting model, because no real surface is. This costs nothing extra — the value is already fetched for Material.DetailNormal.

float DetailScale { get; set; }

How many times the scene's detail field repeats per metre on this surface. Zero takes the scene's own DetailLayer.Density.

Independent of Material.UvDensity and of Material.UvScale on purpose: the base maps and the detail layer are carrying two ends of the frequency range, and tying them together would mean a wall tiled coarsely also getting coarse detail — which is the opposite of the point.

float DetailTint { get; set; }

How far the detail field modulates this surface's colour. Zero is off.

A value of 0.5 in the field is no change, so the modulation is signed and a flat field is the identity. What it buys is tonal variation, which is most of what separates generated masonry from tile — and it is the cheapest weathering there is, because the value is already fetched.

float DetailVariance { get; set; }

How far the detail layer's roughness rises as its normal detail fades out. One by default.

The part nobody does, and the reason fading is not just switching off. Normal variation and roughness are the same quantity at two scales: a surface covered in slopes too small to see is a rough surface. So a detail normal that fades with distance must not simply vanish — the microfacet response it stood for has to go somewhere, or the surface goes smooth and plastic at range, which is the complaint the layer was added to fix, returning at a different distance.

Lost normal detail is converted into roughness instead. At full strength this is exactly today's answer; fully faded, the surface is as rough as the detail made it look. There is no distance at which the material changes character.

Zero switches it off, for a caller who wants the layer to disappear rather than to settle.

bool DoubleSided { get; set; }

Whether back faces are lit as if they faced the camera. On by default because exported CAD assemblies routinely have inconsistent winding, and thin geometry like drapes reads as black without it.

This is about shading, not about which faces are drawn — Material.Cull decides that, and the two are independent. A single-sided-looking solid is Cull = Back; turning this off instead leaves the back faces drawn and lets them go dark, which is what it has always done.

Vector3 EmissiveColor { get; set; }

Linear RGB light the surface emits on its own, added after lighting and unaffected by it. This is what makes screens, indicator lamps and hot metal read as lit in an otherwise dark scene.

TextureMapping EmissiveMapping { get; set; }

UV set and transform for Material.EmissiveTexture. Applied before shared material/node UV settings.

bool EmissiveNightSide { get; set; }

Restricts Material.EmissiveColor and Material.EmissiveTexture to the hemisphere facing away from the first directional light.

What it is for: an emissive map carrying city lights or lava has to be invisible in daylight. Unmasked, a planet's cities glow straight through its own day side.

float EmissiveNightSideEnd { get; set; }

The value of N·L at and below which the night-side emissive is fully lit.

Below Material.EmissiveNightSideStart on purpose: the mask is a smoothstep from start down to end, so the band straddles zero and the lights fade in slightly before the terminator. A band that met exactly at zero would draw a hard line across the disc.

float EmissiveNightSideStart { get; set; }

The value of N·L at and above which the night-side emissive is fully dark. See Material.EmissiveNightSideEnd for why this is the larger of the two.

TextureProgram EmissiveProgram { get; set; }

A program that computes Material.EmissiveTexture. See Material.BaseColorProgram.

The one most animated surfaces want. A screen, a terminal, a sign and a readout are light rather than surface, and every one of them was a stack of baked frames before this existed.

Texture EmissiveTexture { get; set; }

Emissive map, sRGB-encoded, multiplied with Material.EmissiveColor.

Texture MatcapTexture { get; set; }

The image sampled by the view-space normal under ShadingModel.Matcap. Ignored under every other model.

A photograph or render of a lit sphere filling the frame. The centre is the part of the surface facing the camera and the rim is the part turning away, so the useful content is a disc and the corners are never read.

MatcapTextureMode MatcapTextureMode { get; set; }

Whether the matcap takes a sampler unit of its own or shares the base colour's. MatcapTextureMode.Dedicated by default.

float Metallic { get; set; }

0 for a dielectric (plastic, wood, painted metal), 1 for bare metal. Clamped to 0..1.

TextureMapping MetallicRoughnessMapping { get; set; }

UV set and transform for Material.MetallicRoughnessTexture. Applied before shared material/node UV settings.

TextureProgram MetallicRoughnessProgram { get; set; }

A program that computes Material.MetallicRoughnessTexture. See Material.BaseColorProgram.

Texture MetallicRoughnessTexture { get; set; }

Combined metallic-roughness map, in the glTF packing: green is roughness, blue is metallic, red and alpha unused. Linear data — the renderers do not gamma-correct it, and a viewer that does gets uniformly too-glossy surfaces.

string Name { get; set; }

Optional name, carried through from a source file to make diagnostics readable.

TextureMapping NormalMapping { get; set; }

UV set and transform for Material.NormalTexture. Applied before shared material/node UV settings.

TextureProgram NormalProgram { get; set; }

A program that computes Material.NormalTexture. See Material.BaseColorProgram.

float NormalScale { get; set; }

How far Material.NormalTexture is allowed to tilt the normal. 0 disables it, 1 is as authored, above 1 exaggerates. Clamped to 0..4.

Texture NormalTexture { get; set; }

Tangent-space normal map, linear, with +Z out of the surface. Needs Mesh.Tangents; Mesh.WithGeneratedTangents derives them from the UVs when a model has none, which most exported models do not.

TextureMapping OcclusionMapping { get; set; }

UV set and transform for Material.OcclusionTexture. Applied before shared material/node UV settings.

float OcclusionStrength { get; set; }

How much of Material.OcclusionTexture to apply, 0..1.

Texture OcclusionTexture { get; set; }

Baked ambient occlusion in the red channel, linear. Attenuates only the ambient and environment terms, never the key light — occlusion darkens crevices that the sky cannot reach, and applying it to direct light double-darkens whatever the shading already handles.

Vector3 RimColor { get; set; }

Linear RGB of the rim glow. Has no effect until Material.RimIntensity is above zero.

float RimIntensity { get; set; }

Strength of the rim term, and the switch for it — zero disables the whole calculation.

The term is RimColor · pow(1 − N·V, RimPower) · (1 − RimLightBias + RimLightBias·lit) · RimIntensity, added after everything else. On a shell slightly larger than the body it surrounds, with Material.Blend additive and Material.DepthWrite off, that is a planet's atmosphere: a halo at the limb, brightest where the key light strikes it.

float RimLightBias { get; set; }

How much of the rim follows the key light, 0..1. At 0 the halo is even all the way round; at 1 it is entirely on the lit side. The default leaves the unlit limb faintly visible, which is what stops the glow ending in a hard edge at the terminator.

float RimPower { get; set; }

How tightly the rim hugs the silhouette: pow(1 − N·V, RimPower). Higher is a thinner edge.

float Roughness { get; set; }

Perceptual roughness: 0 is a mirror, 1 is chalk. Clamped to 0.03..1 rather than 0..1, because a true zero makes the GGX highlight a single sub-pixel point that aliases into flicker as the camera moves. 0.03 is visually a mirror and numerically stable.

ShadingModel Shading { get; set; }

How this surface turns light into colour. ShadingModel.Standard by default.

Vector3 SubsurfaceColor { get; set; }

What colour the wrapped light turns on its way through, as a linear multiplier. White — the default — leaves it the colour it arrived. Ignored entirely when Material.SubsurfaceWrap is 0.

The tint applies only to the light the wrap added, never to light the surface would have caught anyway. That is what makes it a scattering tint rather than a second albedo: the lit side stays the colour the material says it is, and the softened band past the terminator picks up the colour of whatever the light passed through to get there. For skin that is blood, so (0.8, 0.3, 0.2) is the usual starting point and is why the shadowed side of a nose or an ear goes red rather than grey.

It is a multiplier and not a colour in its own right, so a channel above 1 is legal and brightens the wrapped band. Nothing clamps it on the way in.

float SubsurfaceWrap { get; set; }

How far light wraps past the terminator, from 0 for a Lambertian surface to 1 for one lit all the way round. Clamped to 0..1, and 0 — the default — is bit-for-bit the shading every material had before this property existed.

This is the cheap stand-in for subsurface scattering, and it is worth being exact about what it approximates. Light entering skin, wax, leaves, milk or thin cloth does not stop at the surface: it scatters underneath and leaves somewhere else, which is why a lit face has no hard edge between its light and dark sides and why the rim of an ear glows against a lamp behind it. A Lambertian surface has a hard terminator by definition, so a head shaded with one reads as painted stone however good the geometry under it is.

What this does is widen the diffuse response — (N·L + w) / (1 + w) instead of max(N·L, 0) — so the terminator softens and light reaches round the curve. It is not a scattering solve and does not pretend to be one: there is no thickness, no radius, and no light travelling *through* a thin part. It is what almost every real-time character shader shipped before screen-space subsurface arrived, and the distance between it and Lambert is the distance between flesh and plaster.

Values worth starting from: 0.3–0.5 for skin, 0.5–0.7 for wax and thin leaves, 0.2 for cloth. Past about 0.7 a surface stops reading as lit from any particular direction, which is occasionally the point and usually a mistake.

int ToonBands { get; set; }

How many steps the light response is quantised into under ShadingModel.Toon. Three by default, which is the light/base/dark a cel-shaded hull is usually painted in.

Deliberately a count rather than a ramp texture. A ramp would be more expressive and would cost the last guaranteed sampler unit; a count plus the material's own colours covers the case that exists, and the case that exists is a consumer of this control splitting every hull into up to three meshes to get exactly this. Below two it stops banding at all, which is the honest answer to "one band".

float ToonBias { get; set; }

Slides the bands towards the light or away from it, under ShadingModel.Toon.

Zero leaves them where the light falls. Positive widens the lit areas and pushes the terminator round towards the dark side; negative does the reverse. It is the one control that decides whether a cel-shaded shape reads as bright with a thin shadow or dark with a highlight, and there is no right default for it, so the default is none.

bool Unlit { get; set; }

ShadingModel.Unlit, as the boolean this used to be.

Kept because it is what every existing caller says, and because "unlit or not" reads better than an enum comparison at the two dozen call sites that only ever wanted the one model.

Setting it false returns an unlit material to ShadingModel.Standard and leaves any other model alone. It deliberately does not write the whole model: this is a two-state view of a five-state property, and a false that meant "Standard" would make new Material { Shading = ShadingModel.Toon, Unlit = false } silently not be toon — an ordinary defensive initializer where the order of two lines decides the shading.

float UvDensity { get; set; }

How many times the texture repeats per metre under a projection. One by default; ignored under UvSource.Mesh.

Per metre rather than per surface, which is the whole point: it is the same number on a bolt and on a bulkhead, and the density comes out right by construction instead of by arithmetic at every call site. Material.UvScale still applies on top, so a material can be projected and then stretched.

Vector2 UvOffset { get; set; }

How far this material's texture coordinates are slid, after Material.UvScale.

Animating this is what a scrolling surface is: a conveyor, a waterfall, a jet's exhaust. It is the cheapest animation in the library — one vector a frame, no geometry touched, no texture re-uploaded — and its absence is why a consumer of this control ended up baking a twelve-frame flipbook of one plume, at 384 KB and half a million sin calls, to slide a texture that could have been slid.

Applied in the vertex shader, so it costs a vertex uniform rather than a fragment one and takes nothing from the light budget. See GlBackend.MeshFragmentUniformVectors for why that distinction is worth making.

Vector2 UvPivot { get; set; }

What Material.UvRotation turns about, in texture coordinates after Material.UvScale and before Material.UvOffset. The middle of the tile by default.

The rotation only.Material.UvScale is a tiling density — two means the texture repeats twice — and a density is measured from the origin, so scaling is not affected by this. Making the scale respect the pivot as well was the first version of this property and it silently moved every material that sets a scale.

Never reaches a shader. The three properties are composed into one affine map when the scene is flattened, and a pivot is two translations either side of that map — so this costs a caller nothing at run time and saves them the arithmetic that KHR_texture_transform, which rotates about the origin, makes everybody do by hand.

The middle rather than the origin because that is what a caller means. Rotating an atlas frame about the origin sends it off the sheet; rotating it about its own centre turns the picture on it. Material.UseFrame sets this to the middle of the cell it selects, for that reason.

float UvRotation { get; set; }

How far this material's texture coordinates are turned, in radians, anticlockwise about Material.UvPivot. Zero by default.

The half of a texture transform that every engine leaves out and everybody then writes by hand. glTF's KHR_texture_transform has it, Unity has no rotation at all, and Godot and three.js have one that quietly breaks normal mapping — see below.

A rotation is what breaks a repeat that the eye has already learned. A phase offset slides a pattern; the pattern is still the same pattern. A quarter-turn on every other instance of a crate is what makes forty of them stop reading as forty copies, and it costs nothing that Material.UvOffset does not.

It is applied to the sampled normal as well as to the coordinate, which is the part nobody does. A normal map's x and y are a gradient in texture space; turn the coordinate and the gradient has to turn with it or the surface lights from the wrong direction — and it reads as a lighting fault, so it gets chased in the lighting. The renderers here apply the inverse transpose of this transform to the sampled normal, which is exact and costs two multiplies.

Anticlockwise and in radians, matching KHR_texture_transform, so an imported model and a hand-set material mean the same thing by the same number.

Vector2 UvScale { get; set; }

How far this material's texture coordinates are stretched before they are sampled.

Shared settings move all maps together. Each map may first select its own UV set and transform using BaseColorMapping, NormalMapping and the corresponding mapping properties for other maps.

Two above one and the texture repeats twice across the surface, which needs the texture's wrap mode to be a repeating one to look like anything.

float UvSharpness { get; set; }

How sharply UvSource.Triplanar switches between its three projections. Four by default; ignored by every other source.

The exponent the surface normal is raised to before the three weights are normalised. One is a wide, soft blend that shows all three projections over most of a curved surface; eight is nearly a hard switch at each forty-five degree line. Four is a good default for hard-surface geometry and a lower number suits something organic.

UvSource UvSource { get; set; }

Where this material's texture coordinates come from: the mesh's own, or a projection. UvSource.Mesh by default, which is what every material has always used.

What a projection solves. Every pathology of a mesh's own parameterisation, at once. A projected material has no seam, no pole, no stretch, and no dependence on how the mesh was unwrapped — so a sphere stops wearing a sunburst at its poles, a rounded box stops wearing one on its top, and geometry generated in code needs no texture coordinates at all.

It also changes what a scale means. Under a projection the density is Material.UvDensity, in repeats per metre, and it is the same number on every surface whatever its shape or size — which is the number a consumer of this control spends two hundred and twenty-six lines computing per mesh today.

What it costs.UvSource.Triplanar samples every map three times and blends by the surface normal; the planar sources sample once. So a wall, a floor and a decal want a planar source and a pipe or a rock wants the triplanar one, and the choice is per material because it is a judgement about the shape rather than about the scene.

MappingSpace UvSpace { get; set; }

Whether a projection is fixed to the object or to the world. MappingSpace.Object by default.

Object is the safe default and the useful one. A world-space projection on a ship that flies makes the plating swim across the hull as it moves, which is the feature not working on the thing it is most wanted for. Object space travels with the geometry, so a crate wears the same patch of texture wherever it is put.

World space earns its keep on static level geometry, where it does something object space cannot: two identical walls at two positions read different parts of the texture, so a repeat breaks up for free. And after Batching.Fold merges a room's surfaces into one mesh, that mesh's object space is the room's — so folded geometry gets the world-space behaviour from this default without asking for it.

VertexAlphaMode VertexAlpha { get; set; }

What the alpha of Mesh.Colors means on this material. Defaults to VertexAlphaMode.Opacity, which is what every material did before this existed.

Events

MemberDescription
event EventHandler Changed

Raised when a TextureProgram is attached to or removed from this material, when one on it is resized, and when Material.Refresh is called.

Why this exists, and why it is this narrow. A material is the one thing in the scene graph that is not watched. A node invalidates its scene when it moves, a texture carries a Texture.Revision the renderers re-read, and a scene raises Changed for both — but a material is a plain object that several nodes may share, and mutating one has never told anybody. That was survivable while everything on a material was a value the next snapshot would pick up anyway.

A program is not that. It is a live thing that has to be found and ticked, and the order that reads most naturally — build the scene, show it, then hand a screen its program — produced a screen that stayed black for ever, because the walk that finds programs runs when the scene changes and assigning to a material is not a scene change.

So this is raised for exactly that, and Material.Refresh is the way to say the same thing about any other property a host mutates after the fact.

Methods

MemberDescription
Material Clone()

A copy, so a caller can tint or highlight one node without disturbing the others sharing it.

static Material FromColor(float r, float g, float b, float a = 1.0f)

A plain untextured dielectric of the given linear colour.

void Refresh()

Announce that this material has been changed, for hosts that mutate one already on screen.

The counterpart of Texture.Refresh, and needed for the same reason: the renderers read a material once per snapshot, and nothing about assigning to one causes a new snapshot.

void UseFrame(int columns, int rows, int index)

Points Material.UvScale and Material.UvOffset at one cell of a grid atlas.

columns

Cells across. Must be at least one.

rows

Cells down. Must be at least one.

index

Which cell, counting left to right and then down from the top-left, and wrapped — so an animation can hand it a frame counter that only ever goes up.

See also