Wolfenstein Modloader Buy me a coffeeKaffee spendieren

In plain wordsIn einfachen Worten

This report continues decoding the skeleton (.md6skl) and animation (.md6anim) files used by posed character and weapon models in the id Tech games studied here. The skeleton format is decoded and posing a flat weapon rig like a pistol works, but for a soldier most joints do not come out right with any tested method. The compressed per-frame animation data is still not decoded, so posing an animation frame is not possible yet; all of this is offline format research.

  • The skeleton format is verified across almost all shipped skeletons; one broken file is correctly rejected.
  • Correction: the per-joint rotation table stores the same rotation as the matrix, not its inverse (checked on over 24,000 joints).
  • For the soldier rig, 45 of 71 non-root joints get implausible bone lengths under the tested method.
  • The animation header is decoded one field further; about 23% of sampled animations name a base animation they build on.
  • The search for the frame decoder is narrowed to a cluster of about 51 functions (about 20 KB) in the game code.

Dieser Bericht setzt die Entschlüsselung der Skelett- (.md6skl) und Animationsdateien (.md6anim) fort, die posierte Charakter- und Waffenmodelle in den untersuchten id-Tech-Spielen nutzen. Das Skelettformat ist entschlüsselt, und ein flaches Waffen-Rig wie eine Pistole lässt sich korrekt posieren; bei einem Soldaten kommen aber die meisten Gelenke mit keiner getesteten Methode richtig heraus. Die komprimierten Einzelbild-Daten der Animation sind noch nicht entschlüsselt, daher lässt sich noch kein Animationsbild posieren; alles ist reine Offline-Formatforschung.

  • Das Skelettformat ist an fast allen mitgelieferten Skeletten geprüft; eine defekte Datei wird korrekt abgelehnt.
  • Korrektur: Die Rotationstabelle pro Gelenk speichert dieselbe Rotation wie die Matrix, nicht deren Umkehrung (geprüft an über 24.000 Gelenken).
  • Beim Soldaten-Rig ergeben 45 von 71 Gelenken außer der Wurzel mit der getesteten Methode unplausible Knochenlängen.
  • Der Animations-Header ist ein Feld weiter entschlüsselt; etwa 23 % der Stichprobe nennen eine Basisanimation, auf der sie aufbauen.
  • Die Suche nach dem Einzelbild-Decoder ist auf eine Gruppe von etwa 51 Funktionen (rund 20 KB) im Spielcode eingegrenzt.

Technical detailsTechnische Details

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

Continues decoding the skeleton and animation formats used by posed/skinned character models, aiming to pose a weapon and a soldier at a given frame. The skeleton format and its posing math for a flat weapon rig are unchanged and still corpus-verified; a prior claim that the per-joint quaternion table stores the inverse of the per-joint matrix's rotation is corrected on a large corpus — it is the same rotation. The humanoid bind-pose puzzle, previously scoped to a handful of torso/shoulder joints, turns out to be much bigger: no tested hierarchy-composition hypothesis gives plausible bone lengths for most of a soldier rig's non-root joints. The animation file's outer header is decoded one field further, and the search space for its still-undecoded per-frame compressed data is narrowed from the whole executable to a specific, small code cluster. Everything here is offline format research; posing an animation frame could not be completed, since no per-frame data is decoded yet to apply.

.md6skl — unchanged, corpus-verified

Header, joint name table, parent-index array, and per-joint bind matrix are decoded and corpus-verified across essentially the whole shipped skeleton corpus (one corrupt/truncated file is correctly rejected). Correction this pass: the per-joint quaternion table encodes the same rotation as the matrix table's own rotation, not its inverse — checked on over 24,000 individual joints across every parseable skeleton, not a single hand-checked example.

The bind-pose puzzle (still open, bigger than previously thought)

"Invert each joint's own matrix in model space, no hierarchy (FK) composition" fully and correctly poses a flat weapon rig (e.g. a pistol). For a humanoid soldier rig, this hypothesis is only solidly right for the hip root and the first 1–2 hierarchy levels: 45 of 71 non-root joints get an implausible bone length to their named parent under this hypothesis, and 65 of 71 have an implausible raw translation magnitude outright — which rules out both "no FK" and any ordering of hierarchical composition for most of the rig simultaneously, not a short exception list. A concrete structural clue: left/right mirror symmetry of the raw translation breaks at almost exactly the same joints where bone length also breaks (9 of 10 tested limb pairs) — a lead toward the real mechanism, not an explanation of it.

.md6anim outer header (decoded one field further)

u32-len + ascii   skeleton name (ends .md6skl)
u32-len + ascii   OPTIONAL reference-animation name (ends .md6anim when present,
                  empty for a standalone animation — present on ~23% of a
                  400-file random sample: "bodyparts"/single-limb and
                  "additive" animations that supply partial joint data on
                  top of a named base animation)
12 x s16          two groups of 6; small/plausible on the large majority of
                  sampled files; the handful of exceptions are genuinely
                  world-scale set-piece animations (a sliding tunnel wall, a
                  vehicle, a collapsing walkway), not parse failures

The 12-s16 header is likely a per-channel quantization range: two different animation files of unrelated content but the same generic prop-animation template reproduce the exact same 12 values bit-for-bit. Which group quantizes which channel, and the fixed-point scale, are not resolved.

Per-frame bit-packed joint data is still not decoded. The known compressor function is located in the executable; the animation class's own RTTI gives exactly two constructor/destructor cross-references, bounding a dense ~51-function, ~20 KB cluster immediately bordering the compressor that almost certainly contains the frame decoder — a sharply narrowed search space versus the whole executable. (A previously proposed search method — looking for code references to a related matrix-type's RTTI name — is a dead end: that type has no RTTI/vtable entry at all, so it is never referenced from code; the vtable-of-a-related-class method above is what actually worked.)

Supporting decl types (spineIK, playerArmIK, playerLegIK, a per-character animation-alias table) are plain text, already readable, and corroborate joint naming but say nothing about the bind-pose matrix encoding itself.

Prior art: closed-source community extractors for this game family exist and advertise animation support, evidence the format has been solved elsewhere, but no public documentation of the per-frame bit layout was found.

Failed hypotheses / dead ends

  • Matrix grouped as 4 sequential 3-vectors; FK composition in any order/convention; reading the quaternion one float early as a negative control; joint count read as the wrong integer width.
  • Searching for code references to the matrix-type's own RTTI string (it has none).
  • Guessed decoder function names (none exist as strings in the executable) — the decoder must be found structurally, by its position in the narrowed code cluster, not by name.

Open questions

  • What operation (not plain per-joint inverse, not any FK ordering — both corpus-ruled-out) recovers correct bone lengths for most of the soldier rig.
  • Whether the left/right mirror-break is the same root cause as the bone-length puzzle, or a separate fact about how chain-root/roll-helper joints are authored.
  • Which of the two s16 groups in the animation header quantizes which channel, and at what fixed-point scale.
  • The animation format's actual per-frame bit layout — next step: disassemble the narrowed code cluster described above, using known constants (the header's quantization-range shape, a groups-of-8 addressing idiom already seen inside the compressor) as correlation anchors.

No in-game test applies yet — this is pure offline format research; nothing changes visibly in a mod until both the per-frame compression and the full bind-pose reconstruction are decoded.