Files
llamamod/notes/geometry.md
T

14 KiB
Raw Blame History

Level-Geometrie (TRAK / MAP / STPC / TEXT) – Ein Königreich für ein Lama (PC)

Stand: 2026-11-06. Quelle: groove.exe (Ghidra-Dekompilat re/groove_decomp.c, Capstone) + Datenanalyse aller 34 Level-WADs. Tools: tools/geomlib.py, tools/geom_export.py, tools/geom_import.py, tools/geom_demo.py, tools/disx86.py. Konfidenz: [sicher] = aus Code + Daten verifiziert (Round-Trip byte-identisch / Rendering), [wahrsch.] = aus Daten plausibel, [?] = unbekannt.

1. Überblick: welcher Chunk ist was

Chunk-Dispatcher: FUN_00558xxx bei VA 0x55899e (Vergleich auf Tag-Konstanten, Tag als u32 LE gelesen, z. B. 'CPTS' im File = 0x53545043).

Chunk Loader (VA) Inhalt
WFPC inline → DAT_006da330 u32 Feature-Flags des Levels (steuern MAP/STPC-Aufbau) [sicher]
TEXT 0x4067b0 / 0x406d30 Texturseiten (256x256, 16 Bit) + Texturtabelle (UV-Rechtecke) [sicher]
SMPC / AMPC / SRPC 0x558d30 / 0x558d70 / 0x545350 Sound (SMPC: 'cvg ' VAG-Samples; AMPC: 'ambi'/'VABp'; SRPC Sound-Referenzen) – keine Geometrie
TRAK 0x42aac0 → 0x5563f0 Welt-Geometrie: Kachel-Modelle (Render-Dreiecke + Kollisionspolygone) → globales Array DAT_005846ec [sicher]
STPC 0x42ab50 Objekt-("Strat")-Modelle (gleiches Modellformat), Skelett-Animationen, danach Strat-Skriptdaten (Namen wie "SinkingStatuePad") [sicher für Modelle]
MAP 0x42ac50 Kachel-Raster: Platzierung der TRAK-Modelle + Vertexfarben + Zonen-PVS (+ Strats/Pfade/etc., siehe objects.md) [sicher]
LGHT / LNFO / LGPC 0x42c180 / 0x42ab10 / 0x558db0 Lichter / Level-Info / ? – nicht Geometrie

Die Welt ist ein Kachelsystem (Croc-Erbe): TRAK enthält N "Kachel"-Modelle in lokalen Koordinaten (ungefähr −0.5..0.5 in x/z, können aber bis ±2.4 hinausragen). MAP platziert sie auf einem W×H-Raster (1 Einheit = 1 Zelle) mit Höhe y und Drehung um y. Es gibt keinen BSP; die Raumaufteilung ist das Raster selbst (Zell-Listen) + eine 32-Zonen-Sichtbarkeitstabelle.

2. Koordinatenräume [sicher]

  • Exportraum (= MAP-Floats "A", = Vertexdaten von TRAK/STPC): rechtshändig, y oben, Rasterzellen liegen bei x ∈ [0,W), z ∈ (−H, 0]. OBJ-Export benutzt diesen Raum.
  • Spielraum (MAP-Fixpunkt "B", Kollision, Strats): (x, y, −z), 20.12-Fixpunkt (4096 = 1). Der Renderer rechnet B zurück: pos = (Bx, By, −Bz)/4096, Winkel = rot·2π/4096.
  • Kachel-Transformation (Vertex v lokal → Welt, Exportraum), a = rot/4096·2π: X = x + vx·cos a − vz·sin a ; Y = y + vy ; Z = z + vx·sin a + vz·cos a (verifiziert: 93 % aller Kachel-Randvertices treffen exakt einen Nachbarvertex; die drei anderen Vorzeichen-/Spiegelvarianten liefern 65–70 %).
  • Kollisionsdaten liegen im Spielraum (z gespiegelt gegenüber den Render-Vertices desselben Modells). Beim Dekodieren nz negieren (92 % der rekonstruierten Kollisionsecken liegen < 0.02 neben einem Render-Vertex).
  • rx/rz der Kacheln sind in allen Levels 0; nur Drehung um y (praktisch 0/1024/2048/3072).

