Wie programmiere ich Animationen von einem Skelett zum anderen?

23

Ich versuche, Code zu schreiben, um Animationen zu übertragen, die für ein Skelett entwickelt wurden, um auf einem anderen Skelett korrekt auszusehen. Die Quellanimationen bestehen nur aus Rotationen mit Ausnahme von Übersetzungen im Stammverzeichnis (dies sind die Mocap-Animationen aus der CMU-Bewegungserfassungsdatenbank ). Viele 3D-Anwendungen (z. B. Maya) haben diese Funktion integriert, aber ich versuche, eine (sehr einfache) Version für mein Spiel zu schreiben.

Ich habe einige Arbeiten zur Knochenzuordnung durchgeführt, und da die Skelette hierarchisch ähnlich sind (Zweibeiner), kann ich für alles außer für die Wirbelsäule eine 1: 1-Knochenzuordnung durchführen (kann später daran arbeiten). Das Problem ist jedoch, dass die Grund-Skelett- / Bindungs-Posen unterschiedlich sind und die Knochen unterschiedliche Maßstäbe (kürzer / länger) haben. Wenn ich also nur die Drehung direkt darüber kopiere, sieht es sehr seltsam aus.

Ich habe eine Reihe von Dingen versucht, die der folgenden Lösung von Lorancou ähnlich sind, ohne Erfolg (dh Multiplikation jedes Einzelbilds in der Animation mit einem knochenspezifischen Multiplikator). Wenn jemand Ressourcen für solche Dinge hat (Papiere, Quellcode usw.), wäre das wirklich hilfreich.

Robert Fraser
quelle
Wie soll ich den Schwanz und das Ding zwischen den Beinen ignorieren? : P
kaoD
2
@kaoD Wenn Sie fragen müssen, ist das Skelett bei (0,0) verwurzelt, sodass dort ein künstlicher Knochen vorhanden ist. Was den Schwanz betrifft ... jeder weiß, dass das Leben besser ist, wenn Sie einen Schwanz haben. Ich habe immer gedacht, es wäre effizient für Dinge wie das Tragen von Kaffeetassen und das Balancieren auf Ästen.
Robert Fraser
Ich habe eine Echtzeit-Demo davon gesehen, in der ein Kinect verwendet wurde, um ein in xna angezeigtes Modell zu animieren. Denke, der Code befand sich auf einer Open-Source-Site. Sucht ...
George Duckett
Ich vermute, dass Ihr Problem mehr mit den unterschiedlichen Bindungsposen als mit der Skalierung der Knochen zusammenhängt. Sie könnten versuchen, dies zu isolieren. Gehen Sie beispielsweise vom ursprünglichen Skelett aus, skalieren Sie einige Knochen darauf, um ein neues Skelett zu erstellen, und prüfen Sie, ob Ihr Algorithmus mit diesem Skelett zusammenbricht. Wenn dies nicht der Fall ist, starten Sie vom ursprünglichen Skelett aus neu, aber skalieren Sie die Knochen diesmal nicht. Drehen Sie sie einfach und prüfen Sie, ob Ihr Algorithmus kaputt geht. Wenn ja, dann gibt es wahrscheinlich eine zusätzliche Transformation, die irgendwo durchgeführt werden muss.
Laurent Couvidou

Antworten:

8

