Skip to content
VSPT

Material reference

The contract between the texture pipeline and the renderer. Owned by epic E1. The pipeline pins to a contract version.

The vanilla maps are authored by hand, texture by texture, as text sheets under materials/src/ that compile to the maps below (the vsrtx-texture-author skill in .claude/skills/ and its vsmat.py tool). Any other pipeline that meets this contract is equally valid, which is what mods and texture packs rely on.

Contract version: 1.17 (2026-10-20)

1.17 makes glass with no material of its own clear. A glass texel whose texture brings no maps and no roughness rule takes roughness 0, whatever its face’s gloss label, instead of the fallback table’s 0.85: honoured since 1.16, that made a mod’s glass without maps frosted, blurred and hiding what moves behind it from Ray Reconstruction, where vanilla draws it clear (Chris, 2026-10-20). Glass that brings maps is unchanged: its roughness is its _m’s.

1.16 adds diffuseTransmission, the share of what a scattering glass texel’s body scatters that goes on through it, so opal, milk glass, alabaster and onyx glow when lit from behind (glass.md, “Light through glass bodies”). Additive: without it the body is lit from the side it is seen from, as before, which keeps lake ice as it was. The renderer also honours what the contract has always said of roughness on a glass texel, from 0.1 up (“Rough glass”).

1.15 makes the refractive index, the body, the absorption and the flicker the material’s, read from its rules and honoured by the renderer, where they had been named here and dropped (Chris: “refractive index should be able to be specified as part of the material, not hard coded in the engine based on block type”). ior is a glass texture’s index, 1.0 to 3.0, 1.5 when it gives none; solid true makes glass a body that bends light in and out, false a pane whatever the players’ Solid glass setting says, and with neither the setting decides; absorption is glass’s colour by distance through a body, or through a whole pane, in place of its texture’s tint (Chris: “Implement absorption too”); emissiveFlicker swings an authored emitter’s light. Glass whose texture authors emission is listed as a light from that emission, not only by its glow flag, and a texture whose emissiveNits is under 1 is not listed at all (glacier ice). A glass film is a decor block of glass whose shape lies in the plane of the face it is on (builders-pack.md, R1 to R4).

1.14 keeps vanilla’s painted light and dark. A dielectric is no longer lifted to a mean of 0.04: the floor answered a path tracer’s missing ambient term and an average exposure meter that brought dark scenes back to grey, and the incident meter and the fading of a near-black surface’s sheen answer both now, while the floor lifted basalt, bookshelves, clay bricks and some 550 other textures up to twice past vanilla (Chris). And flatten (f) is held to 0.1: authors had pulled the painted top light and bottom shade of bricks and stones to one tone (up to 0.85) for a height step to bring back as real light, which left aged ashlar with 77% of vanilla’s contrast and cracked ashlar with 64% (“same issue we had with wood”). A sheet may still ask for a floor (albedo_floor).

1.13 does for metals what 1.11 did for dielectrics: a metal’s F0 floor of 0.5 is on the MEAN of the texture’s metal texels, reached by one gain for all of them, and its painted darks stay. The per-texel floor mapped each luminance tier of a metal onto 0.5 by its own gain and offset, which kept the median metal texture at 42% of vanilla’s contrast (Chris: “lost a lot of their contrast and detail”). It also asks for one finish per metal body: painted grime, seams and tone variation are base colour on the same metal, not dielectric or polished patches, which on a model that samples parts of the texture (a hopper) read as “large cow spots of different specularity”.

1.12 moves a mod’s maps out of textures/: a mod or texture pack ships them under assets/<domain>/materials/textures/, because vanilla block types name textures with wildcards and a map beside its texture becomes a variant of the block (decision 0007, modding-materials.md). It makes the roughness and metalness scalars work, accepts 16-bit PNGs (read to 8 bits), and adds one rule of precedence: a map or rule applies only to the picture it was made for, so a texture pack’s repaint drops those that loaded before it.

1.11 narrows the dark end of rule 6 for dielectrics: a material’s MEAN must reach 0.04, not each texel, and a material already there keeps its art as painted. The per-texel floor and the flat albedo targets lifted soils and wood far above vanilla and drained them (Chris: “quite bright and dry”, “washed out and pale”). Painted darks within a material stay: they stand for occlusion a flat face cannot produce. Metals are unchanged.

1.10 gives a scattering texel’s translucency value a meaning: how strongly what passes its body is filtered by its colour, lerp(white, base colour, t), once per crossing. It is what lets resin be amber and a salt lamp glow orange rather than show what is behind them untouched. Vanilla’s partial alpha in the transparent pass is meant as translucency (Chris), so the translucent solids drawn there (resin, crystals, salts, the salt lamp, oiled hide, nets) are scattering texels now, not clear panes.

1.9 makes a scattering texel a smooth interface over its scattering body, rather than coverage alone: the interface reflects and refracts like glass (shiny, and bent by the texel’s normals), and the body takes the share alpha of what gets in. 1.8’s version, where the uncovered share passed untouched, was noisy under Ray Reconstruction and had no shine (Chris). It also says that a glass texel’s _n bends the view through a pane, which the contract always meant and the renderer now does.