3. Modellformat (TRAK und STPC) [sicher]

Fixup-Funktionen: TRAK FUN_005563f0(&ptr, n, &out) (Header 0x84), STPC FUN_0041f770(&ptr, n, &out) (Header 0x8c). Datei-Layout: alle Header eines Aufrufs hintereinander, dann die Daten der Modelle in Reihenfolge (TRAK: ein Aufruf mit n=Anzahl; STPC: n Aufrufe mit je 1 Modell → Header/Daten abwechselnd). Zeigerfelder sind in der Datei 0 und werden beim Laden gesetzt.

Header

Off Typ Bedeutung
0x00 3×f32 Mittelpunkt der Bounding-Box
0x0c 8×3 f32 Ecken der Box, Reihenfolge (−x−y+z)(+x−y+z)(−x+y+z)(+x+y+z)(−x−y−z)(+x−y−z)(−x+y−z)(+x+y−z). Etwas größer als die Vertices (Rand nicht konstant, ~0.004–0.014; vermutl. aus Originalmesh). Für Frustum-Culling (FUN_00402840).
0x6c u16 nVerts
0x6e u16 nFaces
0x70 u32 → Verts (Datei: 0)
0x74 u32 → Faces (Datei: 0)
0x78 / 0x7a / 0x7c 3×u16 Anzahl Kollisionspolygone in 3 Gruppen (Code summiert immer alle drei; Gruppenbedeutung [?])
0x7e u16 0
0x80 u32 → Kollision (Datei: 0)
nur STPC 0x84 u16 = nVerts bei starren Modellen, 0 bei Skin-Modellen
nur STPC 0x86 u16 nBones (Skin-Modelle) – davor in den Daten nBones×u16 = Vertexzahl je Bone (Summe = nVerts, verifiziert)
nur STPC 0x88 u32 → Bone-Tabelle (Datei: 0)

Daten (in dieser Reihenfolge)

  1. (nur STPC) nBones × u16
  2. Vertex 24 Byte: f32 x,y,z ; f32 nx,ny,nz (Normalen geglättet; Lichtwerte kommen aus MAP-Vertexfarben)
  3. Face 28 Byte (nur Dreiecke): u16 flags ; u16 v0,v1,v2 ; u16 tex ; u16 0 ; f32 nx,ny,nz,d mit n = normalize((v1−v0)×(v2−v0)), d = n·v0 (verifiziert für alle Faces). Beim Laden wird tex durch Zeiger auf Texturtabelleneintrag (DAT_00581154 + tex*20) ersetzt. Ebene dient dem Backface-Culling (Kamera·n − d ≥ ε).
  4. Kollisionspolygon 32 Byte, nGruppeA+B+C Stück: u8 typ ; u8 material ; 5 × { i8 nx, i8 ny, i8 nz, u8 0, i16 d }
    • Ebene 0 = Fläche, Ebenen 1..4 = Kanten (Normalen nach innen, Punkt drin ⇔ n·p − d ≥ −Toleranz).
    • Normalen ×127 (Code rechnet ×32 → 4096-Skala), d ×4096 (Fixpunkt) → lokale Koordinaten nur ±8 Einheiten!
    • typ Bit0 = Viereck (4 Kanten), sonst Dreieck (3 Kanten, 5. Ebene = 0). Bit2 ≈ Normale zeigt nach unten (ny<0; Decken/Überhänge) [wahrsch.].
    • material: Untergrund-Typ (0 = normal; 0x11/0x12 werden je nach WFPC 0x8000000 ignoriert, 0x1e = Wasser-/Todesebene [wahrsch.]; Wert landet im Spieler-Bodenstatus).
    • Kollisionsabfragen: FUN_004036d0 (Boden/Decke), FUN_00403ad0 (Wände), FUN_004042f0, FUN_004046e0. Getestet werden nur die Kacheln der Rasterzelle, in der der Spieler steht (Zellliste aus MAP), Punkt relativ zu Kachel-B-Position, um −rot gedreht.

