Files
gta6/docs/superpowers/specs/2026-09-17-animationen-design.md
T
knsoandClaude Opus 5 a0cd95cceb fix(showroom): Funde der Schlussdurchsicht auf der Testseite behoben
Die Spielfigur wirft jetzt einen Schatten (player.init durchlief applyCharacter
nie); drei stille Fehlerpfade meckern jetzt statt roh abzubrechen (fehlender
Clip fuer die Fingerhaltung, Waffe ohne Griff, mit neuem Test); die
Waffenauswahl im Panel zeigt Beschriftungen statt IDs; der Waffen-Umbau prueft
jetzt die Meshzahl je Datei; stageTransform ist aus main.js nach lineup.js
gezogen und mit dem im Browser gemessenen Fall getestet; slotPosition nimmt
DUMMIES als Argument statt es aus dem Modul zu lesen; dazu Kommentar- und
Entwurfskorrekturen (Docstring, Beckenspur-Test, Frustum-Culling, Port/Kamera/
VIEW_DISTANCE im Entwurf).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 03:19:42 +02:00

379 lines
29 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.
# 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`. Die Seite läuft über ihre eigene `.claude/launch.json`-Konfiguration `animationen` auf Port 5180; das Preview-Werkzeug erlaubt in einer localhost-Adresse keinen Pfad, deshalb wird der Server gestartet und danach im Browser selbst auf `/animationen.html` navigiert.
- **`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).
- **Kamera:** Der Nahwert ist 0,1 statt 0,5 wie im Spiel: Hier steht nur eine Ebene, es gibt keine übereinanderliegenden Flächen, die im Tiefenpuffer flackern könnten, dafür geht die Kamera näher an die Dummies heran.
- **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 Abstand zur Spielfigur. 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:** Die Figur beginnt über dem Ursprung. Wohin die Kamera anfangs schaut, bestimmt der Controller: `PLAYER_MODEL_BASE.rotateY` dreht die Kapsel um −90°, und `CameraSystem.ts` setzt die Kamera daraus seitlich. Deshalb wird nicht die Figur versetzt, sondern die Aufstellung. Nach dem ersten Bild steht `stage` 8 Mannequin-Meter vor der Figur in Blickrichtung der Kamera und ist so gedreht, dass die Dummies zur Figur schauen.
### 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 innerhalb von `stage` nach +Z, also zur Spielfigur (siehe Start). Die Clips sind darauf ausgerichtet (Umbau, Schritt 3), die Dummies selbst werden nicht gedreht.
- **Abstand zur Spielfigur:** `VIEW_DISTANCE = 8` ist bewusst nah gewählt, keine Nachlässigkeit: Beim Start stehen vier der acht Dummies aus Reihe 1 im Bild, die übrigen erreicht man mit wenigen Schritten.
- **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 <Ordner mit den 11 FBX> -> 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 <Ordner WeaponsPack> -> 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.
- **Befund (gemessen, Plan-Task 9):** Startwert `[0, 0, 90]` bringt die Längsachse der Waffe zwar korrekt auf die
Knochenachse der Hand (im Browser mit einem Vektorvergleich Fingerrichtung/Waffen-X bestätigt, Punktprodukt 1),
aber der Handknochen selbst sitzt am Handgelenk — ohne Positionsversatz reicht die Waffe deshalb bis in den
Unterarm. Mit `position.y = 0,09` (≈ halbe Handlänge, in Richtung der Finger) rutscht der Griffpunkt in die
Handfläche. Dieser Wert passt für alle fünf Waffen unverändert:
- Pistole, AK47, M4, 870 Shotgun: `position: [0, 0.09, 0]`, `rotation: [0, 0, 90]`.
- L96: gleiche Position, aber `rotation: [0, 0, -90]`. Ihre Bounding-Box liegt seitenverkehrt zu den anderen vier
(Länge und Höhe tauschen die Seite, an der der Ursprung sitzt) — 180° zusätzliche Drehung um die eigene
Querachse gleicht das aus. Über den ganzen Clip geprüft (AK47, Anfang/Mitte/Ende von `idle_loadGun-Rifle_tk04`):
Griff bleibt an der Hand.
- **Hand je Clip:** An allen acht Waffen-Dummies hält die rechte Hand (`hand_r`) die Waffe, die Vorgabe `hand: "r"`
stimmt. Bei „Pistole/Gewehr nachladen" sieht man das am Bewegungsablauf: Die rechte Hand bleibt vorn auf Höhe der
Waffe, während die linke zum Magazin greift und beide Hände sich kurz treffen, wenn das Magazin eingesetzt wird.
Bei „Schuss in den Kopf" führt ebenfalls die rechte Hand die Pistole zum Kopf.
- **Maske und Normalmap:** An der AK47 im Browser geprüft (Nahaufnahme mit Holzschaft und Metall-Receiver im selben
Bild plus Punktproben aus der `metallicRoughness`-Textur per Raycast/UV): Holz liegt bei Rauheit ≈ 0,85 und
Metallwert ≈ 0,22, der Receiver bei Rauheit ≈ 0,97 und Metallwert ≈ 0,69 — Metall ist also deutlich metallischer
als Holz, die Kanäle sind nicht vertauscht. Beide Flächen wirken unter dem bedeckten Himmel der Seite eher matt,
weil die Rauheit auf beiden hoch ist (beim Receiver sogar am höchsten) — das liest sich wie ein mattes,
parkerisiertes Finish, nicht wie ein vertauschter Kanal. Die Normalmap ist nicht flach (Mittelwert nahe
`(128, 128, 255)` mit echter Streuung an den Randwerten von Blau) und liegt korrekt ohne Farbraum vor. Kein
Tausch nötig.
## 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: Bei jedem Dummy ist der größte Knochenwinkel zwischen der Pose 1/30 s vor und 1/30 s nach dem Rücksprung nicht größer als der größte Winkel zwischen zwei Posen im Abstand von 2/30 s im Clipinneren.
- 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.
## Gemessen im Browser (2026-09-18)
Schlussprüfung (Plan-Task 10): `animationen.html` über den Dev-Server geladen, `window.__showroom` genutzt.
Die Browser-Pane lief dabei verborgen, `requestAnimationFrame` also gedrosselt — Bildtakt und Bildzeit wurden
deshalb von Hand über `loop(time)` angetrieben statt über echtes `requestAnimationFrame` (Einzelheiten im
Task-Bericht).
- **Dateigrößen** (`fs.statSync`, dezimale MB): `weapons-mocap.glb` 1,66 MB. Waffen: Pistole 0,16 MB, AK47
0,23 MB, M4 0,21 MB, 870 Shotgun 0,12 MB, L96 0,14 MB.
- **Maßstab und Dummy-Größe:** `figureScale` (Weltmaßstab von `player.playerModel`, wie in Task 8) ist 0,4921.
Die Mannequin-Ruhepose ist 1,83 m hoch (Befund oben); Dummies und Spielfigur sind damit gleich groß, je
1,83 × 0,4921 ≈ 0,90 m.
- **Hand je Clip:** An allen acht Waffen-Dummies bestätigt: `hand_r`, wie in `DUMMIES` hinterlegt
(`hand: "r"`). Keine Änderung nötig.
- **Griffwerte je Waffe** (aus `WEAPONS`, unverändert seit Task 9): Pistole, AK47, M4, 870 Shotgun
`position [0, 0.09, 0]`, `rotation [0, 0, 90]`; L96 gleiche Position, `rotation [0, 0, -90]`.
- **Schleifennaht je Dummy:** Naht (Winkel zwischen der Pose 1/30 s vor und 1/30 s nach dem Rücksprung) gegen
größten Zweibild-Schritt im Clipinneren (Abstand 2/30 s), in Grad. Bei allen 13 Dummies ist die Naht
kleiner als der größte Schritt:
| Reihe, Platz | Clip | Waffe | Naht | Größter Schritt |
|---|---|---|---|---|
| 1, 1 | `idle_loadGun-Hand_tk01` | Pistole | 0,9° | 46,2° |
| 1, 2 | `idle_loadGun-Hand_tk02` | Pistole | 0,9° | 49,4° |
| 1, 3 | `taunt_shootSelfInHead_tk01` | Pistole | 0,7° | 39,5° |
| 1, 4 | `taunt_shootSelfInHead_tk02` | Pistole | 0,7° | 44,7° |
| 1, 5 | `idle_loadGun-Rifle_tk04` | AK47 | 0,8° | 55,0° |
| 1, 6 | `idle_loadGun-Rifle_tk05` | M4 | 0,6° | 30,4° |
| 1, 7 | `idle_loadGun-Rifle_tk04` | 870 Shotgun | 0,8° | 55,0° |
| 1, 8 | `idle_loadGun-Rifle_tk05` | L96 | 0,6° | 30,4° |
| 2, 1 | `stabTwist_Knife_tk01` | — | 0,9° | 58,5° |
| 2, 2 | `stabTwist_Knife_tk02` | — | 0,8° | 61,3° |
| 2, 3 | `leftHandThrow_grenade_tk01` | — | 1,0° | 51,3° |
| 2, 4 | `leftHandThrow_grenade_tk02` | — | 0,6° | 59,7° |
| 2, 5 | `hunter_bowArrow_tk01` | — | 1,2° | 40,7° |
Gleiche Clips (Reihe 1, Platz 5/7 und 6/8) ergeben gleiche Werte, weil die Naht eine Eigenschaft des Clips
ist, nicht der Waffe.
- **Bildzeit:** Gemessen als Zeit eines vollständig gezeichneten Bildes (CPU- und GPU-Zeit zusammen), weil die
`requestAnimationFrame`-Kadenz aus dem Plan in der verborgenen Pane nicht läuft: je Bild `performance.now()`
vor `loop(time)`, danach `gl.finish()` (erzwingt das Warten auf die GPU), `performance.now()` danach. Über
60 so angetriebene Bilder mit allen 13 Mixern aktiv: Median 2,9 ms, Maximum 3,5 ms — weit unter den 20 ms
Richtwert aus dem Plan. Dabei `renderer.info.render`: 45 Draw Calls, 209 145 Dreiecke (deckungsgleich mit
der Messung aus Task 8).
- **Maske und Normalmap:** Beim erneuten Betrachten der AK47 in dieser Prüfung kein Hinweis auf vertauschte
Kanäle oder eine flache Normalmap. Deckt sich mit dem in „Griff einpassen" festgehaltenen, dort rechnerisch
belegten Befund (Holz Rauheit ≈ 0,85 / Metallwert ≈ 0,22, Receiver Rauheit ≈ 0,97 / Metallwert ≈ 0,69); hier
nur gegengeprüft, nicht neu vermessen.
- **Durchdringungen:**
- **Taunt am Kopf:** Bei beiden Take von `taunt_shootSelfInHead` liegt die Pistole sichtbar an der
Kopfseite/Schläfe an, in zwei Kamerawinkeln geprüft. Kleinster gemessener Abstand zwischen
Waffenursprung und dem `Head`-Knochen: 4,3 cm (Take 2, bei t = 2,48 s im Clip). Die in „Fehlerfälle und
Risiken" vermerkte Möglichkeit „Pistole kann in den Kopf ragen" bestätigt sich als sichtbarer Kontakt.
- **Linke Hand am Gewehr:** Die AK47 liegt erkennbar im beidhändigen Griff (rechte Hand hinten am
Griffstück). Ein unverdeckter Blick auf die linke Hand gelang in dieser Prüfung nicht, die Kamera folgt
der Spielfigur und ließ sich nicht frei zur Seite stellen. Gemessen: kleinster Abstand zwischen
`hand_l` und dem Waffenursprung 3,7 cm (bei t = 2,25 s im Clip `idle_loadGun-Rifle_tk04`) — die Hand ist
damit sehr nah am Gewehrkörper; eine sichtbare Durchdringung wurde nicht geprüft.
- **Spiel:** `http://localhost:5180/` neu geladen, Welt stand nach wenigen Sekunden. Einzige Konsolenfehler:
viermal `WrongDocumentError: pointer lock` (bekanntes Artefakt der verborgenen Pane, schon in Task 8/9
dokumentiert) und einmal `net::ERR_BLOCKED_BY_CLIENT`. Der Netzwerk-Mitschnitt zeigt für alle eigenen
Ressourcen der Seite (Skripte, Texturen, Modelle, Chunk-Daten) Status 200; das blockierte Element gehört zu
keiner davon. Kein neuer, dieser Änderung zuzuordnender Fehler.
## 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.