Das Problem war die numerische Stabilität. Ungefähr 30 Stunden Arbeit in 2 Monaten, nur um herauszufinden, dass ich es von Anfang an richtig gemacht habe. Wenn ich die Rotationsmatrizen vor dem Einfügen in den Retarget-Code orthonormiert habe, hat die einfache Lösung des Multiplizierens von Quelle * Inverse (Ziel) perfekt funktioniert. Natürlich ist Retargeting viel mehr als das (insbesondere unter Berücksichtigung der unterschiedlichen Formen des Skeletts, dh der Schulterbreite usw.). Hier ist der Code, den ich für den einfachen, naiven Ansatz verwende, wenn jemand neugierig ist:

    public static SkeletalAnimation retarget(SkeletalAnimation animation, Skeleton target, string boneMapFilePath)
    {
        if(animation == null) throw new ArgumentNullException("animation");
        if(target == null) throw new ArgumentNullException("target");

        Skeleton source = animation.skeleton;
        if(source == target) return animation;

        int nSourceBones = source.count;
        int nTargetBones = target.count;
        int nFrames = animation.nFrames; 
        AnimationData[] sourceData = animation.data;
        Matrix[] sourceTransforms = new Matrix[nSourceBones];
        Matrix[] targetTransforms = new Matrix[nTargetBones];
        AnimationData[] temp = new AnimationData[nSourceBones];
        AnimationData[] targetData = new AnimationData[nTargetBones * nFrames];

        // Get a map where map[iTargetBone] = iSourceBone or -1 if no such bone
        int[] map = parseBoneMap(source, target, boneMapFilePath);

        for(int iFrame = 0; iFrame < nFrames; iFrame++)
        {
            int sourceBase = iFrame * nSourceBones;
            int targetBase = iFrame * nTargetBones;

            // Copy the root translation and rotation directly over
            AnimationData rootData = targetData[targetBase] = sourceData[sourceBase];

            // Get the source pose for this frame
            Array.Copy(sourceData, sourceBase, temp, 0, nSourceBones);
            source.getAbsoluteTransforms(temp, sourceTransforms);

            // Rotate target bones to face that direction
            Matrix m;
            AnimationData.toMatrix(ref rootData, out m);
            Matrix.Multiply(ref m, ref target.relatives[0], out targetTransforms[0]);
            for(int iTargetBone = 1; iTargetBone < nTargetBones; iTargetBone++)
            {
                int targetIndex = targetBase + iTargetBone;
                int iTargetParent = target.hierarchy[iTargetBone];
                int iSourceBone = map[iTargetBone];
                if(iSourceBone <= 0)
                {
                    targetData[targetIndex].rotation = Quaternion.Identity;
                    Matrix.Multiply(ref target.relatives[iTargetBone], ref targetTransforms[iTargetParent], out targetTransforms[iTargetBone]);
                }
                else
                {
                    Matrix currentTransform, inverseCurrent, sourceTransform, final, m2;
                    Quaternion rot;

                    // Get the "current" transformation (transform that would be applied if rot is Quaternion.Identity)
                    Matrix.Multiply(ref target.relatives[iTargetBone], ref targetTransforms[iTargetParent], out currentTransform);
                    Math2.orthoNormalize(ref currentTransform);
                    Matrix.Invert(ref currentTransform, out inverseCurrent);
                    Math2.orthoNormalize(ref inverseCurrent);

                    // Get the final rotation
                    Math2.orthoNormalize(ref sourceTransforms[iSourceBone], out sourceTransform);
                    Matrix.Multiply(ref sourceTransform, ref inverseCurrent, out final);
                    Math2.orthoNormalize(ref final);
                    Quaternion.RotationMatrix(ref final, out rot);

                    // Calculate this bone's absolute position to use as next bone's parent
                    targetData[targetIndex].rotation = rot;
                    Matrix.RotationQuaternion(ref rot, out m);
                    Matrix.Multiply(ref m, ref target.relatives[iTargetBone], out m2);
                    Matrix.Multiply(ref m2, ref targetTransforms[iTargetParent], out targetTransforms[iTargetBone]);
                }
            }
        }

        return new SkeletalAnimation(target, targetData, animation.fps, nFrames);
    }
Robert Fraser
quelle
Wurde der Code auf dieser Seite aktualisiert, seit er geschrieben wurde? Es ist schwierig zu verstehen, ohne den Kontext der Engine, die es verwendet. Ich versuche auch, Animations-Retargeting durchzuführen. Es wäre großartig, wenn Sie einen Pseudocode für die Schritte zum Umgang mit Retargeting hätten.
SketchpunkLabs
4

Ich glaube, dass Ihre einfachste Option darin besteht, die ursprüngliche Bindungshaltung mit Ihrem neuen Skelett abzugleichen, wenn Sie die Möglichkeit haben (wenn Ihr neues Skelett noch nicht gehäutet ist).