Face-Flags (u16)

Bit Bedeutung Konfidenz
0x0008 neuer Render-Batch: Renderzustand (Seite, Blendmode) aus diesem Face neu laden, vorherigen Batch zeichnen. Sortierung nach Seite/Zustand + Bit am Wechsel. sicher (Code)
0x0010, 0x0800, 0x0020 UV-Ecken-Auswahl (s. u.) sicher
0x0400 kein Backface-Culling (doppelseitig) sicher
0x4000 Vertexfarben ignorieren (weiß) sicher
0x0001, 0x0002 Sonder-Rendermodi (Sortierung/Transparenz; zusammen mit Globalflags 0x5f6ee8) wahrsch.
0x8000, 0x1000, 0x2000, 0x0200, 0x0100, 0x0004 in Daten vorhanden, Renderer (0x556510) wertet sie nicht aus ? (vermutl. PS1-Reste)

UVs – die Engine hat KEINE freien UVs [sicher]

Jedes Dreieck zeigt eine Hälfte eines Texturrechtecks (Eintrag der TEXT-Tabelle). UV-Rechteck: U0=u0/256, U1=(u1+1)/256, V0=v0/256, V1=(v1+1)/256. Mit m = Flag 0x20 (spiegelt U), a = m?U0:U1, b = m?U1:U0 bekommen die Ecken (v0,v1,v2):

  • ohne 0x800: (a,V1) (b,V1) (a,V0)
  • 0x800, ohne 0x10: (a,V1) (b,V1) (b,V0) ← erste Hälfte eines Vierecks
  • 0x800 + 0x10: (b,V0) (a,V0) (a,V1) ← zweite Hälfte (Renderer FUN_00556510 ab Zeile "if ((uVar6 & 0x800) == 0)"). Texturen mit Flag Bit0 sind Flachfarben (u0,u1,v0 = R,G,B). 96 % der Welt-Faces haben 0x800.

4. TRAK-Chunk [sicher]

u32 n ; n × Header(0x84) ; Daten Modell 0..n−1. Alle 34 Level-WADs parsen exakt bis Chunkende; geomlib.build_trak(parse_trak(x)) == x byte-identisch für alle Levels. Typisch 100–650 Modelle, max. 422 Verts / 798 Faces pro Modell. Modell 0 ist in vielen Levels eine unsichtbare Ebene (4 Verts, 0 Faces, 1 Kollisionspolygon, material 0x1e) bei y=−2.75 – die Wasser-/Absturzebene unter den Inseln.

5. MAP-Chunk – Geometrieteile (Loader FUN_0042ac50) [sicher]

(Strat/Pfad/Zonen-Blöcke: siehe notes/objects.md; hier nur, was die Geometrie betrifft.) Reihenfolge in der Datei:

  1. u32 N (Kacheln), u32 W, u32 H (Raster)
  2. A: N × {f32 x, f32 y, f32 z, u32 rot, u32 model, u32 animFlag} (Exportraum; x,z = Zellmitte .5). Nur zum Einsortieren in die Zelllisten benutzt: Zelle = floor(x) + floor(−z)·W.
  3. u32 n + n×u32 (Liste, von Strats referenziert)
  4. u32 n + n×90 Byte (Lichter/Kameras? – objects.md)
  5. u32 n + n×34 Byte (Pfadknoten)
  6. Zonentabelle 256 Byte = 32 × {u32 Sichtmaske, i32 Höhenwert} → DAT_006da350
  7. Zonenraster W·H × u32 (Zonennummer 0..31 je Zelle) → world+0x48
  8. B: N × {i32 rx, i32 ry, i32 rz, i32 x, i32 y, i32 z} 20.12-Fixpunkt, Spielraum (z = −A.z), ry = A.rot. Wird zum Rendern und für Kollision benutzt. A und B sind in allen Levels konsistent.
  9. Modellindex: N × u32 (= A.model)
  10. falls WFPC & 0x10000: u32 n ; n × {u32 cellX, u32 y, u32 cellZ, u32 modelA, u32 modelB} – Kachel-Wechsel (Türen/Brücken o. Ä.), Eintrag k gehört zur k-ten Kachel mit animFlag≠0.
  11. Vertexfarben je Kachel i: nVerts(model) × u32 RGBA (0x80 = 1.0, Spiel ×2 mit Clamp); Byte3 des ersten Eintrags = Anzahl Farbsätze (0/1 = einer; ≥2 → weitere nVerts×u32 folgen, zweiter Satz wird bei DAT_005865d4==1 benutzt). Für animFlag-Kacheln danach dieselbe Struktur für modelB. Zeigertabelle world+0x58 (bei WFPC 0x10000000 doppelt groß).
  12. Rest (u32 n2,n3 + 58-Byte-Strat-Einträge, optional Pfade (0x200000), Sichtweite (0x10), u16) – objects.md.