1.8 adds scattering texels to the glass class: a glass texel whose _m carries translucency is not an interface but a scattering body, and its alpha is coverage, as in vanilla’s blend (see “Scattering texels” under Glass). It is how lake ice becomes an even, lit, partly see-through sheet. Additive: no glass texel authored before 1.8 carries translucency, which the glass class ignored until now.

1.7 says two things about the glass rule that ice exposed. The rule reads the texel at full resolution: a minified alpha must not turn an opaque texel into glass, or glass into nothing. And a pack’s base colour may ship an alpha that differs from vanilla’s, which is how a texture drawn in the transparent pass is made an opaque surface; lake and glacier ice now are (see “Ice is not a pane” under Glass). Additive: nothing authored against 1.6 changes meaning.

1.6 says plainly where a flame’s emission comes from, because candles and lanterns were lighting nothing at all. For most of the game’s small light sources the flame is not painted into the texture: it is a face with a glow flag in the shape, a few centimetres across, sampling a degenerate UV, so the texture’s emissive mask never gets a look in. See Emission from the shape’s glow flag.

1.5 lets glass be clear: in the glass class roughness 0 is allowed and means a perfectly smooth interface, exempt from the renderer’s 0.045 floor, and distortion belongs to the normals. See Glass.

1.4 adds Emitter calibration: the block’s own light level scales the emission of the texture it draws. Without it a shared emitter texture is as bright in a lantern as in a candlestick, and the game says a lantern is nearly three times the light. Additive: a texture drawn by only one block, at the level its emissiveNits was chosen for, is unchanged.

1.3 fixes the composite threshold: an overlay whose alpha never reaches 0.5 contributed no material at all, which silenced four families the game really draws (see Composite textures). Additive in effect: every overlay whose peak alpha is 0.5 or more behaves exactly as in 1.2.

1.2 adds the glass rules (texel alpha in the glass class, fallback tint, the solid scalar). Additive: nothing authored against 1.1 changes meaning.

1.1 makes the contract HDR-native (hdr.md): absolute emission, a perceptual emissive mask, emitter colour outside Rec.709, flicker, and explicit colour spaces. The emissive mask change reinterprets a channel, which by the rule below would be a major version; it is 1.1 only because 1.0 was superseded the day it was written and no pack was ever built against it.

Changes after 1.0: additions that old packs satisfy unchanged bump the minor version. Anything that reinterprets an existing channel, field or default bumps the major version. A pack declares the version it targets (see Material files).

Low-resolution pixel art under fully modern light transport. The reference point is the 2023 System Shock remake: chunky, unfiltered texels that are plainly visible up close, lit by smooth, high-resolution lighting, shadows and reflections.

Two rules follow, and everything else in this document serves them.

  1. Material inputs are per texel. Base colour, normal, roughness, metalness, emission and translucency are constant across a texel and change abruptly at its edge. Nothing in the material is ever bilinearly filtered under magnification.
  2. Light is not per texel. Shading is evaluated at the true hit point. Shadows, penumbrae, reflections and indirect light cross texel boundaries at output resolution. Lighting is never snapped to texel centres and never quantised. Two looks a player may choose do it on purpose: texelated caustics (water.md, “Caustics”) and texelated shadows (lighting.md, “Texelated shadows”; the setting Shadows, smooth by default). Everything else keeps the rule.

So a texel is a small flat facet with its own orientation and finish: a torch highlight steps across a granite wall texel by texel, while the shadow of the torch holder falls across the same wall as a smooth curve.

Companion maps are found by filename suffix, in any asset domain. A mod or texture pack puts them under assets/<domain>/materials/textures/, mirroring textures/ (materials/textures/block/granite_n.png for textures/block/granite.png), never beside the art: the game would take a PNG in textures/ for a texture, and a block type that names its textures with a wildcard would draw it. The renderer’s own pack, compiled at deploy, keeps them beside each other inside the pack, where the game never looks.

