Jak programowo retargetować animacje z jednego szkieletu na drugi?

23

Próbuję napisać kod, aby przenieść animacje zaprojektowane dla jednego szkieletu, aby wyglądały poprawnie na innym szkielecie. Animacje źródłowe składają się tylko z rotacji, z wyjątkiem tłumaczeń w katalogu głównym (są to animacje mocap z bazy danych przechwytywania ruchu CMU ). Wiele aplikacji 3D (np. Maya) ma wbudowane to narzędzie, ale staram się napisać (bardzo prostą) wersję dla mojej gry.

Zrobiłem trochę pracy nad mapowaniem kości, a ponieważ szkielety są hierarchicznie podobne (dwunożne), mogę wykonać mapowanie kości 1: 1 dla wszystkiego oprócz kręgosłupa (mogę pracować nad tym później). Problemem jest jednak to, że podstawowe pozycje szkieletu / wiązania są różne, a kości mają różne skale (krótsze / dłuższe), więc jeśli po prostu skopiuję obrót prosto, wygląda to bardzo dziwnie.

Próbowałem wielu rzeczy podobnych do rozwiązania Lorancou poniżej, ale bezskutecznie (tj. Pomnożenie każdej klatki w animacji przez mnożnik właściwy dla kości). Jeśli ktoś ma jakieś zasoby na takie rzeczy (dokumenty, kod źródłowy itp.), Byłoby to naprawdę pomocne.

Robert Fraser
źródło
Jak oczekujesz, że zignoruję ogon i przedmiot między nogami? : P
kaoD
2
@kaoD Jeśli musisz zapytać, szkielet jest zakorzeniony w (0,0), więc jest tam sztuczna kość. Co do ogona ... wszyscy wiedzą, że życie jest lepsze, jeśli masz ogon. Zawsze myślałem, że będzie to przydatne w przypadku noszenia filiżanek kawy i balansowania na gałęziach drzew.
Robert Fraser,
Widziałem demo tego w czasie rzeczywistym, w którym zastosowano kinekt do animacji modelu wyświetlanego w xna. Myślę, że kod był na stronie open source. Przeszukuje ...
George Duckett
Podejrzewam, że twój problem dotyczy bardziej wyraźnych pozycji wiązania niż skalowania kości, możesz spróbować to wydzielić. Na przykład zacznij od oryginalnego szkieletu, przeskaluj na nim kilka kości, aby utworzyć nowy szkielet i sprawdź, czy Twój algorytm się z tym nie psuje. Jeśli nie, zrestartuj pierwotny szkielet, ale tym razem nie skaluj kości, po prostu obróć je i sprawdź, czy Twój algorytm się nie psuje. Jeśli tak, to prawdopodobnie jest gdzieś dodatkowa transformacja.
Laurent Couvidou

Odpowiedzi:

8

Problem polegał na stabilności liczbowej. Około 30 godzin pracy nad tym w ciągu 2 miesięcy, tylko po to, aby dowiedzieć się, że robiłem to od samego początku. Kiedy ortormalizowałem macierze rotacji przed podłączeniem ich do kodu retarget, proste rozwiązanie polegające na pomnożeniu odwrotności source * (target) działało idealnie. Oczywiście jest o wiele więcej do retargetowania niż to (w szczególności biorąc pod uwagę różne kształty szkieletu, tj. Szerokość ramion itp.). Oto kod, którego używam do prostego, naiwnego podejścia, jeśli ktoś jest ciekawy:

    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
źródło
czy kod na tej stronie został zaktualizowany od czasu jego napisania? Trudno jest zrozumieć bez kontekstu silnika, który go używa. Próbuję również przeprowadzić retargeting animacji. Byłoby wspaniale mieć pseudo-kod z instrukcjami obsługi retargetowania.
SketchpunkLabs
4

Uważam, że najłatwiejszą opcją jest po prostu dopasowanie oryginalnej pozycji wiązania do nowego szkieletu, jeśli masz taką możliwość (jeśli nowy szkielet nie jest jeszcze pozbawiony skórki).

Jeśli nie możesz tego zrobić, możesz spróbować. To tylko intuicja, prawdopodobnie przeoczam wiele rzeczy, ale może ci pomóc znaleźć światło. Dla każdej kości:

  • W „starej” pozycji wiązania masz jeden czwartorzęd, który opisuje względny obrót tej kości w porównaniu do jej kości macierzystej . Oto wskazówka, jak go znaleźć. Nazwijmy to q_old.

  • Tamże na swoją „nową” pozycję wiązania, nazwijmy to q_new.

  • Względny obrót można znaleźć od „nowej” pozycji wiązania do „starej” pozycji bin, jak opisano tutaj . To q_new_to_old = inverse(q_new) * q_old.

  • Następnie w jednym kluczu animacji masz jedną ćwiartkę, która przekształca tę kość ze „starej” pozy wiązania w animowaną. Nazwijmy to q_anim.

Zamiast używać q_animbezpośrednio, spróbuj użyć q_new_to_old * q_anim. Powinno to „anulować” różnice orientacji między pozycjami wiązania przed zastosowaniem animacji.

To może załatwić sprawę.

EDYTOWAĆ

Powyższy kod wydaje się zgodny z logiką, którą tu opisuję, ale coś jest odwrócone. Zamiast tego:

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

Możesz spróbować:

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

Myślę, że musisz przejść od celu do źródła przed zastosowaniem animacji źródłowej, aby uzyskać tę samą ostateczną orientację.

Laurent Couvidou
źródło
Stanowiska wiązania źródeł i celów będą się różnić, dlatego wdrażam to :-). Rzeczywiście, najpierw spróbowałem pomnożyć przez odwrotność rotacji celu. Próbowałem ponownie obliczyć rotacje kości, zgodnie z twoją sugestią, ale wynik był taki sam. Oto film o tym, co dzieje się nie tak: youtube.com/watch?v=H6Qq37TM4Pg
Robert Fraser
Czy na pewno zawsze wyrażasz swoje obroty w stosunku do kości macierzystej? Widząc swój film, wygląda na to, że używasz gdzieś obrotu bezwzględnego / światowego, w którym zamiast tego powinieneś zastosować obrót względem rodzica.
Laurent Couvidou
Tak, jestem całkiem pewien, że używam tutaj względnych transformacji (próbowałem z absolutami, wygląda to o wiele dziwniej). Zaktualizowałem OP kodem, którego użyłem do tego filmu. Zamiast próbować debugować go w ten sposób, wolę zobaczyć kod źródłowy lub samouczki, w których udało się to zrobić, a potem mogę dowiedzieć się, co robię źle.
Robert Fraser
Jasne, ale może nie ma takiego samouczka :) Myślę, że odwróciłeś coś w powyższym kodzie, zredaguję moją odpowiedź.
Laurent Couvidou
Próbowałem na wiele sposobów i żaden z nich nie zadziałał. Spróbuję faktycznie obliczyć globalne obroty dla poszczególnych klatek, aby zobaczyć, co się dzieje. Ale dziękuję za pomoc; Dam ci 100 powtórzeń.
Robert Fraser,