Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

Material Decls: Render-Program "Flags" and Adding a New Material

Partly solvedTeilweise gelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

This round studied the material definitions of Wolfenstein II, which tell the engine how a surface looks. There are no on/off switches for effects like glowing or two-sided surfaces; instead each material picks one of 429 fixed, pre-built shader programs whose name carries the effect. A brand-new material with its own self-made texture was built and added to a copy of a real game archive, and it passed every structural check. It has not been tested in the running game, so it is not yet known whether it actually renders.

  • 11,469 retail materials use 201 different keys; none is a simple two-sided/translucent/emissive switch.
  • Effects are chosen by picking one of 429 compiled shader programs by name.
  • 53 of 57 materials on the custom kit map have a matching shader variant; 4 glass/additive ones do not, and their in-game effect is unknown.
  • A new image plus material passed the round trip, both header checks, a missing-reference check and 3 of 3 negative controls.
  • Nothing was tested in game; the new material must also be referenced by a model to appear at all.

Diese Runde untersuchte die Material-Definitionen von Wolfenstein II, die der Engine sagen, wie eine Oberfläche aussieht. Es gibt keine Ein/Aus-Schalter für Effekte wie Leuchten oder beidseitige Flächen; stattdessen wählt jedes Material eines von 429 fest vorgebauten Shader-Programmen, in dessen Namen der Effekt steckt. Ein ganz neues Material mit selbst erstellter Textur wurde gebaut, in eine Kopie eines echten Spielarchivs eingefügt und bestand jede strukturelle Prüfung. Im laufenden Spiel wurde es nicht getestet, ob es wirklich angezeigt wird, ist also noch offen.

  • 11.469 Original-Materialien nutzen 201 verschiedene Schlüssel; keiner ist ein einfacher Schalter für beidseitig, durchscheinend oder leuchtend.
  • Effekte entstehen durch die Wahl eines von 429 kompilierten Shader-Programmen über dessen Namen.
  • 53 von 57 Materialien der eigenen Baukasten-Karte haben eine passende Shader-Variante; bei 4 Glas-/Additiv-Materialien fehlt sie, die Wirkung im Spiel ist unbekannt.
  • Ein neues Bild plus Material bestand den Round-Trip, beide Header-Prüfungen, eine Prüfung auf fehlende Verweise und 3 von 3 Gegenproben.
  • Nichts wurde im Spiel getestet; das neue Material muss außerdem von einem Modell verwendet werden, um überhaupt sichtbar zu sein.

Technical detailsTechnische Details

Der technische Teil ist auf Englisch – so, wie er im Wiki steht. Unterstrichene Begriffe haben eine Erklärung auf Deutsch.

Round-4 research pass on the material decl type: what the format looks like, what the two-sided/translucent/emissive/disintegrate/wound "flags" the engine is believed to have actually are, which of those are safe to use on a static-instance kit-map piece, and whether a brand-new material that references a brand-new, self-authored texture can be appended to a real retail container. All of it is static analysis plus offline round-trip builds against copies of real archives — nothing was tested in a running game. The headline finding: there are no boolean flags on a material at all; "two-sided"/"emissive"/etc. are baked into which of a fixed catalog of 429 compiled shader programs a material names. A from-scratch material + texture pair was built, appended to a copy of a populated retail container, and passed every structural check this project has (round-trip, both header hashes, a dangling-reference check, pixel decode) plus three negative controls — confirmed structurally sound, not confirmed to render in game.

1. Format

A material decl is UTF-8 text: {\n, then one key\tvalue\n line per property (engine-written order is alphabetical), }\n. Keys are renderParm names — renderParm is itself a decl type (1,661 entries) whose payload just declares a type/default/scope, e.g.:

renderParm ambientprogram   { Program 0 surface }
renderParm emissive         { Vec 0.00 surface }
renderParm lightmapscalebias{ Vec { 1, 1, 0, 0 } }   # no scope keyword -> instance-scoped

Across 11,469 retail materials (28 archives, dlc_0/dlc_4 excluded as known non-retail copies), 201 distinct keys occur. The ten most common: the 8 loose* PBR slots (~7,938 each, the ordinary textured-surface case), specularmap/powermap/bumpmap (legacy mirrors), surfacetype (7,731 — a numeric index into something not traced this round, plausibly a physics/decal table), stageprogram (2,559), ambientprogram (1,060), the 4 decal-atlas slots (651/457/383 each), shadowprogram (469), zprepassprogram (374).

2. "Flags" are program names, not booleans

No material in the 11,469-entry corpus sets a separate twosided/translucent/emissive/decal boolean. Instead, ambientprogram/stageprogram/shadowprogram/zprepassprogram/blendprogram each pick one of a closed catalog of 429 distinct renderProgResource names (case-insensitive) — compiled, binary shader bytecode + a reflection table, not authorable as text. The property is encoded in the program name itself:

key distinct values example names carrying the "flag"
ambientprogram 15 outsidetwosided(76), outsideemissive(184), outsidedisintegrate(168), outsidewound(111), outsideunique(39)
stageprogram 86 glassrefractiontwosided, basicemissive, basicemissivedisintegrate
shadowprogram 8 shadowdisintegratetranstwosided, shadowvmtrtransunique

A literal emissive key does exist (86 occurrences, a Vec4 color+intensity) but is a different mechanism — additive glow color on particle-FX materials, not the outsideEmissive program self-lit architecture uses. decalallowed (default 0) is never set by any shipped material; decal reception is resolved elsewhere, most likely through surfacetype — not settled this round.