File Channels Encoding
granite.png RGB base colour, A coverage sRGB, straight alpha. De-lit (see below)
granite_n.png RGB tangent-space normal, A height linear, 8 bit
granite_m.png R roughness, G metalness, B emissive mask, A opacity to transmission linear, 8 bit
  • All maps are 8-bit PNG (a 16-bit one is read by its high byte), the same resolution as the base texture and texel-aligned with it. No higher-resolution companion maps: one base texel, one material.
  • A base texture whose own name ends in _n or _m is not supported.
  • The files on disk are 8-bit and stay that way under HDR. Base colour is a reflectance, bounded by 1, and each texel is flat, so there is no gradient to band. The HDR range lives in the emission scalars and in the renderer’s float buffers, not in the maps.
  • A missing _n means flat normal and height 0.5. A missing _m means the fallback. Each is independent of the other.
  • Alpha is coverage. For opaque it is ignored. For cutout and thin it is a binary test at 0.5; there is no partial coverage in the tracer. The glass class reads alpha differently: see Glass.
  • For metals (metalness 1) the base colour is the specular colour F0.
  • On disk it is sRGB. The renderer’s working space is linear Rec.2020 (PLAN D10); the loader decodes the sRGB curve and converts primaries once, at load. _n and _m are data and are never colour-converted. Every limit on base colour in this document (the 0.9 clamp, the dielectric and metal ranges) applies to the linear value, which the conversion of primaries changes only slightly.
  • Tangent space, +X along U, +Y up (OpenGL convention), +Z out of the surface.
  • Decode is n = (v - 128) / 127, clamped to [-1, 1], then normalised. Value 128 is exactly zero, so (128, 128, 255) is exactly flat.
  • The facet must face outward: decoded z >= 0.5 (at most 60 degrees of tilt). The renderer clamps steeper normals; the validator rejects them.
  • Height is (A / 255 - 0.5) x 4 texels, so +-2 texels of relief around the surface at 128. Units are texels, so the meaning does not change with texture size. A PNG without an alpha channel means height 0.5 everywhere.
  • Height is data for the pipeline’s own normal derivation and is reserved for the renderer. The 1.x renderer does not displace or parallax: silhouettes and block edges stay exactly where the engine’s geometry puts them.
  • R roughness is perceptual; GGX alpha = r^2. The renderer clamps to [0.045, 1].
  • G metalness is a per-texel choice, not a blend: a texel is rust or it is iron. Use 0 or 255. Intermediate values are legal, are interpolated between the two lobes, and draw a validator warning when they cover more than a few percent of a texture.
  • B emissive mask is perceptual: emitted luminance scales with mask^2. An emitter texture spans a range no 8-bit linear channel can hold (a 10,000-nit flame core next to a 5-nit afterglow); squared, the smallest step is 1/65,025 of the peak. See Emission.
  • A is opacity to transmission: 255 transmits nothing, 0 transmits everything. Translucency = 1 - A. It is inverted on purpose, so that an _m saved without alpha, or with the opaque alpha most image editors add, is a valid non-translucent material. Translucency is thin-surface diffuse transmission for leaves, cloth and paper, and is only evaluated for the cutout and thin classes. It is not glass.
Case Rule
Magnification (texel larger than a pixel) Nearest, all maps, always
Minification (texel smaller than a pixel) Box-filtered mip chain, level chosen by ray cone, nearest within a level, linear between levels
Secondary rays Same, with the cone widened by path roughness
  • The renderer builds mips from level 0; the pipeline ships level 0 only.
  • Every level is a whole-texel 2x2 box average, so a mip is still pixel art, at half the density. Odd sizes (48, 96) stop at the last even division.
  • Normal mips average the vectors and renormalise, and the lost length is folded into roughness (Toksvig), so a rough-hewn wall stays matte at distance instead of turning glossy. Metalness and translucency average linearly; emission averages as emitted luminance (mask^2) and is re-encoded, so a distant torch keeps its total output. Base colour averages in linear light, weighted by coverage; coverage mips keep the 0.5 test’s area.
  • Level 0 is never blended with level 1 while a texel is at least one pixel wide. Up close, the texel grid is always crisp.
  • Temporal and spatial filters downstream (E7) demodulate base colour and use per-texel normals as edge stops, so denoising and upscaling do not soften texel edges.

Per-texture scalars, from material files:

Field Default Meaning
class from render pass opaque, cutout, thin, glass, liquid
roughness, metalness from fallback Constants used when there is no _m map. Ignored when there is one. A rule’s roughness wins over the face’s gloss label
emissiveNits from glow level Absolute scene luminance in nits at mask 1. Not a display value and not a multiplier: a torch flame is around 10,000
emissiveColor none [r, g, b] in linear Rec.2020, any scale; the loader normalises it to luminance 1. May lie outside Rec.709
emissiveKelvin none Blackbody temperature, shorthand for an emissiveColor on the locus. Fire, embers and lava belong here, and below about 1,900 K that is outside sRGB. Give one of the two, not both
emissiveRefLevel none The lightHsv level (0-32) of the block emissiveNits was chosen for. Set it on any texture more than one block draws, or whose block has a lightHsv. See Emitter calibration
emissiveFlicker none { "amplitude": 0-1, "hz": rate }. The renderer modulates emissiveNits by up to the amplitude with smooth noise around that rate, the same phase for a whole block. Read since 1.15; rates are held to 30 Hz
ior 1.5 (liquid: 1.33) Glass only, 1.0 to 3.0; read since 1.15. Liquids keep 1.33: the water’s caustics and its window are made for that index
absorption none { "color": [r, g, b], "distance": metres }: transmitted colour after that distance, linear Rec.2020, 0 to 1 each. glass and liquid only. On thin-walled glass, see Glass. Read since 1.15 for glass; a liquid’s is not read, water’s absorption being its own (docs/water.md)
diffuseTransmission 0 glass only, 0 to 1: for a scattering texel (below), the share of what its body scatters that goes on through it, so the body glows when lit from behind. Opal and milk glass about 0.5, alabaster 0.4. Read since 1.16; ice and resin leave it out
solid false for glass, always true for liquid true: the block is a filled volume of the medium, light is absorbed over the distance travelled inside and refracts on entry and exit. false: thin-walled, a pane with no thickness, no refraction offset. Read since 1.15: given, it wins over the players’ Solid glass setting; not given, the setting decides. A solid absorbs by distance only where absorption is authored; without it, its colour is its faces’ tint

