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

29 KiB
Raw Blame History

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.