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).
The look
Section titled “The look”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.
- 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.
- 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
_nor_mis 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
_nmeans flat normal and height 0.5. A missing_mmeans the fallback. Each is independent of the other.
Base colour
Section titled “Base colour”- Alpha is coverage. For
opaqueit is ignored. Forcutoutandthinit is a binary test at 0.5; there is no partial coverage in the tracer. Theglassclass 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.
_nand_mare 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.
Normal and height (_n)
Section titled “Normal and height (_n)”- 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 4texels, 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.
Surface (_m)
Section titled “Surface (_m)”- 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
_msaved 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 thecutoutandthinclasses. It is not glass.
Sampling
Section titled “Sampling”| 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.
Classes and scalars
Section titled “Classes and scalars”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 bylerp(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: withsolidtrue it is physical,colorafterdistancemetres, by Beer-Lambert along the path inside, and the faces only reflect. Withsolidfalse there is no distance to speak of:coloris what one whole pane passes at normal incidence (both faces together, so the square root of it at each) anddistanceis ignored. Either way it replaces the texture’s tint, for the whole texture: a stained glass sheet of many colours leavesabsorptionout. Withsolidnot 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 whendistanceis 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: trueis for crystal blocks, or anyone who wants a prism (not for ice: see below). - On glass texels
_mroughness is the roughness of the interface and_ntilts 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
glasstexel 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 lownormal_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
otoken). 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
_malpha 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 bylerp(white, base colour, t), wheretis 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._mroughness 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’sdiffuseTransmissionlets 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
otoken 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.
Emission
Section titled “Emission”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 writesemissiveNitsas the sheet’s figure over the emitters’ mean base luminance, and a sheet’semissive_nitsis what its flame emits on average (decision 0024). The pack’s value keeps the meaning above. - With
emissiveColororemissiveKelvin, 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
lightHsvstill drives the gameplay light grid. It is used for emission colour only by the fallback. E1’s emitter calibration setsemissiveNitsso 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 noemissiveKelvinand ships each emitter’s blackbody colour as its base colour, the default above; the authoring tool’sklegend 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).
Emission from the shape’s glow flag
Section titled “Emission from the shape’s glow flag”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
glowflag is an emissive mask in its own right,sqrt(glow / 255), and the renderer takesmax(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,emissiveKelvinandemissiveRefLevel, 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/quartzis 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 noemissiveNits, the power comes from the block instead: total luminous fluxF = F_ref x (level / 14) ^ 2, spread over that block’s glow-flagged faces, withlevelthe V of itslightHsvandF_refthe 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
glow255 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.
Emitter calibration
Section titled “Emitter calibration”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) ^ 2level 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 |
Material files
Section titled “Material files”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 andblock/metal/tarnished/rusty-ironcan 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.vsrtxMaterialon 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.pngassets/<domain>/materials/textures/block/stone/rock/granite1_m.pngassets/<domain>/materials/generated.jsonA pack never needs to ship or modify a base texture unless it de-lights it.
Composite textures
Section titled “Composite textures”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.
Requirements on the texture pipeline
Section titled “Requirements on the texture pipeline”- 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.
- 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.
- Texel-exact alignment of
_nand_mwith the base, or cutout edges sparkle. - 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.
- Use the existing labels as priors. Texture path families (
block/metal/**,block/stone/gem/**), facereflectiveModeand faceglowmark what the artists meant to be metal, gem, glossy or lit. See the audit below. - 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% (DielectricF0inpath_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. - 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
emissiveNitsas a real luminance. - 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.
Runtime behaviour the pipeline cannot see
Section titled “Runtime behaviour the pipeline cannot see”- 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_mhas 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).
Fallback when maps are missing
Section titled “Fallback when maps are missing”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.
Audit behind the table (vanilla 1.22.7)
Section titled “Audit behind the table (vanilla 1.22.7)”reflectiveModeappears 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.reflectiveappears in four block types (metal blocks, ingot piles, crystal clusters), with no mode.glowappears on 3,749 faces in 206 shape files; block-levelglowLevelin 9 types;lightHsvin 19.- Vanilla content the fallback gets wrong (gold jewellery and gems outside a
metalor 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.
Deliberately left out
Section titled “Deliberately left out”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.