geomlib.build_map_tiles(..., zones='keep', anim=orig) reproduziert alle MAP-Chunks byte-identisch.

Raster / Sichtbarkeit (kein BSP) [sicher]

  • Zelllisten: world+0x40 (W·H Zeiger, verkettete Listen von Kachelindizes), aus A gebaut.
  • Rendern (FUN_0042bf40): Rechteck um die Kamera (Sichtweite) über das Raster; für jede Zelle alle Kacheln der Liste, aber nur wenn Bit zone[Zelle] in der Maske der Kamerazone gesetzt ist (DAT_0058642c = Zonentabelle[zone[Kamerazelle]].maske, FUN_0041e020). Zone 0 hat im Dorf-Level Maske 0x1. → Neue Geometrie muss passende Zonen bekommen, sonst unsichtbar. geom_import --zones flat setzt Raster=0 und Maske(Zone 0)=0xffffffff (alles sichtbar).
  • Kollision: nur Kacheln der Spielerzelle (s. o.).

6. STPC-Chunk (Objekte) [sicher bis Animationen]

u32 n1 ; n1 × (Header 0x8c + Daten) statische/geskinnte Objektmodelle (33 im Dorf) ; u32 n2 ; n2 × Animation (FUN_0041f8b0, Header 32 Byte: +0 nA (nA×4 Byte), +8 nFrames, +0xc (0 → nFrames×8 Byte), +0x10 nDelta (Frame 0: nDelta×8, sonst nDelta×2 (+2 wenn ungerade) Byte komprimierte Vertexdeltas), +0x18 nBones (je Frame nBones×64 Byte = 4×4-Matrix)) ; falls WFPC & 0x100: u32 n3 ; n3 × {16-Byte-Kopf (+4 Anzahl, +8 → n×8 Byte), Modelle} ; danach Strat-Daten (Skripte, Namen) – nicht vom Lader geparst, über Offsets (DAT_006d9dbc+off) aus MAP referenziert. geom_export --objects exportiert die n1 Modelle nebeneinander.

7. TEXT – soweit für Geometrie nötig [sicher]

u32 nPal ; nPal×{u32, u32 n, n×u16, n×u8} ; u32 nPages ; nPages×{u32 flags, u32 w, u32 h, (flags&0x80: u32 len + RLE16) | (flags≠0: w·h·2) | w·h} ; u32 nTex ; nTex × 8 Byte ; Rest (40 Byte im Dorf) [?]. Textureintrag: u16 flags, i8 page, i8 page2(-1), u8 u0, u8 u1, u8 v0, u8 v1 (u1/v1 inklusiv). Flags: Bit0 Flachfarbe, Bit2 1-Pixel-Rand, Bit3/Bit4 Breite/Höhe ×2, Bits 12–14 Blendmodus, Bits 8–10 weiterer Modus. Pixel: ABGR1555 (R = Bits 0–4), 0 = transparent. Details: notes/textures.md (anderer Agent).

8. Neue Geometrie schreiben – was konsistent sein muss

