Files
llamamod/notes/geometry.md
T

204 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).