Vanilla glass alpha is a blend strength, not coverage (plain.png is about 0.2 to 0.3 everywhere, leaded.png adds alpha-1 lead lines, frame.png is binary), so in the glass class a texel’s alpha selects what the texel is:

Base alpha The texel is
>= 0.999 an opaque surface (lead came, frame, muntin), shaded with the texture’s maps like any opaque texel
< 0.02 not there (the holes of a frame texture)
between glass: a dielectric interface with ior, tinted as below; or, if the texel’s _m carries translucency, a scattering texel (1.8, below)
  • Tint without authored absorption: each glass face filters transmitted light by lerp(white, base colour, alpha) in the working space, whatever the thickness. Two faces make a pane, so a pane passes the square of that. This is what vanilla’s per-face blend amounts to, so untouched glass keeps its vanilla colour strength.
  • Authored absorption: with solid true it is physical, color after distance metres, by Beer-Lambert along the path inside, and the faces only reflect. With solid false there is no distance to speak of: color is what one whole pane passes at normal incidence (both faces together, so the square root of it at each) and distance is ignored. Either way it replaces the texture’s tint, for the whole texture: a stained glass sheet of many colours leaves absorption out. With solid not given, the players’ Solid glass setting decides which of the two it is, so one block of a pane and of a body come out alike when distance is 1.
  • Thin-walled is the default for glass, because a vanilla window is a full 1 m block and a filled metre of tinted glass is nearly opaque. solid: true is for crystal blocks, or anyone who wants a prism (not for ice: see below).
  • On glass texels _m roughness is the roughness of the interface and _n tilts it (the renderer bends the view through a pane like a thin prism, front facet over a flat back: about (ior - 1) x the tilt). The renderer honours the roughness from 0.1 up, and traces the interface flat under it; metalness and translucency are ignored. Opaque texels in the same texture use all channels as usual, so leaded glass is authored in one sheet.
  • Clear glass is clear. Author it at roughness 0, which on a glass texel means a perfectly smooth interface: the 0.045 floor does not apply here, and a smooth interface is specular transmission, not a narrow GGX lobe. Roughness on glass scatters what you see THROUGH it, and a pane is a whole metre thick in this game, so even 0.05 reads as haze and 0.2 as fog. Vanilla’s window glass is clean glass and gets 0.
  • Distortion belongs to the normals, not to roughness. Old, hand-made or bubbled glass bends what is behind it without frosting it: that is _n, gently, at a low normal_strength (0.2 to 0.4), which the pixel art’s own unevenness already gives you. Reach for roughness only where the art really draws frosting, dirt or a sand-blasted face, and even then stay low (0.10 to 0.20). A texture merely called “smoky” or “vintage” is usually tinted or uneven, not frosted: look before deciding.
  • The rule reads the texel, not a mip. Whether a texel is opaque, glass or absent is decided from its alpha at full resolution, whatever level the ray cone reads the colour at. A box mip averages the lead of a leaded window, or the opaque texels of partly clear ice, into glass, and the frame’s holes into faint glass: at a distance the came would fog and the ice would vanish. Classify from level 0 (a point sample there, which the frame’s jitter integrates into the right mix); the tint and the maps may be minified as usual.
  • A pack may replace the alpha. The pack’s base colour PNG replaces the atlas texel whole, alpha included, so a sheet can ship an alpha other than vanilla’s (the sheet format’s o token). That is the only way a texture the game draws in the transparent pass becomes an opaque surface.
  • Scattering texels (1.8, 1.9). A glass-class texel whose _m alpha is under 1 (it carries translucency) is ice: a smooth interface over a body full of bubbles and fractures. The interface is exactly a pane’s (Fresnel reflection and transmission, index 1.5, thin-walled, its facet from the texel’s _n), so the ice shines and what is seen in it and through it is bent. Of what the interface lets in, the body takes the share alpha, which is what vanilla’s blend means, and scatters it as an opaque, matte surface of the texel’s colour; the rest goes on through, filtered by lerp(white, base colour, t), where t is the texel’s translucency (1.10: 0.15 for lake ice, 0.9 for resin). The body is the ice’s volume, so a ray meets it once, on the way in, not again where it leaves (lake ice’s far face is drawn, against the water). A shadow ray keeps (1 - F) per face, times (1 - alpha) and the tint on the way in. Because alpha is a share and not a tint, a minified alpha is simply the averaged share and it holds at every distance. _m roughness does not apply (the interface is smooth, the body matte). Alpha still bounds it: under 0.02 nothing, 0.999 and over an opaque surface. Since 1.16 a material’s diffuseTransmission lets the body pass that share of what it scatters on through it, so a lamp or the sun behind it lights it; without it the body is lit only from the side it is seen from. The guides for a denoiser describe the ice itself, every frame.
  • Vanilla’s partial alpha in the transparent pass means translucency. Real glass is a clear pane (the rule above). A translucent solid (resin, a crystal, salt, oiled hide) is authored as scattering texels, with its body’s share (the base alpha, set with the sheet’s o token when vanilla’s does not suit) and its tint (t) chosen for what it is. Soft decals whose alpha is a wash (writing on a clay tablet, grime) stay panes: their dark tint already reproduces vanilla’s blend.
  • Ice is not a pane. Vanilla draws lake and glacier ice in the transparent pass at a blend alpha (0.58 and 0.70) that stands for how much of what you see is the ice’s own scattered light. As a clear thin pane that alpha is only a faint tint, so the ice vanished; and a glacier, whose faces between ice blocks are culled, became a hollow shell. Real ice scatters from its bubbles and fractures. So lake and glacier ice ship at alpha 1: an opaque surface whose smooth face gives the sheen and whose diffuse albedo stands in for the scattering, at the brightness vanilla’s blend shows. Packed glacier ice was already opaque in vanilla. Since 1.8 lake ice is a scattering texel at vanilla’s alpha instead (an even, lit, partly see-through sheet); glacier ice stays opaque, because the faces between glacier blocks are culled and any coverage below 1 would show through the whole glacier. A per-texel mix of opaque and clear ice texels is legal but reads as noise; use scattering.