Corroborated in the disassembly, not just inferred from data: ambientProgram/stageProgram are real string literals in .rdata (RVA 0x2635db0/0x2658ef0), and ambientProgram is read by compiled code at RVA 0x5B1D61–0x5B1D8D — consistent with this being the default-resolution fallback for the 6,878 loose materials that never set the key explicitly (read from raw bytes, not a full decompile).

3. Which programs are safe on static instances (kit-map specific)

Every idStaticInstanceEntity in a kind-2 world has its program's "Unique" (lightmapped) bit forced to 1 unconditionally — an existing finding this round narrows to concrete materials. The kit map's 160 pieces use 57 distinct materials, all resolved (100%):

group count has a compiled "Unique" program?
implicit default (no override) 50 yes — outsideUnique + 8 permutations
ambientprogram=outsideemissive 3 yes — outsideUniqueEmissive
glass (stageprogram=glass/blendprogram=glasstransmittance) 2 no — 0 of 429 names combine "glass" and "unique"
stageprogram=glassrefraction 1 no
stageprogram=basicadd 1 no

53/57 (93%) are evidenced-safe. The 4 glass/basicadd pieces either render through a path that never consults the forced Unique bit (plausible — they look like forward/transparency-pass materials, not the deferred G-buffer pass the Unique mechanism gates) or would hit a missing compiled-program combination — not settled either way (no shader-bytecode decompiler exists in this toolkit). Also flagged for any future kit piece: outsidedisintegrate, outsidewound, and the specific combination outsidetranstwosided likewise have no compiled Unique counterpart, even though some of their individual components do.

4. Adding a new material + own texture

Proven this round by building and verifying one: appending a from-scratch-authored material and a brand-new image onto a copy of an existing, already-populated retail container (not just an empty freshly-built one, and not a clone-and-swap of an existing material).

Recipe:

  1. Build a flat-color image with the project's existing author_flat() helper. Keep the edge ≤32px so it stays fully inline (no .texdb row needed) — sidesteps an open +0x60 policy question entirely.
  2. Append it as a new image entry (existing append-entry tooling, unmodified).
  3. Author the material text by hand: the 8 loose* slots + diffusemap + surfacetype, alphabetical order. loosealbedo = lower(diffusemap) + "$mtlkind=albedo$streamed". For every other slot, copy a whole retail value verbatim rather than composing a syntactically-plausible new one — see the corrected assumption below.
  4. Omit ambientprogram/stageprogram/etc. entirely unless you need an explicit override; the implicit default is the evidenced-safe choice for a static-instance kit piece.
  5. Append the material text as a new material entry, chained onto the same container.
  6. Verify with all four checks, not just hashes: the project's data-hash and header-hash tools, stale_data_hashes(), and a dangling-reference check (a material can pass both hashes while pointing at an image name nothing provides — see negative control below).
  7. To actually be rendered, the new material's name still has to be referenced from a model's surface list or an entity — building the decl pair alone places nothing.

Verified result: image and material round-trip byte-exact, 0 self-contradictions, both header hashes OK, 0 dangling references, the stored pixel data decodes to exactly the authored color. Three negative controls, all caught: a corrupted image header (caught by the bitmap self-check), a material pointing at a nonexistent image name (caught only by the dedicated dangling-reference check — the hash checks alone report OK, since hash validity and reference validity are different things), and a single flipped payload byte (caught by the stale-hash check).

Corrected assumption: a first attempt invented a plausible-looking loosesmoothness value (.../grey_md.tga$mtlkind=smoothness$smoothnessnormal=.../flat_normal$streamed) — it parses and passes both header hashes but resolves to no real image anywhere in the corpus, because that slot's identity is the literal name of a specific baked image, and retail only ever bakes specific combinations, not every syntactically valid one. Fixed by copying an exact retail-precedented string instead.

PROVEN

  • Material format, round-trip, the loosealbedo derivation rule — cross-validated again this round (11,469/11,469 parse, 0 failures).
  • 201 distinct material keys exist; none is a literal two-sided/translucent/decal boolean — those properties are which of 429 compiled program names a material picks.
  • All 57 kit-map materials resolved; 53/57 have a confirmed compiled "Unique" program variant, 4/57 (2 glass, 1 glassrefraction, 1 basicadd) do not.
  • A brand-new image+material pair appends cleanly onto a copy of an existing populated retail container and passes every structural check this project owns, plus 3/3 negative controls.

INFERRED

  • The code at RVA 0x5B1D61–0x5B1D8D is the default-program-resolution fallback — plausible from the branch shape and the census, not from a full decompile.
  • Whether the 4 glass/basicadd kit materials are excluded from, or silently broken by, the forced static-instance Unique bit.
  • surfacetype's numeric code plausibly indexes a global physics/decal/sound table — not traced.

Open questions

  • Whether a streamed new image (edge >32px, needing a real .texdb row) works the same way — deliberately sidestepped this round by staying inline.
  • What surfacetype and the table decl type it plausibly indexes actually encode.
  • Whether unknown_0x72 on the 5 near-duplicate copies each renderProgResource name ships with is a platform/quality-tier selector.

In-game test (not yet run)

Apply the built material/image pair on top of the already-proven magazine-size-edit technique in the same small container, and redirect one existing, visible material's loosealbedo to the new texture's name. If the targeted surface renders flat green after loading, the new, retail-nonexistent image entry was found and textured — the one thing this round could not settle at a desk. Separately, if either glass kit piece is ever placed as a static instance and renders black, mislit, or crashes, that is the first concrete evidence for the open Unique-bit question in §3.

Related: TNC Modding Surface, TNC Texture Database, File Formats.