Umgesetzt in tools/geom_import.py (OBJ → TRAK + MAP-Kachelteil, Rest des WADs unverändert):

  1. TRAK: u32 Anzahl; Header mit korrekten nVerts/nFaces/nKoll; Box+Mittelpunkt (für Culling, wird neu berechnet); Face-Ebenen aus Vertices; Vertexnormalen. Keine Prüfsummen.
  2. Face-Texturindex < Anzahl Texturtabelleneinträge; UVs nur über Rechteck-Ecken (+Flags). Bit 0x8 am Batch-Anfang (Seiten-/Zustandswechsel, spätestens alle 256 Faces).
  3. Limits: ≤ 8000 Vertices pro Modell (Transformpuffer 0x58b0f0..0x5d15f8, 36 Byte/Vertex), Batch-Ausgabepuffer 0x5d15f8 ≈ 4800 Vertices; Kollisions-d als i16/4096 → Kollisionspolygone nur innerhalb ±8 Einheiten vom Kachelursprung. u16-Indizes.
  4. MAP: N in Kopf, A (Floats) und B (Fixpunkt, z negiert) identisch halten, Modellindizes < TRAK-Anzahl, Kachelzellen innerhalb W×H, Vertexfarben-Block exakt nVerts(Modell) pro Kachel (sonst verschiebt sich der Rest von MAP = Strats!), Animationsblock (WFPC 0x10000) passend zu den Flags (Import setzt ihn auf 0 Einträge), Zonen (s. o.).
  5. Kollision gehört zur Zelle: ein Polygon wirkt nur, wenn es in der Kachelliste der Zelle steht, in der der Spieler steht → Import trägt jedes Polygon in jede überdeckte Zelle ein (auch als reine Kollisionskachel ohne Faces, wie Original-Modell 0).
  6. WAD-Gesamtlänge (wadtool/geomlib schreiben sie neu). Keine weiteren Prüfsummen gefunden.
  7. Strats (Startpunkt, Gegner, Kamera-Pfade) liegen weiter an ihren alten Positionen (objects.md).

Tests (ohne Spielstart)

  • Round-Trip TRAK und MAP aller 34 WADs byte-identisch.
  • Export → Import (--coll-obj) → Export t1l1m001: Rendering identisch (img/), 26435/26442 Dreiecke (7 entartete fallen weg), Bodenhöhe an 2800 Zufallspunkten: 99 % < 5 mm Abweichung, 10 Punkte anders (Polygone jetzt auch in Nachbarzellen eingetragen).
  • Unabhängiger Gegencheck: tools/mapchunk.py (anderer Agent) parst die erzeugten MAPs vollständig.
  • Noch NICHT im Spiel getestet: Demo-WADs in $CLAUDE_JOB_DIR/tmp: demo_add.wad (Original + 2 Blöcke + Rampe, Zonen original), demo.wad (alles ersetzt durch Bodenplatte y=0 + Blöcke).

9. Offen / unbekannt

  • Bedeutung der drei Kollisionsgruppen (Summe wird immer zusammen benutzt) und vollständige Materialliste.
  • Face-Flags 0x8000/0x1000/0x2000/0x200/0x100/0x4; genaue Wirkung 0x1/0x2.
  • Exakte Herkunft des Box-Randes im Header (Neu-Berechnung mit 1/128 Rand reicht vermutlich).
  • Zonentabelle 2. Wert (Höhe, DAT_00586424) – evtl. Wasser-/Nebelhöhe.
  • STPC-Animation: Bedeutung von nA-Block und Delta-Kompression im Detail (FUN_0041fa30).
  • Ob der Spieler-Startpunkt/Kamera an Geometrie gekoppelt ist (objects.md).

10. Werkzeuge

  • geom_export.py WAD OUT [--collision] [--objects] → level.obj/mtl (UV, Vertexfarben, ein Objekt pro Kachel), page_NN.png, collision.obj (Gruppen matNN_grpG), objects.obj, preview_top/persp.png (texturierter Software-Renderer).
  • geom_import.py BASIS.wad MESH.obj NEU.wad [--coll-obj C.obj] [--keep-tiles] [--zones flat|keep] [--default-tex N]
  • geom_demo.py BASIS.wad demo.obj [--plaza] – Beispielgeometrie aus Texturkachel-Vierecken.
  • geomlib.py – Parser/Builder (TRAK, STPC-Modelle, MAP-Kachelteil, TEXT-Tabelle/Seiten, Kollision).