Bagaimana cara retarget animasi animasi dari satu kerangka ke kerangka lainnya?

23

Saya mencoba menulis kode untuk mentransfer animasi yang dirancang untuk satu kerangka agar terlihat benar pada kerangka lain. Animasi sumber hanya terdiri dari rotasi kecuali untuk terjemahan di root (mereka animasi mocap dari CMU basis data gerak ). Banyak aplikasi 3D (mis. Maya) memiliki fasilitas ini, tetapi saya mencoba menulis versi (sangat sederhana) untuk game saya.

Saya telah melakukan beberapa pekerjaan pada pemetaan tulang, dan karena kerangkanya secara hierarki mirip (biped), saya dapat melakukan pemetaan tulang 1: 1 untuk semua hal kecuali tulang belakang (bisa bekerja nanti). Masalahnya, bagaimanapun, adalah bahwa kerangka dasar / pose mengikat berbeda, dan tulang-tulang adalah skala yang berbeda (lebih pendek / lebih lama), jadi jika saya hanya menyalin rotasi langsung ke atasnya terlihat sangat aneh.

Saya telah mencoba sejumlah hal yang mirip dengan solusi lorancou di bawah ini tetapi tidak berhasil (yaitu mengalikan setiap frame dalam animasi dengan pengganda spesifik-tulang). Jika ada yang punya sumber daya tentang hal-hal seperti ini (makalah, kode sumber, dll), itu akan sangat membantu.

Robert Fraser
sumber
Bagaimana Anda berharap saya mengabaikan ekor dan benda di antara kedua kaki? : P
kaoD
2
@ kaoD Jika Anda harus bertanya, kerangka itu berakar pada (0,0) sehingga ada tulang palsu di sana. Sedangkan untuk ekor ... semua orang tahu hidup lebih baik jika Anda memiliki ekor. Saya selalu berpikir itu akan efisien untuk hal-hal seperti membawa cangkir kopi dan menyeimbangkan anggota badan pohon.
Robert Fraser
Saya telah melihat demo waktu nyata di mana kinect digunakan untuk menghidupkan model yang ditampilkan dalam xna. Pikirkan kode itu di situs sumber terbuka. Akan mencari ...
George Duckett
Saya menduga bahwa masalah Anda lebih pada pose ikatan yang berbeda daripada dengan penskalaan tulang, Anda bisa mencoba mengisolasinya. Misalnya mulai dari kerangka asli, skala beberapa tulang di atasnya untuk membuat kerangka baru, dan lihat apakah algoritma Anda rusak dengan yang satu ini. Jika tidak, maka restart dari kerangka asli tetapi kali ini tidak skala tulang, putar saja dan lihat apakah algoritma Anda rusak. Jika ya, maka ya mungkin ada transformasi tambahan untuk tampil di suatu tempat.
Laurent Couvidou

Jawaban:

8

Masalahnya adalah stabilitas numerik. Kira-kira 30 jam mengerjakan ini selama 2 bulan, hanya untuk mengetahui bahwa saya sudah melakukannya sejak awal. Ketika saya ortho-dinormalisasi matriks rotasi sebelum menghubungkannya ke kode retarget, solusi sederhana mengalikan sumber * invers (target) bekerja dengan sempurna. Tentu saja, ada lebih banyak penargetan ulang dari itu (khususnya, dengan mempertimbangkan berbagai bentuk kerangka, yaitu lebar bahu, dll). Inilah kode yang saya gunakan untuk pendekatan yang sederhana dan naif, jika ada yang ingin tahu:

    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
sumber
Apakah kode pada halaman ini telah diperbarui sejak ditulis? Kesulitan berusaha memahami tanpa konteks mesin yang menggunakannya. Saya mencoba melakukan penargetan ulang animasi juga. Akan bagus untuk memiliki beberapa kode pseudo dari langkah-langkah bagaimana menangani penargetan ulang.
SketchpunkLabs
4

Saya percaya bahwa pilihan termudah Anda adalah mencocokkan pose mengikat asli dengan kerangka baru Anda jika Anda punya kemungkinan (jika kerangka baru Anda belum dikuliti).

Jika Anda tidak bisa melakukan itu, berikut ini sesuatu yang bisa Anda coba. Ini hanya intuisi, saya mungkin menghadap banyak hal, tetapi mungkin membantu Anda menemukan cahaya. Untuk setiap tulang:

  • Dalam pose mengikat "lama" Anda, Anda punya satu angka empat yang menggambarkan rotasi relatif dari tulang ini dibandingkan dengan tulang induknya . Berikut ini petunjuk cara menemukannya. Sebut saja q_old.

  • Ibid. untuk pose mengikat "baru" Anda, sebut saja q_new.

  • Anda dapat menemukan rotasi relatif dari pose bind "baru" ke pose bin "lama", seperti dijelaskan di sini . Itu q_new_to_old = inverse(q_new) * q_old.

  • Kemudian dalam satu tombol animasi, Anda punya satu angka empat Anda yang mengubah tulang itu dari pose mengikat "lama" menjadi pose animasi. Sebut saja yang ini q_anim.

Alih-alih menggunakan q_animsecara langsung, coba gunakan q_new_to_old * q_anim. Ini harus "membatalkan" perbedaan orientasi antara pose mengikat, sebelum menerapkan animasi.

Mungkin berhasil.

EDIT

Kode Anda di atas tampaknya mengikuti logika yang saya jelaskan di sini, tetapi ada sesuatu yang terbalik. Alih-alih melakukan ini:

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

Anda bisa mencobanya:

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

Saya pikir Anda perlu mengubah dari target Anda ke sumber Anda sebelum menerapkan animasi sumber, untuk mendapatkan orientasi akhir yang sama.

Laurent Couvidou
sumber
Pose mengikat sumber dan target akan bervariasi, itulah sebabnya saya menerapkan ini :-). Memang, mengalikan dengan kebalikan dari rotasi target adalah hal pertama yang saya coba. Saya mencoba menghitung ulang rotasi tulang, sesuai saran Anda, tetapi hasilnya sama. Berikut adalah video tentang apa yang salah: youtube.com/watch?v=H6Qq37TM4Pg
Robert Fraser
Apakah Anda yakin selalu mengekspresikan rotasi relatif ke tulang induk? Melihat video Anda, sepertinya Anda menggunakan rotasi absolut / dunia di suatu tempat, di mana Anda harus menggunakan rotasi relatif ke orang tua.
Laurent Couvidou
Ya, saya cukup yakin saya menggunakan transformasi relatif di sini (saya sudah mencoba dengan absolut, kelihatannya jauh lebih aneh). Saya memperbarui OP dengan kode yang saya gunakan untuk video ini. Daripada mencoba men-debug dengan cara ini, saya lebih suka melihat beberapa kode sumber atau tutorial di mana itu dilakukan dengan sukses, maka saya bisa mencari tahu apa yang saya lakukan salah.
Robert Fraser
Tentu, tapi mungkin tidak ada tutorial untuk melakukan hal itu :) Saya pikir Anda membalikkan sesuatu dalam kode Anda di atas, saya akan mengedit jawaban saya.
Laurent Couvidou
Saya sudah mencoba banyak cara, dan tidak ada yang berhasil. Saya akan mencoba menghitung rotasi global berdasarkan per-frame untuk melihat apa yang salah. Terima kasih atas bantuan Anda; Saya akan memberi Anda 100 perwakilan.
Robert Fraser