diff --git a/docs/superpowers/specs/2026-09-17-animationen-design.md b/docs/superpowers/specs/2026-09-17-animationen-design.md new file mode 100644 index 0000000..e6fbf1c --- /dev/null +++ b/docs/superpowers/specs/2026-09-17-animationen-design.md @@ -0,0 +1,290 @@ +# Animations-Testseite mit Waffen-Dummies — Entwurf + +**Datum:** 2026-09-17 +**Zusammenhang:** Der Nutzer hat zwei Packs mitgegeben, `~/Downloads/MotionLibrary_Weapons.zip` (Mocap) und `~/Downloads/ASISWeaponsPack.zip` (Waffen), mit dem Auftrag: „Mach eine neue Branch. In der du Character spawnst. Die aus diesem Animationen-Pack die Animationen einfach in einer Loop ausführen. Und man kann sich die angucken. Man soll aber selbst auch mit einem Charakter rumlaufen können. Und man soll […] die Waffe, die ich dir mitgebe, falls es eine Animation gibt, die passt zu der Waffe, […] diese Waffe in der Hand halten, also die Dummies, die dann da rumstehen." +**Baut auf:** `src/characters/index.js` (Mannequin `ual.glb`, `PLAYER_MODEL_BASE`), `src/world/sky.js` und `src/world/shadows.js` (Himmel, Licht, Kaskadenschatten), `tools/assets/places.mjs` (Aufbereitung mit gltf-transform, WebP, meshopt). + +## Ziel + +Eine eigene Seite `animationen.html`. Darauf stehen 13 Mannequin-Dummies und spielen die 11 Mocap-Clips in Dauerschleife. Acht von ihnen halten eine Waffe aus dem Pack, die zu ihrem Clip passt. Der Nutzer läuft mit seiner eigenen Figur zwischen ihnen herum. + +## Befund (gemessen) + +### Animationen + +| Punkt | Wert | +|---|---| +| Dateien | 11 × FBX 7.7 binär, zusammen 13,2 MB. Kein Mesh, nur Skelett, ein Clip und die Producer-Kameras von MotionBuilder. | +| Skelett | 24 Knochen mit Xsens-MVN-Namen: `Reference`, `Hips`, `Spine`–`Spine3`, `Neck`, `Head`, je Seite `Shoulder`, `Arm`, `ForeArm`, `Hand`, `UpLeg`, `Leg`, `Foot`, `ToeBase`. Keine Finger. | +| Einheit, Achsen | cm, Y oben, Blick nach +Z, links des Darstellers bei +X | +| Ruhepose | T-Pose, jede Knochen-Weltdrehung 0° | +| Spuren | `Hips.position` und 23 × `.quaternion`, 120 fps, Zeitstempel auf ms gerundet (Abstände 7–9 ms) | +| Frame 0 | die T-Pose selbst, 8 ms vor Frame 1, mit dem die Bewegung beginnt | +| Hüfthöhe der Ruhepose | je Aufnahmetag verschieden: 0,909 m (14.07.) und 0,900 m (15.07.) | + +| Clip (aus dem Dateinamen) | Dauer | Drift netto | weiteste Abweichung nach Driftabzug | Hüfte am Anfang | Naht: Blickwinkel / größte Knochenabweichung | +|---|---|---|---|---|---| +| `hunter_bowArrow_tk01` | 77,65 s | 2,17 m | 3,13 m | −2° | 72° / 91° | +| `leftHandThrow_grenade_tk01` | 6,65 s | 0,06 m | 0,21 m | −44° | 7° / 15° | +| `leftHandThrow_grenade_tk02` | 4,30 s | 0,06 m | 0,18 m | −38° | 0° / 7° | +| `taunt_shootSelfInHead_tk01` | 7,83 s | 0,09 m | 0,16 m | −17° | 1° / 15° | +| `taunt_shootSelfInHead_tk02` | 6,71 s | 0,04 m | 0,42 m | −16° | −6° / 13° | +| `stabTwist_Knife_tk01` | 8,48 s | 0,37 m | 0,19 m | −28° | −1° / 15° | +| `stabTwist_Knife_tk02` | 6,27 s | 0,08 m | 0,20 m | −25° | −3° / 17° | +| `idle_loadGun-Hand_tk01` | 6,56 s | 0,01 m | 0,05 m | −19° | 2° / 19° | +| `idle_loadGun-Hand_tk02` | 5,89 s | 0,02 m | 0,03 m | −18° | 0° / 18° | +| `idle_loadGun-Rifle_tk04` | 7,75 s | 0,01 m | 0,02 m | −10° | 1° / 16° | +| `idle_loadGun-Rifle_tk05` | 6,13 s | 0,02 m | 0,02 m | −24° | 2° / 14° | + +Drift und Naht sind ab Frame 1 gemessen. „Hüfte am Anfang" ist der Winkel des Hüft-Vorwärtsvektors gegen +Z in der Waagrechten. + +### Mannequin (`public/models/ual.glb`) + +| Punkt | Wert | +|---|---| +| Aufbau | `Armature` → `Mannequin` (Skinned Mesh, 13 743 Dreiecke, 2 Primitive, keine Texturen) und `root` (−90° um X, Blender-Z-oben) → `pelvis` … | +| Skelett | 65 Knochen mit Unreal-Namen. Je Hand 15 Fingerknochen und 5 Blattknochen. 53 Clips. | +| Ruhepose | T-Pose, Blick nach +Z, links bei +X, 1,83 m hoch, `pelvis` bei 0,917 m | +| Spielfigur | Der Controller skaliert das Modell auf `playerCapsuleHeight` (180) × `scale` (`PlayerController.ts`, Zeilen 438–476). Mit `PLAYER_MODEL_BASE.scale` = 0,005 ist die Figur laut Code 0,9 Welteinheiten hoch, nicht 1,83. | +| Finger in `Pistol_Idle_Loop`, Frame 0 | beide Hände gekrümmt, gegen die Ruhepose: Zeigefinger 45° (rechts) / 41° (links), Mittelfinger 63° / 48°, rechter Daumen 41° | +| Finger in `Idle_Loop`, Frame 0 | locker gekrümmt: Zeige- und Mittelfinger 77°, Daumen 26° | + +Höhen der Wirbelsäule im Vergleich (Ruhepose, m): + +| MVN | Höhe | Mannequin | Höhe | +|---|---|---|---| +| `Hips` | 0,900 | `pelvis` | 0,917 | +| `Spine` | 0,995 | `spine_01` | 1,051 | +| `Spine1` | 1,095 | `spine_02` | 1,174 | +| `Spine2` | 1,187 | — | — | +| `Spine3` | 1,279 | `spine_03` | 1,315 | +| `Neck` | 1,411 | `neck_01` | 1,488 | +| `Head` | 1,506 | `Head` | 1,569 | + +### Waffen + +| Waffe | Quelle im Pack | Eckpunkte (nicht indiziert) | Länge | Texturen | +|---|---|---|---|---| +| Pistole | `Pistol/SM_Pistol.fbx` | 8 592, zwei UV-Sätze | 0,19 m | `T_`/`N_` 2048 px, `M_` 512 px | +| AK47 | `AK47_Option2(Same Socketing)/SM_AK47.fbx` | 15 534 | 0,87 m | `T_`/`N_` 2048 px, `M_` 1024 px | +| M4 | `M4 Carbine/SM_M4_Carbine.fbx` | 17 403 | 0,86 m | `T_`/`N_` 2048 px, `M_` 1024 px | +| 870 Shotgun | `870 Shotgun/SM_870_Shotgun.fbx` | 7 812 | 1,00 m | `T_`/`N_`/`M_` 2048 px | +| L96 | `L96 Sniper Rifle/SM_L96_Sniper_Rifle.fbx` | 21 318 | 1,17 m | `T_` 2048 px RGBA (Alpha überall 255), `N_` 2048 px, `M_` 1024 px | + +- **Knoten:** + - Jede Datei hat genau ein Mesh mit einem Material. + - Die Pistole trägt den Knotenmaßstab 100 auf Eckpunkten, die schon in Metern liegen. + - Die L96 trägt die Knotendrehung (180°, 0°, 180°). + - Die übrigen haben keine Knotentransformation. + - Der Ursprung liegt am Griff, bei der M4 zum Beispiel zwischen x −0,30 und 0,56. +- **AK47 „Option 1 (Different Socketing)":** 25 Skinned Meshes an den Knochen `Root`, `Trigger` und `Magazine`, Lambert-Material, anderer Griffpunkt, Texturen in 4096 px. Wird nicht verwendet. +- **Normalmaps:** schon im OpenGL-Format. Bei allen fünf Waffen ist ∂R/∂y negativ mit ∂G/∂x korreliert (−0,09 bis −0,42). Bei DirectX-Karten wäre die Korrelation positiv. +- **Masken `M_*.tga`:** + - TGA Typ 2 (unkomprimiert), 32 Bit, kein ID-Feld, Ursprung unten links, TGA-2.0-Fußzeile. + - R, G und B sind gleich (ein Grauwert), dazu kommt Alpha. + - AK47, Median je Fläche: Holz Grau 0,85 / Alpha 0,22, Metall Grau 0,95 / Alpha 0,70. + - Daraus folgt: **Grau = Rauheit, Alpha = Metall**. Der Alphakanal trennt Holz von Metall, der Grauwert nicht. +- **Lizenz:** In keinem der beiden Zips liegt eine Lizenzdatei. + +### Welche Waffe zu welchem Clip passt + +- **Pistole:** `idle_loadGun-Hand`, `taunt_shootSelfInHead` +- **Gewehr (AK47, M4):** `idle_loadGun-Rifle` +- **Messer, Granate, Bogen:** Diese Waffen sind nicht im Pack. +- **870 und L96:** Für sie gibt es keinen eigenen Clip. + +## Entscheidungen des Nutzers + +| Frage | Entscheidung | +|---|---| +| Ort | Eigene Testseite. Verworfen: eine Reihe Dummies in der Welt am Startpunkt. | +| Shotgun und L96 | Sie kommen dazu, mit den beiden Gewehr-Nachladeclips. | +| Weg | Umbau vorab mit Node-Skripten. Verworfen: FBX im Browser parsen und übertragen, sowie `SkeletonUtils.retargetClip` statt eigener Übertragung. | +| Entwurf Abschnitt 1–3 | bestätigt (Seite, Aufstellung und Panel; Umbau, Griff und Finger; Tests, Branch und Lizenz) | +| Arbeitsweise | Arbeitsbaum `.claude/worktrees/animationen`, Branch `leonida/animationen` von lokalem `main` (`ee49fd7`, 24 Commits vor `origin/main`). Auf `leonida/strassen` arbeitet parallel eine andere Sitzung. | + +## Testseite + +### Seite und Szene + +- **Einstieg:** `animationen.html` im Wurzelverzeichnis neben `index.html` lädt `src/showroom/main.js`. Lokal ist die Seite unter `http://localhost:5173/animationen.html` erreichbar. +- **`vite.config.js`:** `build.rollupOptions.input` nennt beide Seiten, damit `npm run build` auch die Testseite baut. +- **Licht:** Renderer, Tonemapping, Kamera, Nebel, `createSky` und `createSunShadows` wie in `src/main.js`, mit denselben Werten für Sonne, Himmelslicht und Kuppelhelligkeit (dort lokale Konstanten, hier mit Verweis übernommen). +- **Boden:** eine Ebene von 200 × 200 Welteinheiten bei y = 0 mit der Textur `concrete_pavement` aus `public/textures` (Diffuse, `nor_gl`, `arm`). Sie ist der einzige Collider der Spielfigur und muss schon bei `player.init` stehen. Deshalb hängt sie nicht an `stage` (siehe Maßstab) und ist für jeden Maßstab groß genug. Die Kachel misst 1,8 Mannequin-Meter (Poly Haven misst `concrete_pavement` mit 1,8 × 1,8 m). `texture.repeat` wird nach `player.init` gesetzt. +- **Nicht auf der Seite:** Stadt, Streaming, Fahrzeuge, Minimap, Figurenwahl. + +### Maßstab + +Die Spielfigur hat nicht die 1,83 m des Mannequins (siehe Befund). Die Aufstellung hängt deshalb an einer Gruppe `stage`. Sie bekommt nach `player.init` den **Weltmaßstab des Spielermodells** (`player.playerModel.getWorldScale`), ausgelesen und nicht nachgerechnet. Alle Maße unten sind Mannequin-Meter: Dummies, Waffen, Schilder, Abstände und der Startpunkt. Welteinheit = Mannequin-Meter × Maßstab. Dummies und Spielfigur sind damit gleich groß. + +### Spielfigur + +- **Modell:** `ual.glb` mit `playerController` und `PLAYER_MODEL_BASE` aus `src/characters/index.js`, wie im Spiel. Laufen, Sprinten, Springen und Kamera verhalten sich wie gewohnt. +- **Start:** bei (0 | 8) Mannequin-Metern vor Reihe 1, mit Blick nach −Z auf die Dummies. `player.init` beginnt über dem Ursprung, danach setzt `player.reset` die Figur auf 8 × Maßstab. + +### Aufstellung (`src/showroom/lineup.js`) + +Reine Daten und reine Funktionen, ohne Browser testbar. + +| Reihe, Platz | Clip | Schild | Waffe | Hand | +|---|---|---|---|---| +| 1, 1 | `idle_loadGun-Hand_tk01` | Pistole nachladen · Take 1 | Pistole | `r` | +| 1, 2 | `idle_loadGun-Hand_tk02` | Pistole nachladen · Take 2 | Pistole | `r` | +| 1, 3 | `taunt_shootSelfInHead_tk01` | Taunt: Schuss in den Kopf · Take 1 | Pistole | `r` | +| 1, 4 | `taunt_shootSelfInHead_tk02` | Taunt: Schuss in den Kopf · Take 2 | Pistole | `r` | +| 1, 5 | `idle_loadGun-Rifle_tk04` | Gewehr nachladen · Take 4 | AK47 | `r` | +| 1, 6 | `idle_loadGun-Rifle_tk05` | Gewehr nachladen · Take 5 | M4 | `r` | +| 1, 7 | `idle_loadGun-Rifle_tk04` | Gewehr nachladen · Take 4 | 870 Shotgun | `r` | +| 1, 8 | `idle_loadGun-Rifle_tk05` | Gewehr nachladen · Take 5 | L96 | `r` | +| 2, 1 | `stabTwist_Knife_tk01` | Messerstich mit Drehung · Take 1 | — | — | +| 2, 2 | `stabTwist_Knife_tk02` | Messerstich mit Drehung · Take 2 | — | — | +| 2, 3 | `leftHandThrow_grenade_tk01` | Granate werfen (links) · Take 1 | — | — | +| 2, 4 | `leftHandThrow_grenade_tk02` | Granate werfen (links) · Take 2 | — | — | +| 2, 5 | `hunter_bowArrow_tk01` | Bogenschütze · Take 1 | — | — | + +- **Reihe 1:** bei z = 0, acht Plätze bei x = −12,25 + 3,5 · i. +- **Reihe 2:** bei z = −8. Die ersten vier Plätze liegen wie in Reihe 1 (x = −12,25 bis −1,75), der Bogenschütze steht bei x = 8,75. Zum nächsten Dummy hat er damit 8 m Abstand, genug für seine 3,13 m Wanderung. +- **Blickrichtung:** Alle Dummies schauen nach +Z zur Spielfigur. Die Clips sind darauf ausgerichtet (siehe Umbau, Schritt 3), die Dummies werden also nicht gedreht. +- **Schild:** ein Canvas-Sprite 2,2 m über dem Platz (nicht am Dummy, damit es beim Bogenschützen stehen bleibt). Es zeigt den Text aus der Spalte „Schild" und darunter die Waffe oder „ohne Waffe". +- **„Hand":** Vorgabe ist `r`. Hält ein Darsteller die Waffe sichtbar links, zum Beispiel weil die rechte Hand nachlädt, wird der Eintrag im Browser auf `l` gesetzt und dies im Bericht festgehalten. + +### Dummies (`src/showroom/dummies.js`) + +- **Laden:** `ual.glb` einmal, `weapons-mocap.glb` und die Waffen mit `GLTFLoader` samt `MeshoptDecoder`, wie `src/world/places.js`. +- **Je Dummy:** + - `SkeletonUtils.clone` des Mannequins, Schatten an. + - Ein eigener `AnimationMixer` mit zwei Aktionen: + - der Mocap-Clip mit `LoopRepeat`, + - die Fingerhaltung als konstanter Clip, nur mit den `.quaternion`-Spuren der Fingerknochen aus dem ersten Frame. Dummies mit Waffe bekommen sie für beide Hände aus `Pistol_Idle_Loop`, Dummies ohne Waffe aus `Idle_Loop`. + - Beide Aktionen bewegen verschiedene Knochen, keine Gewichtung nötig. +- **Waffe:** Kind des Handknochens (`hand_r` oder `hand_l`), mit dem Griff der Waffe aus `WEAPONS` (Position in m, Drehung in Grad, Reihenfolge XYZ). +- **Jedes Bild:** `mixer.update(delta · Tempo)`, außer bei Pause. + +### Panel (lil-gui) + +- **Tempo:** 0 bis 2 in Schritten von 0,05, Vorgabe 1. +- **Pause:** an/aus. +- **Griff:** + - Waffe auswählen. + - Position x/y/z von −0,3 bis 0,3 m, Drehung x/y/z von −180° bis 180°. + - Wirkt sofort auf alle Dummies mit dieser Waffe. + - „Werte ausgeben" schreibt den Griff als JSON in die Konsole, zum Übernehmen in `WEAPONS`. + +## Umbau der Animationen (`tools/assets/motions.mjs`) + +``` +node tools/assets/motions.mjs -> public/models/motions/weapons-mocap.glb +``` + +`tools/assets/fbx.mjs` lädt FBX in Node mit dem `FBXLoader` von three.js. Dazu setzt es die Browser-Platzhalter, die der Loader braucht (`self`, `window`, `document.createElementNS`), und blendet die Kamerawarnungen aus. Die Rechenschritte sind reine, exportierte Funktionen: + +1. **Clip-Name:** `/^\d{8}_s\d+_(.+)_ERJA-mvn\d+\.fbx$/` liefert zum Beispiel `idle_loadGun-Hand_tk01`. +2. **Abtasten:** + - D ist die Zeit von Frame 1 bis zum Clipende, n = round(D · 30) + 1. + - Der Quellclip läuft in einem `AnimationMixer` und wird an n gleichmäßig verteilten Zeitpunkten von Frame 1 bis zum Ende abgelesen, im Abstand D/(n−1), also etwa 1/30 s. + - Je Zeitpunkt werden die Weltdrehung jedes zugeordneten Knochens und die Weltposition von `Hips` abgelesen. + - Die Zeiten des neuen Clips laufen von 0 bis D. Frame 0 (T-Pose) fällt dadurch weg. +3. **Blickrichtung:** Der Hüft-Vorwärtsvektor des ersten Bilds zeigt in der Waagrechten um θ neben +Z. Alle Weltdrehungen und Hüftpositionen des Clips werden um −θ um die Hochachse gedreht. +4. **Ort:** + - Die waagrechte Hüftposition des ersten Bilds wird zu 0. + - Die Drift wird linear abgezogen: Bild i verliert i/(n−1) · (Ende − Anfang), in x und z. + - Ende und Anfang stehen danach am selben Punkt. Die Höhe bleibt unverändert. +5. **Übertragen:** + - Zuordnung MVN → Mannequin: `Hips`→`pelvis`, `Spine`→`spine_01`, `Spine1`→`spine_02`, `Spine3`→`spine_03`, `Neck`→`neck_01`, `Head`→`Head`. Je Seite außerdem `Shoulder`→`clavicle`, `Arm`→`upperarm`, `ForeArm`→`lowerarm`, `Hand`→`hand`, `UpLeg`→`thigh`, `Leg`→`calf`, `Foot`→`foot`, `ToeBase`→`ball`. + - Die Zuordnung folgt der gemessenen Höhe. `Spine2` hat kein Gegenstück, ihre Drehung geht über die Weltdrehung in `spine_03` ein. + - Weltdrehung Mannequin = Weltdrehung MVN · Ruhe-Weltdrehung Mannequin. Das gilt, weil beide Skelette in derselben T-Pose mit gleichem Blick ruhen und jede MVN-Ruhe-Weltdrehung 0° ist. + - Lokale Drehung = (Weltdrehung Elternknochen)⁻¹ · Weltdrehung. Eltern ohne Zuordnung (`root`, `Armature`) gehen mit ihrer Ruhepose ein. + - Hüfte: Position in m = Position in cm / 100 · (0,917 / Hüfthöhe der Datei), umgerechnet in den Raum von `root`. + - Finger, Blattknochen und `root` bekommen keine Spur. +6. **Schleife schließen:** + - Naht = größte Abweichung der Weltdrehung zwischen letztem und erstem Bild über alle Knochen. + - Überblendzeit b = Naht / 90 °/s, begrenzt auf 0,25 s bis 1,5 s. Das ergibt 1,01 s für den Bogenschützen und 0,25 s für alle übrigen. + - In den letzten b Sekunden werden alle lokalen Drehungen (slerp) und die Hüftposition (lerp) mit dem Gewicht smoothstep((t − (T − b)) / b) zum ersten Bild gezogen. Das letzte Bild ist dann gleich dem ersten. +7. **Schreiben:** + - gltf-transform-Dokument mit der Knotenhierarchie von `ual.glb` (Namen und Ruhe-TRS, ohne Mesh und Skin). + - Je Clip eine Animation mit gemeinsamem Zeit-Accessor, 22 Drehungskanälen und einem Translationskanal (`pelvis`), alle LINEAR und als Float32. + - Keine Kompression, geschätzt 1,6 MB (144 s × 30 Bilder × 91 Werte × 4 Byte). + +Die Clips binden über Knotennamen an die Knochen eines geklonten Mannequins. + +## Umbau der Waffen (`tools/assets/weapons.mjs`) + +``` +node tools/assets/weapons.mjs -> public/models/weapons/{pistol,ak47,m4,shotgun870,l96}.glb +``` + +- **Geometrie:** + - Positionen, Normalen und der erste UV-Satz kommen aus dem Mesh von `FBXLoader`. + - Die Knotendrehung wird angewendet, der Knotenmaßstab nicht. Die Eckpunkte liegen schon in Metern (siehe Befund). + - Längenprüfung: Pistole 0,15–0,25 m, Gewehre 0,7–1,3 m, sonst bricht das Skript mit Meldung ab. + - UV: v → 1 − v, weil FBX den Ursprung unten links hat und glTF oben links. + - Danach `weld()` und meshopt wie in `places.mjs`. +- **Material** (glTF metallicRoughness): + - Grundfarbe aus `T_` (sRGB, Alpha verworfen). + - Normalmap aus `N_`, unverändert. + - `metallicRoughnessTexture` wird neu zusammengesetzt: G = Grauwert der Maske (Rauheit), B = Alpha der Maske (Metall), R = 255. + - Beide Faktoren stehen auf 1. +- **TGA:** eigener Leser `readTga(buffer)` → `{ width, height, data }` in RGBA mit oberster Zeile zuerst. Er kann nur Typ 2 mit 32 Bit, bei allem anderen bricht er mit Meldung ab. Der Ursprung kommt aus Bit 5 von Byte 17. +- **Texturen:** `sharp` → WebP mit höchstens 1024 px Kantenlänge, die 512-px-Maske der Pistole bleibt 512 px. Qualität 80, Normalmaps 92, wie `places.mjs`. +- **AK47:** nur „Option 2". + +## Griff einpassen + +- **Werkzeug:** Die Griffwerte in `WEAPONS` entstehen im Browser mit dem Panel. +- **Ansicht:** Nahaufnahme von der Seite und von oben an einer Stelle des Clips, an der die Hand ruhig die Waffe hält. +- **Ziel:** Das Griffstück liegt in der Handfläche, der Lauf zeigt entlang der Hand nach vorn, und die Waffe steckt nicht im Unterarm. +- **Vorgehen:** Das Pack verspricht für Pistole, M4, 870, L96 und AK47 (Option 2) denselben Griffpunkt. Zuerst wird die Pistole eingepasst, ihr Wert ist dann der Startwert der übrigen. Jede Waffe bekommt trotzdem ihren eigenen Eintrag. + +## Tests + +- **`tests/motions.test.js`:** + - `clipId` liest alle 11 Dateinamen richtig. + - MVN in Ruhepose ergibt die Ruhepose des Mannequins, in lokalen Drehungen auf 1e-6. + - Eine Weltdrehung von 90° am linken Oberarm kommt als Weltdrehung von `upperarm_l` an (· Ruhe-Weltdrehung). + - Eine Drehung an `Spine3` dreht Schlüsselbein und Arm in der Welt mit, deren lokale Drehung bleibt gleich. + - Hüfte: cm → m, Faktor 0,917 / Hüfthöhe, Raum von `root`. + - Blickrichtung: Nach Schritt 3 zeigt die Hüfte im ersten Bild nach +Z. + - Ort: Das erste Bild steht bei x = z = 0, das letzte ebenso. + - Abtastung: n = round(D · 30) + 1 Zeiten von 0 bis D in gleichem Abstand, das erste Bild entspricht Frame 1 der Quelle. + - Schleife: Letztes und erstes Bild sind gleich. Die Überblendzeit hält die Grenzen 0,25 s und 1,5 s ein. +- **`tests/weapons.test.js`:** + - `readTga` liest eine künstliche 2 × 2-Datei richtig, mit Ursprung unten und oben. + - Typ 10 (RLE) und 24 Bit werden abgelehnt. + - Maske → metallicRoughness: G = Grau, B = Alpha, R = 255. + - UV: v → 1 − v. + - Die Längenprüfung schlägt bei falscher Einheit an (Pistole mal 100). +- **`tests/showroom.test.js`:** + - 13 Dummies. Jeder Clip existiert in `weapons-mocap.glb`, jede Waffe hat eine Datei in `public/models/weapons/` und einen Griff. + - Kein Platz näher als 3,5 m an einem anderen. Um den Bogenschützen sind mindestens 4 m frei. + - Jede Waffen-GLB hat ein Mesh, meshopt, WebP-Texturen bis 1024 px und liegt in den Längengrenzen. +- **Browser:** + - `animationen.html` lädt ohne Konsolenfehler, alle 13 Mixer laufen. + - Übersichtsbild und Nahaufnahme je Waffe. + - Schleifennaht: Die Knochendrehungen eines Dummys ein Bild vor und ein Bild nach dem Rücksprung liegen nicht weiter auseinander als zwei benachbarte Bilder mitten im Clip, gemessen an den Knochen mit der größten Naht vor dem Umbau. + - Die Spielfigur läuft per Tastatur, ihre Position ändert sich. + - Bildzeit mit allen Dummies. + - Maskenbelegung auf Sicht: Das Holz der AK47 glänzt nicht metallisch. +- **Gesamt:** `npm test` grün, `npm run build` baut beide Seiten, `index.html` lädt weiter wie bisher. + +## Fehlerfälle und Risiken + +- **Maskenbelegung falsch:** Glänzt Holz metallisch oder wirkt Metall stumpf wie Kunststoff, werden Grau und Alpha getauscht und neu gebaut. Die Browserprüfung oben fängt das. +- **Proportionen:** + - Der Darsteller ist kleiner als das Mannequin, der Kopf liegt bei 1,506 m statt 1,569 m. + - Beim Taunt kann die Pistole in den Kopf ragen, die Hände können sich durchdringen, die Füße ein paar Zentimeter schweben oder einsinken. + - Es gibt keine IK. Auffälliges wird im Gespräch entschieden. +- **Linke Hand am Gewehr:** Sie ist nicht an den Handschutz gebunden und liegt dort, wo die Mocap-Daten sie hinlegen. +- **Blickrichtung:** Nach Schritt 3 schauen die Hüften nach vorn. Der Granatwurf mit −44° Anfangsdrehung zielt danach entsprechend schräg. +- **Bogenschütze:** Er wandert bis 3,13 m (× Maßstab) von seinem Platz und dreht sich dabei. Die Überblendung am Ende dauert 1 s. +- **Node-Platzhalter für `FBXLoader`:** Sie hängen an three 0.184.0 (in `package.json` fest). Ein Update kann den Umbau brechen, die erzeugten GLBs bleiben davon unberührt. +- **Lizenz:** Die Zips enthalten keine Lizenzdatei. Die erzeugten GLBs werden wie die übrigen Modelle versioniert (Remote `git.webklar.com`), die Zips selbst nicht (`*.zip` in `.gitignore`). + +## Nicht Teil dieser Arbeit + +- Dummies in der Leonida-Welt. +- Waffen für die Spielfigur (Tragen, Zielen, Schießen). +- IK für Hände oder Füße. +- Kollision mit Dummies. +- Superhero-Figuren oder Frisuren als Dummies. +- AK47 „Option 1". +- Eigene Modelle für Messer, Granate und Bogen.