emitted radiance = colour x mask^2 x emissiveNits (linear Rec.2020, nits)
colour = emissiveColor or emissiveKelvin, normalised to luminance 1, if given
= the texel's base colour, otherwise
  • With the default, the pixel art’s own hues carry through (a flame that is drawn pale at the core and red at the edge stays that way), but the colour can never be more saturated than sRGB, and a white texel emits exactly emissiveNits. A dark-coloured flame therefore emits a share of it (a torch’s dark red, 8.5 %), so our authoring tool writes emissiveNits as the sheet’s figure over the emitters’ mean base luminance, and a sheet’s emissive_nits is what its flame emits on average (decision 0024). The pack’s value keeps the meaning above.
  • With emissiveColor or emissiveKelvin, the scalar replaces the texel’s colour and the mask alone shapes the emitter. This is how an emitter leaves Rec.709. The base colour still describes the surface when it is lit by something else.
  • Emission is not clamped and not tone mapped; display mapping happens once, at the end of the frame (hdr.md). The 0.9 clamp applies to reflectance only.
  • The block’s lightHsv still drives the gameplay light grid. It is used for emission colour only by the fallback. E1’s emitter calibration sets emissiveNits so that traced brightness tracks the light level.
  • Flicker is in the contract because temporal filtering (E7) has to know a change in brightness is intended and not noise.
  • Several colour temperatures in one texture (2026-10-12, lantern-look). The scalar colour is one per texture, so a texture whose emitters need different temperatures (block/candle: the candles’ flame at 1,900 K and the lantern’s at 4,000 K) gives no emissiveKelvin and ships each emitter’s blackbody colour as its base colour, the default above; the authoring tool’s k legend token does it, raising the mask so that emitted luminance is unchanged. Nothing new is read: it is the base-colour rule, and Rec.709’s limit on it applies (below about 1,900 K the locus leaves sRGB, and the blue channel clips to 0).

A texture’s emissive mask only works where the artist painted the flame into the texture AND the shape samples it. That is true of a torch head. It is not true of most small light sources:

Face UV it samples (sixteenths) glow
candle flame, bunch of candles [0, 0, 0, 0.5] (zero width) 196
lantern flame, small [0, 0, 0.5, 0.5] (one texel) 255
lantern flame, large [0, 0, 0, 0.5] (zero width) 255
lantern glass panes real rectangles 255

