Eine offene Welt im Browser bauen, Teil 17: Animationen, die kein Retargeting brauchten, und eine Live-Asset-Suche
Von Oleg Sidorkin, CTO und Mitgründer von Cinevva
Neu hier? Nutze den Serienleitfaden. Er erklärt, was ein Spike ist, und verlinkt alle Teile.
Teil 16 gab uns eine Welt, in der man Objekte platzieren kann. Dieser Teil gibt dem Spieler etwas Besseres, das er darin tun kann: einen kampftauglichen Animationssatz und eine Möglichkeit, durch Eingabe eines Suchbegriffs jedes von tausend CC0-Modellen in die Welt zu laden.
262 Clips, ein Skelett, drei Retargeting-Korrekturen
Spike 35 in einem neuen Tab öffnen ↗ · Quellcode ansehen
Das Spieler-Rig ist ein CC4-artiges 3MIKE-Skelett mit 213 Bones, darunter ein Gesichts-Rig mit mehr als 80 Bones und vollständigen Fingergelenken. Die Animationsquellen passen nicht dazu. Der erste Ansatz übertrug handverlesene Mixamo-Kampfclips, doch der Quellenbestand war dünn, und die Fortbewegung war ungünstig zwischen Mixamo und einigen wenigen Kimodo-BVH-Idle-Animationen aufgeteilt. Der eigentliche Durchbruch gelang, als wir die Quellbibliothek auf die beiden Pakete der Universal Animation Library von Quaternius umstellten: insgesamt 262 Clips auf einem einheitlichen UE5-artigen Mannequin mit 65 Bones – Schwertkombinationen, Bogenschießen, Klettern, Wallruns, Ausweichbewegungen, Trefferreaktionen, Emotes und ein deutlich saubererer Fortbewegungssatz.
Das bedeutet eine Quelle mit 65 Bones für ein Ziel mit 213 Bones – ohne übereinstimmende Bone-Namen, T-Pose-Ausrichtungen oder Gliedmaßenproportionen. Jeder Clip wird in drei Schritten neu zugeordnet: über eine Zuordnung der Bone-Namen, eine Bind-Pose-Ausrichtung, damit beide Rigs dieselbe Referenzorientierung verwenden, und eine Skalierung der Positionstracks, damit das kleinere Quell-Rig die Figur nicht knietief im Boden versinken lässt. Um das korrekt hinzubekommen, mussten wir eine Reihe von Fehlern aufspüren, von denen jeder etwas Konkretes lehrte.
Die Figur war übermäßig verdreht; jede Schulter und jeder Ellbogen waren um etwa 30° zu weit rotiert. Die Ursache war eine nicht übereinstimmende Pose: Die UAL-Quelle wird in einer echten T-Pose geliefert, die Bind-Pose von CC4 ist eine A-Pose, und der Retargeter ging davon aus, dass beide Rigs dieselbe Referenzpose einnehmen. Dadurch wurde die Differenz zwischen A- und T-Pose zu jedem Delta pro Frame addiert. Die Korrektur zwingt die Armkette von CC4 zum Erfassen der Bind-Pose in eine echte T-Pose und führt anschließend das Retargeting durch, sodass die Deltas klein bleiben.
Dann bewegte sich der Körper nicht mehr fort. Bei Rückstößen wich der Oberkörper zurück, während die Füße an Ort und Stelle blieben. UAL legt die Verschiebung auf dem Bone root ab, nicht auf dem Becken. Das Auslesen der Beckenposition ergab daher nahezu keine Bewegung. Die Korrektur addiert root.position und pelvis.position, skaliert den horizontalen Anteil und schreibt einen einzigen Track für die Hüftposition. Dabei stellten wir fest, dass die vertikalen Offsets um etwa 12 % abwichen, weil wir für die Y-Achse das globale Verhältnis der Gliedmaßenproportionen verwendet hatten, obwohl für die Beckenhöhe das Verhältnis von Hüfte zu Boden nötig ist. Zwei Verhältnisse, zwei Achsen: RL_BoneRoot.position ließ sich darauf zurückführen, dass UALs root (bei
Die kleinste Korrektur war beim Zuschauen am befriedigendsten. Die Figur hielt das Schwert mit schlaffen Fingern in der Bind-Pose, weil die Bone-Zuordnung keine Fingereinträge enthielt und der Retargeter nur Tracks für zugeordnete Bones schreibt. Durch das Hinzufügen von 30 Finger-Bones – fünf Finger, drei Segmente, zwei Hände, wobei UALs vierter Spitzen-Helfer ohne Verformungsfunktion entfiel – schloss sich die Hand um den Griff und öffnete sich beim Loslassen.
262 Clips prüfen, ohne 262 Clips anzusehen
Eine Bibliothek mit 262 Clips lässt sich nicht per Augenmaß vollständig überprüfen. Deshalb entwickelten wir einen Offline-Test für die Trajektorienparität: ein Headless-Node-Skript, das jedes Paket lädt, das Quell-Skelett mit 60 Hz abtastet, die Retargeting-Pipeline ausführt und die Weltpositionen und -rotationen jedes Bones nach der Skalierung mit der Quelle vergleicht. Die maximale Abweichung des Beckens auf der Y-Achse lag bei 0,003 m. Die Hände zeigten einen konstanten Offset von 2,5°, der zunächst wie „die Finger folgen der Animation nicht“ wirkte. Ein konstanter Offset ist jedoch das Bind-Delta zwischen der flachen UAL-Hand und der leicht gewölbten CC4-Bind-Pose und bleibt über den gesamten Clip unverändert. Echte Animationsfehler zeigen sich als Abweichung, die sich von Frame zu Frame verändert. Sobald das klar war, wurde der Test zu einer einmaligen Regressionsprüfung: Bleibt die Abweichung eines Clips nicht mehr auf dieser konstanten Basislinie, hat eine kürzliche Änderung das Retargeting beschädigt.
Ein direkt daneben angezeigtes Referenz-Mannequin machte die visuelle Hälfte der Fehlersuche eindeutig. Ein Druck auf die Rückwärtsschrägstrich-Taste zeigt neben dem Spieler das Quell-Rig an, zu dem der aktuelle Clip gehört. So wird aus der Frage nach einer „verdrehten Schulter“ die Frage: „Ist die Verdrehung bereits in der Quelle vorhanden, oder wurde sie durch das Retargeting hinzugefügt?“ Numerische Tests erkennen Regressionen, die visuelle Referenz deckt Bind-Pose-Fehler auf, die in den Zahlen nicht sichtbar werden, und gemeinsam beendeten sie das Rätselraten.
Da die Pipeline nun stabil war, erforderte die Umstellung der WASD-, Sprung- und Schwimm-Basis von Mixamo auf UAL nur eine schlanke Alias-Zuordnung: Die FSM verwendet weiterhin generische Zustandsnamen wie idle und walk, die bei der Wiedergabe in UAL-Clipnamen aufgelöst werden. Das Schwimmen benötigte Rig-Offsets pro Clip, weil die Posen für Kraulen und Wassertreten das Becken auf unterschiedlichen anatomischen Höhen verankern. Daher versenken wir das Rig beim aktiven Schwimmen um einen halben Meter und beim Wassertreten noch tiefer und blenden mit gleichmäßigen 5 Hz zwischen beiden Zuständen über.
Ein Wort eingeben, ein Modell erhalten
Spike 36 in einem neuen Tab öffnen ↗ · Quellcode ansehen
Spike 34 verschlang einen ganzen Tag für die manuelle Zusammenstellung eines CC0-Pakets. Langfristig ist ein Suchfeld die richtige Lösung. Polyhaven veröffentlicht über eine unkompliziert nutzbare JSON-API und ein deterministisches CDN etwa 1.100 CC0-Modelle. Dieser Spike verbindet den gesamten Ablauf – Abfrage, Vorschaubilder, Laden und Rendern – von einer statischen Seite ohne Build-Schritt aus, mit ungefähr 300 Zeilen Vanilla-JS plus three.js.
Beim Start wird der vollständige Katalog einmalig geladen, etwa 600 KB. Die Suche verwendet eine rein clientseitige Gewichtung – Name vor ID, Kategorie vor Tag – mit 120 ms Entprellung und rendert die besten 60 Karten. Vorschaubilder werden über einen IntersectionObserver verzögert geladen, damit das Tippen nicht 60 Anfragen auf einmal auslöst. Der interessante Teil ist das Laden. Polyhavens Datei-Endpunkt stellt glTF mit mehreren Dateien bereit, nicht GLB. Texturen werden über mehrere Auflösungen hinweg gemeinsam genutzt und liegen in separaten Dateien vor. Zusätzlich liefert der Endpunkt eine include-Zuordnung von relativen Pfaden zu absoluten CDN-URLs zurück. Anstatt das JSON selbst herunterzuladen und anzupassen, leiten wir diese Zuordnung durch LoadingManager.setURLModifier. Das wird für jede vom Loader benötigte Abhängigkeit aufgerufen – die .bin-Datei und jede Textur – und löst sie über das CDN auf. Ein Klick, scheinbar eine Datei. Sowohl die API als auch das CDN setzen unkomplizierte CORS-Header, was wir vor jeglichem Client-Code mit curl überprüft haben. Daher ist kein Proxy nötig. PBR-Materialien werden mit RoomEnvironment und den Standardwerten des ACES-Tone-Mappings korrekt gerendert, ohne Anpassungen für einzelne Assets. Mit 1k-Texturen bleibt ein typisches Modell bei 2 bis 5 MB statt 20 bis 40 MB mit 4k-Texturen.
Ein meshoptimizer-WASM-Durchlauf rundet das Ganze mit einem nichtdestruktiven Reduzierer ab: Jedes Mesh speichert einen Klon seiner ursprünglichen Geometrie, und eine Änderung des Verhältnisses erstellt den Indexpuffer aus diesem Klon neu, statt die Geometrie schrittweise immer weiter zu vereinfachen. Geometrie mit mehreren Materialien wird pro Gruppe vereinfacht, und geometry.groups wird neu aufgebaut, damit die Material-Slots nicht zusammenfallen. Ein Sessel wird von 5.626 Dreiecken bei voller Auflösung auf 2.812 bei halber Auflösung reduziert.
In diesem Kapitel behandelte Technologien
Skelett-Retargeting mit Bind-Pose-Ausrichtung. Das Übertragen einer Animation von einem Skelett auf ein anderes mit abweichenden Bone-Namen, Proportionen und Referenzposen erfordert drei Korrekturen: eine Zuordnung der Bone-Namen, eine Bind-Pose-Ausrichtung, damit beide Rigs dieselbe Referenzorientierung verwenden – wobei die A-Pose-Armkette des Ziels in die T-Pose der Quelle gezwungen wird – und eine Skalierung der Positionstracks. Eine nicht übereinstimmende Pose addiert die Rotation von der A- zur T-Pose zu jedem Delta pro Frame und verdoppelt damit die Gelenkrotation. Nicht zugeordnete Helfer-Bones müssen entfernt werden, da ein Root-Bone bei
Zwei Skalierungsverhältnisse für ein Rig. Die horizontale Verschiebung verwendet das globale Verhältnis der Gliedmaßenproportionen – also die Gesamtgröße des Skeletts –, während der vertikale Becken-Offset das Verhältnis von Hüfte zu Boden root, nicht auf dem Becken. Daher müssen beide zu einem einzigen Track für die Hüftposition addiert werden, damit die Bewegung bei Rückstoß-, Kletter- und Fortbewegungsclips erhalten bleibt.
Offline-Tests für Trajektorienparität. Ein Headless-Skript tastet das Quell-Skelett mit 60 Hz ab, führt die Retargeting-Pipeline aus und vergleicht die Welttransformationen jedes Bones mit der Quelle. Ein konstanter Offset pro Frame ist das harmlose Bind-Delta, während eine von Frame zu Frame variierende Abweichung ein echter Fehler ist. Dadurch wird der Test zu einer Regressionsprüfung, die anschlägt, sobald eine Änderung einen Track entfernt oder verzerrt. Die GLB-Isolation pro Paket – frisch laden und vor dem nächsten Paket freigeben – vermeidet Cache-Konflikte während des gesamten Durchlaufs.
LoadingManager.setURLModifier für glTF-Graphen aus einem CDN. Wenn ein CDN glTF als Graph aus relativen URIs zusammen mit einer Include-Zuordnung – relativer Pfad zu absoluter URL – ausliefert, löst setURLModifier jede vom Loader angeforderte Abhängigkeit über das CDN auf, ohne das JSON umzuschreiben. Dadurch wird eine aus mehreren Dateien und Auflösungen bestehende Distribution zu einem Ladevorgang mit nur einem Klick. Wenn vor dem Laden des nächsten Modells die Geometrie, Materialien und Textur-Maps des vorherigen Modells freigegeben werden, sammeln sich während einer Browsing-Sitzung nicht Hunderte Megabyte GPU-Speicher an.
Nichtdestruktive Mesh-Vereinfachung. Wird ein Klon der ursprünglichen Geometrie jedes Meshes gespeichert und pro Vereinfachungsverhältnis nur der Indexpuffer neu aufgebaut, bleiben Änderungen schnell und wiederholte Vereinfachungen verursachen keine kumulativen Schäden. Die Verarbeitung pro Ausschnitt von geometry.groups und das anschließende Rekonstruieren der Gruppen bewahren die Zuordnung mehrerer Materialien. Unter LOD und meshoptimizer wird erläutert, wie dies in entfernungsbasiertes LOD einfließt.
Teil 17 von 29. Zurück: Teil 16 – Struktur für eine Welt, die immer weiter wächst Weiter: Teil 18 – Ein Streupinsel, dessen Ergebnisse wie von einer KI platziert wirken Serienleitfaden: /de/blog/2026-02-25-open-world-browser-series-guide