Wenn Sie das nicht können, können Sie Folgendes versuchen. Dies ist nur eine Intuition, ich übersehen wahrscheinlich viele Dinge, aber es könnte Ihnen helfen, das Licht zu finden. Für jeden Knochen:

  • In Ihrer "alten" Bindungshaltung haben Sie eine Quaternion, die die relative Rotation dieses Knochens im Vergleich zu seinem Elternknochen beschreibt . Hier ist ein Hinweis, wie Sie es finden. Nennen wir es q_old.

  • Ebd. Nennen wir es für Ihre "neue" Bind-Pose q_new.

  • Sie können die relative Drehung von der "neuen" Bindungspose zur "alten" Bin-Pose finden, wie hier beschrieben . Das ist q_new_to_old = inverse(q_new) * q_old.

  • Dann haben Sie in einem Animationsschlüssel Ihr einziges Quaternion, das diesen Knochen von der "alten" Bindungspose in eine animierte Pose umwandelt. Nennen wir das hier q_anim.

Anstatt q_animdirekt zu verwenden, versuchen Sie es mit q_new_to_old * q_anim. Dies sollte die Orientierungsunterschiede zwischen den Bindungsposen "aufheben", bevor die Animation angewendet wird.

Es könnte den Trick machen.

BEARBEITEN

Ihr Code oben scheint der Logik zu folgen, die ich hier beschreibe, aber etwas ist umgekehrt. Anstatt dies zu tun:

multipliers[iSourceBone] = Quaternion.Invert(sourceBoneRot) * targetBoneRot;

Sie könnten das versuchen:

multipliers[iSourceBone] = Quaternion.Invert(targetBoneRot) * sourceBoneRot;

Ich denke, dass Sie sich von Ihrem Ziel zu Ihrer Quelle transformieren müssen, bevor Sie die Quellanimation anwenden, um die gleiche endgültige Ausrichtung zu erhalten.

Laurent Couvidou
quelle
Die Bindungspositionen der Quellen und der Ziele werden variieren, weshalb ich dies implementiere :-). In der Tat war das erste, was ich versuchte, die Multiplikation mit der Umkehrung der Zielrotation. Ich habe versucht, die Knochenrotationen gemäß Ihrem Vorschlag neu zu berechnen, aber das Ergebnis war das gleiche. Hier ist ein Video von dem, was falsch läuft: youtube.com/watch?v=H6Qq37TM4Pg
Robert Fraser
Sind Sie sicher, dass Sie Ihre Rotationen immer relativ zu einem übergeordneten Knochen ausdrücken? Wenn du dein Video siehst, sieht es so aus, als würdest du irgendwo eine Absolut / Welt-Rotation verwenden, wo du stattdessen eine Rotation relativ zum übergeordneten Element verwenden solltest.
Laurent Couvidou
Ja, ich bin mir ziemlich sicher, dass ich die relativen Transformationen hier verwende (ich habe es mit absoluten Zahlen versucht, es sieht viel seltsamer aus). Ich habe das OP mit dem Code aktualisiert, den ich für dieses Video verwendet habe. Anstatt zu versuchen, es auf diese Weise zu debuggen, würde ich lieber einige Quellcodes oder Tutorials sehen, in denen es erfolgreich durchgeführt wurde, dann kann ich herausfinden, was ich falsch mache.
Robert Fraser
Sicher, aber vielleicht gibt es kein Tutorial, um genau das zu tun :) Ich denke, Sie haben etwas in Ihrem obigen Code invertiert. Ich werde meine Antwort bearbeiten.
Laurent Couvidou
Ich habe viele Möglichkeiten ausprobiert und keine davon hat funktioniert. Ich werde versuchen, die globalen Rotationen tatsächlich auf Einzelbildbasis zu berechnen, um zu sehen, was falsch läuft. Vielen Dank für Ihre Hilfe. Ich gebe dir die 100 Wiederholungen.
Robert Fraser