A candle’s flame element is 0.4 x 0.5 x 0.4 sixteenths, about 2.5 cm, and its faces sample a degenerate strip at the texture’s origin. What makes it look like a flame in the vanilla renderer is the glow flag on the face, not pixels. So:

  • A face’s glow flag is an emissive mask in its own right, sqrt(glow / 255), and the renderer takes max(texture mask, sqrt(glow / 255)) per texel, as the emissive channel already said for runtime glow. This is the primary path for candles, lanterns, chandeliers and anything whose flame is a glow-flagged sliver.
  • A texture drawn by a glow-flagged face still needs emissiveNits, emissiveKelvin and emissiveRefLevel, even where its own mask is zero, because the glow gives the shape of the emission and the scalars give its power and colour. A texture with no scalars under a glow-flagged face has nothing to be bright with.
  • A shared texture cannot carry the power. block/glass/quartz is a lantern’s pane, but also a plain quartz-glass block, a bookshelf’s bottles and a pump head, none of them lit. Emissive scalars on the texture would light all of them. So where a glow-flagged face’s texture has no emissiveNits, the power comes from the block instead: total luminous flux F = F_ref x (level / 14) ^ 2, spread over that block’s glow-flagged faces, with level the V of its lightHsv and F_ref the flux of a torch (start at about 100 lumens and set it against a render). Flux rather than luminance, because the renderer knows the area and the author cannot: the same rule then lights a 2.5 cm flame and a whole lantern pane correctly.
  • A lantern’s glass panes carry glow 255 in vanilla, and that glow is not emission (decision 0023, 2026-10-11). Vanilla needs it because it has no light transport: a glowing pane is how it draws glass with a flame behind. Read as emission it made the pane a light panel a hundred times the flame’s area, paler than a flame and brighter than the flame seen through it (“overbright”, “LED lamps”, “you can’t see inside”). So the mod clears the flag on a lantern’s glass before the renderer sees it, and the lantern’s light is its flame box, seen through clear glass. Glowing glass that is not a lantern’s (the devastation’s temporal cylinders) still emits by the rule above. This reverses what this bullet said from 1.6 to 2026-10-11, that the pane is the emitting area.
  • A texture that must not glow under a glow flag declares emissive scalars with no emissive texel. A zero mask then wins over the fallback. The candle’s wax and the small lantern’s candle cup (block/metal/lantern/grid/*, glow 100) are such textures.

A texture’s emissiveNits is the luminance of that surface as drawn. But several blocks draw the same emitter texture at very different powers: block/candle is the flame of a candle (light level 7), of a chandelier (10 to 14), and of a lantern (18 small, 20 large); block/fire is the flame of an oil lamp (11) and of several other things. Gameplay reads the light grid, so a spot the grid calls bright must look bright (PLAN, “look drift”).

So the block’s light level scales the emission of every texture it draws:

emission = colour x mask^2 x emissiveNits x (level / reference) ^ 2

level is the V of the block’s lightHsv (0 to 32), and reference is the level of the block the texture’s emissiveNits was chosen for, declared per texture as emissiveRefLevel. Where a texture has no emissiveRefLevel, or the block has no lightHsv, the scale is 1.

The exponent is 2 because the voxel light grid loses one level per block, so a level L source lights to about L blocks, and a point source reaching a fixed threshold at radius L has intensity proportional to L^2. Relative to a torch (14): a large lantern is 2.0, a paper lantern 2.3, a lit firepit 1.3, an oil lamp 0.6, a bare candle 0.25.

This is a scale on the emitter’s power, not on its look: which texels glow, their relative brightness and their colour are the texture’s, and stay the artist’s. A block whose light the grid does not model at all (lava, an ore that only looks warm) is unscaled.

The exponent and the anchor are a first calibration, not a measurement, and E1 owes a render to check them. One case shows why: a lantern’s flame is block/candle, twenty texels of it, and the game makes a large lantern level 20 against a bare candle’s 7. The rule then asks those twenty texels for about 65,000 nits, six times a real candle flame. That is self-consistent (the same small flame must carry six times the light) but it says the game’s lantern is brighter than its drawn flame can physically account for, which in a real lantern is explained by a reflector and a larger flame. If a calibrated lantern reads as a hot speck rather than a lamp, the answer is not to abandon the rule but to move the emission to the area that really glows: a lit lantern’s glass panes, not the flame behind them. Decide that against a render, in a cave, not on paper.

Decided 2026-10-11 the other way (decision 0023): the panes do not emit, and a placed lantern’s flame faces are moved from the candle’s texel (0,0) of block/candle to its own, (1,1), the core, made 2.4 times larger, at 530,000 nits and 4,000 K, about 36,000 lumens (over a torch’s, at Chris’s word, 2026-10-12); a candle’s stays at (0,0), about 11 lumens at 1,900 K. emissiveRefLevel is still not implemented: the texel move is how a lantern’s flame differs from a candle’s.

Default class from EnumChunkRenderPass:

Render pass Class
Opaque, TopSoil opaque
OpaqueNoCull, OpaqueWaterPlant, Decor cutout
BlendNoCull thin
Transparent glass
Liquid liquid
Meta not rendered

Scalars ride on the normal asset system, so mods and texture packs supply and override them by the usual domain and priority rules.

  • Location: assets/<domain>/materials/*.json. Schema: material.schema.json.
  • Each file is { "contract": "1.1", "materials": { "<pattern>": { ...scalars } } }.
  • A pattern is a texture path relative to textures/, without extension, in the file’s own domain. * matches within a path segment, ** across segments.
  • All matching entries apply, least specific first (fewest literal characters), so block/metal/** can set a family default and block/metal/tarnished/rusty-iron can refine it. Ties resolve by asset priority (the game’s load order), then file name.
  • A map or rule applies only to the picture it was made for. When an origin later in the load order ships different pixels for a texture (a texture pack’s repaint), the maps and rules from earlier origins, the renderer’s own included, do not apply to it; those from that origin or a later one do.
  • Per-block override: attributes.vsrtxMaterial on a block, item or entity type, an object keyed by texture code ("all", "north", "metal", …) holding the same scalars. It applies after all material files. Not implemented yet (1.12).

Hand-off layout for a generated pack, which is an ordinary asset folder or mod zip:

assets/<domain>/materials/textures/block/stone/rock/granite1_n.png
assets/<domain>/materials/textures/block/stone/rock/granite1_m.png
assets/<domain>/materials/generated.json

A pack never needs to ship or modify a base texture unless it de-lights it.

The engine builds textures from parts at load time (base++overlay, blend modes, @90 rotation, alpha, alternates, tiles). The loader applies the same operations to the companion maps, with these rules. The principle is rule 1: a texel ends up with exactly one material, never a mixture of two.

Operation Base colour _n and _m
Alternates, tiles Separate PNG each Each file carries its own maps. A variant without maps uses the fallback, never a sibling’s maps
Overlay, blend mode Normal or OverlayCutout Engine blend Per texel: the overlay’s maps where the overlay’s alpha is at or above its replacement threshold, otherwise what is underneath. An overlay without maps leaves the material underneath untouched
Overlay, any other blend mode (Multiply, Darken, Screen, …) Engine blend Untouched. These are tints and stains, not new surfaces
Rotation @90, @180, @270 Texels rotated Texels rotated, and the normal’s XY vector rotated by the same angle
Alpha (@a parameter) Applied Untouched
TopSoil per-vertex grass blend (runtime) Blended in the hit shader Grass maps where blend weight x grass alpha >= 0.5, otherwise soil

The replacement threshold of an overlay is min(0.5, max(0.08, peak / 2)), where peak is the highest alpha in that overlay texture. For a drawn decal with solid texels (moss, lichen, an ore’s crystals) the peak is 1 and the threshold is the plain 0.5 of 1.2. For a soft wash the artist never took above, say, 0.4 (the cracked-rock decal peaks at 0.42, footsteps at 0.25, light dust at 0.40, sylvite dusting at 0.40), a fixed 0.5 would mean the overlay changed nothing at all, and four families the game draws would have no material of their own. Half of the artist’s own peak is the strongest part of what they drew.

The threshold governs the material only. Base colour is always the engine’s own blend, so a 40% dust wash still tints the colour by 40%; what changes at the threshold is which material the texel is made of, because a texel has exactly one (rule 1). A thin film of dust or soot does take over a surface’s microstructure even when it barely changes its colour, so this is the physical answer as well as the practical one.

Runtime-generated textures:

  • Chiselled and microblock geometry references the source block’s textures, so it inherits their materials with no special case.
  • Textures rendered at runtime (sign and book text, painted or dyed surfaces, map pieces) have no companion maps. They take the scalars of the texture they are drawn onto and a flat normal. Ink does not change the finish.
  1. De-light the base colour. The source art has shading and AO painted in; a path tracer would shade it twice. Move painted shading into height and normal. Keep the palette: de-lighting removes the light, not the artist’s colour choices.
  2. Author per texel. Every value is decided for one base texel. Do not generate at a higher resolution and downsample with filtering; that produces exactly the intermediate normals and half-metal texels this contract is written to exclude.
  3. Texel-exact alignment of _n and _m with the base, or cutout edges sparkle.
  4. Coherent facets. Normals should describe the forms the pixel art draws (a brick’s bevel, a plank’s grain, a rivet), with flat interiors. Per-texel noise reads as glitter under a moving light. Reserve it for surfaces that are meant to glitter.
  5. Use the existing labels as priors. Texture path families (block/metal/**, block/stone/gem/**), face reflectiveMode and face glow mark what the artists meant to be metal, gem, glossy or lit. See the audit below.
  6. Energy. Base colour after runtime tint is clamped to 0.9 by the renderer, in the linear working space. Keep untinted dielectric base colour within 0.04 to 0.9 linear, and metal F0 within 0.5 to 1.0 linear. The dark end, since 1.11: for a dielectric the floor was on the texture’s dielectric mean, by one gain for all of it (so a dark seam inside a plank is not lifted on its own, which greyed the seams). Since 1.14 there is no floor: a dielectric is left exactly as painted, however dark, unless its sheet asks for one (albedo_floor, by the same single gain). The exposure meter reads the light, not the material, so a dark material stays dark as in vanilla, and a near-black surface loses its sheen (below). Dark texels stay: the crevices between planks and the shadowed crumbs of soil stand for occlusion that a flat face cannot produce, and lifting them one by one (1.0 to 1.10 did) washes the material out. Real figures, for judging: 0.04 to 0.06 wet black humus, 0.05 to 0.08 peat, 0.04 to 0.05 charcoal, 0.08 to 0.15 rust, 0.06 to 0.10 dark wet wood. Metals, since 1.13, the same way: the texture’s metal texels are scaled together, by one pure gain, until their mean reaches the library target (about 0.52, real iron to copper), so painted dark metal stays darker than its neighbours: it stands for grime, a seam or a shadowed hammer mark, as a plank’s crevice does. The brightest 1% stay at or under 1.0, which may hold the mean lower. Never clamp. A dielectric reflects 4% at normal incidence whatever its colour, which on near-black ground outshines the surface. The renderer therefore takes a dielectric texel’s F0 down below a base colour of 0.04, with the square of its luminance (0.01 at 0.02, 0.002 at 0.009), and its grazing reflectance with it: porous dark materials, ash, charcoal, burnt soil, reflect far less than 4% (DielectricF0 in path_tracer.hlsl, 2026-10-02). The grazing reflectance is F0 / 0.04, at most 1, since 2026-10-13 (it was 50 x F0): a dielectric whose F0 is taken down, by its darkness or by a player’s shine setting, has its whole reflection scaled, head on and grazing alike (decisions/0028). The shine settings are the player’s, not the contract’s: a pack authors as before. A metal body has one finish: one roughness (fine tiers within about 0.05 that follow the texel detail are fine) and metalness 1. Grime, tarnish tones and seams the artist painted are darker base colour on that metal. A different finish needs visible structure of its own: another material (a wooden handle, a leather strap, paint, a gem), or rust whose colour shows it is rust. A tone patch made into a patch of different specularity reads as a spot, worst on a model that samples only part of the texture.
  7. Emitters are absolute. Remove painted glow and halo from the base colour around an emitter: the tracer produces the real one. Mark only the texels that are the light source, shape them with the mask, and give emissiveNits as a real luminance.
  8. Determinism. Same input, same output, so modded texture packs can be regenerated.

The E1 validator enforces 3, 6, the _n decode limits, the metalness warning and file completeness, and runs in CI for any pack.

  • Climate and season tint multiply the base colour in the hit shader (grass, leaves).
  • TopSoil blends a grass overlay over the soil texture per vertex.
  • Wind moves foliage vertices; see E2 for how the scene layer handles it.
  • Glow can be dynamic: some entities raise their glow level at runtime. The renderer uses max(emissive mask, sqrt(runtime glow / 255)) as the mask, so a runtime glow still works on a texture whose _m has no emission.
  • Heated metal does not go through the glow level or the mask at all. Vanilla sets it as a shader uniform, and here the pose carries the temperature and the hit shader adds blackbody radiance at it, at emissivity 0.6, on top of the material’s own emission. No map needs to author it (hot-metal.md).

Used for modded content and for M1 bring-up, before any generated maps exist. Evaluated only when there is no _m map and no scalar from a material file.

The engine’s labels are per face, not per texture: reflectiveMode and glow are set on shape element faces and reach the renderer in the vertex flags (reflective bit 11, mode in bits 29-31, glow in bits 0-7; block types can also set vertexFlags.reflective with no mode, which reads as weak). The scene layer decodes them (E2) and the fallback is resolved per face.

Roughness, from the gloss label:

Face label (JSON value) Roughness
none 0.85 (metal family: 0.55)
weak (1), or reflective: true with no mode 0.5
mild (5) 0.6
medium (2) 0.4
strong (3) 0.25
sparkly (4) 0.15

The table is for surfaces. A glass texel takes roughness 0 whatever its label (1.17): on glass roughness blurs what is seen through it (Glass), and vanilla draws glass clear, so glass with no maps stays as the game shows it.

Metalness, from the texture path, in any domain: 1 if the path under textures/ has a metal segment (block/metal/**, item/tool/shield/metal/**, …), otherwise 0. This rule ships as an ordinary material file, so it can be read and overridden like any other.

Emission: mask = sqrt(glow / 255), so that emitted luminance stays proportional to the glow level under the squared decode. emissiveNits comes from the E1 emitter calibration in absolute nits. Colour is lightHsv converted to linear Rec.2020 and normalised when the block has one, otherwise the base colour. No flicker.

  • reflectiveMode appears on about 1,500 faces, but in only 16 shape files: ingots and ingot piles, two metal tablets, a trader’s mirror, a crystal skull, crystallised ore items, a quartz layer, a shiny voxel layer, and two pieces of gold waist wear. Weak (1) is unused.
  • The mode does not separate metal from dielectric. Strong marks both metal tablets and the crystal skull; sparkly marks both gold chains and quartz. It is a gloss label only, which is why metalness comes from the path instead.
  • vertexFlags.reflective appears in four block types (metal blocks, ingot piles, crystal clusters), with no mode.
  • glow appears on 3,749 faces in 206 shape files; block-level glowLevel in 9 types; lightHsv in 19.
  • Vanilla content the fallback gets wrong (gold jewellery and gems outside a metal or labelled path) is corrected by explicit entries in the vs-rtx material file, not by more heuristics.

The roughness numbers are starting points, tuned against the reference renderer in E1. Tuning them is not a contract change; changing what selects them is.

Separate AO maps (the tracer produces AO), specular tint, clearcoat, anisotropy, subsurface beyond thin translucency, displacement and parallax, companion maps at a higher resolution than the base, and any filtered magnification mode. The first five because nothing in the game’s content calls for them; the last three because they are the “remastered” look this project exists to avoid.