Come retarget programmaticamente animazioni da uno scheletro all'altro?


23

Sto cercando di scrivere codice per trasferire animazioni progettate per uno scheletro in modo che appaiano corrette su un altro scheletro. Le animazioni di origine sono costituite solo da rotazioni ad eccezione delle traduzioni nella radice (sono le animazioni mocap dal database di motion capture della CMU ). Molte applicazioni 3D (ad esempio Maya) hanno questa funzione integrata, ma sto provando a scriverne una versione (molto semplice) per il mio gioco.

Ho fatto un po 'di lavoro sulla mappatura ossea e, poiché gli scheletri sono gerarchicamente simili (bipedi), posso fare una mappatura ossea 1: 1 per tutto tranne che per la colonna vertebrale (ci posso lavorare più avanti). Il problema, tuttavia, è che le pose dello scheletro / bind di base sono diverse e le ossa hanno scale diverse (più corte / più lunghe), quindi se copio la rotazione direttamente sopra sembra molto strano.

Ho tentato una serie di cose simili alla soluzione di lorancou di seguito senza alcun risultato (ovvero moltiplicando ogni fotogramma nell'animazione per un moltiplicatore specifico dell'osso). Se qualcuno ha delle risorse su cose come questa (documenti, codice sorgente, ecc.), Sarebbe davvero utile.


Come ti aspetti che ignori la coda e la cosa tra le gambe? : P
kaoD,

2
@kaoD Se devi chiedere, lo scheletro è radicato su (0,0), quindi c'è un osso falso lì. Per quanto riguarda la coda ... tutti sanno che la vita è migliore se hai una coda. Ho sempre pensato che sarebbe efficace per cose come portare tazze di caffè e bilanciarsi sugli arti degli alberi.
Robert Fraser,

Ho visto una demo in tempo reale di questo in cui è stato usato un Kinect per animare un modello visualizzato in xna. Pensa che il codice fosse su un sito open source. Cercherà ...
George Duckett l'


Ho il sospetto che il tuo problema sia più con le pose distinte del legame che con il ridimensionamento delle ossa, potresti provare a isolarlo. Ad esempio, inizia dallo scheletro originale, scala alcune ossa su di esso per creare un nuovo scheletro e vedi se il tuo algoritmo si rompe con questo. In caso contrario, ricomincia dallo scheletro originale ma questa volta non ridimensionare le ossa, basta ruotarle e vedere se il tuo algoritmo si rompe. Se lo fa, allora sì, probabilmente c'è un'altra trasformazione da eseguire da qualche parte.
Laurent Couvidou,

Risposte:


8

Il problema era di stabilità numerica. Circa 30 ore di lavoro su questo nel corso di 2 mesi, solo per capire che lo stavo facendo fin dall'inizio. Quando ho normalizzato le matrici di rotazione prima di collegarle al codice di retarget, la semplice soluzione di moltiplicare la sorgente * inversa (target) ha funzionato perfettamente. Ovviamente, c'è molto di più nel retargeting di quello (in particolare, tenendo conto delle diverse forme dello scheletro, cioè della larghezza delle spalle, ecc.). Ecco il codice che sto usando per l'approccio semplice e ingenuo, se qualcuno è curioso:

    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);
    }

il codice in questa pagina è stato aggiornato da quando è stato scritto? Fare fatica a cercare di capire senza il contesto del motore che lo utilizza. Sto provando a fare anche il retargeting di animazione. Sarebbe bello avere un pseudo codice dei passaggi su come gestire il retargeting.
SketchpunkLabs

4

Credo che la tua opzione più semplice sia semplicemente quella di abbinare la posa originale del legame con il tuo nuovo scheletro se ne hai la possibilità (se il tuo nuovo scheletro non ha ancora la pelle).

Se non riesci a farlo, ecco qualcosa che potresti provare. Questa è solo un'intuizione, probabilmente sto trascurando molte cose, ma potrebbe aiutarti a trovare la luce. Per ogni osso:

  • Nella tua "vecchia" posa di legame, hai un quaternione che descrive la rotazione relativa di questo osso rispetto all'osso principale . Ecco un suggerimento su come trovarlo. Chiamiamolo q_old.

  • Ibid. per la tua "nuova" posa di legame, chiamiamola q_new.

  • È possibile trovare la rotazione relativa dalla posa "nuova" del legame alla posa "vecchia" del cestino, come descritto qui . Questo è q_new_to_old = inverse(q_new) * q_old.

  • Quindi in una chiave di animazione, hai il tuo unico quaternione che trasforma quell'osso dalla posa "vecchia" in una posa animata. Chiamiamo questo q_anim.

Invece di utilizzare q_animdirettamente, prova a utilizzare q_new_to_old * q_anim. Questo dovrebbe "annullare" le differenze di orientamento tra le pose di rilegatura, prima di applicare l'animazione.

Potrebbe fare il trucco.

MODIFICARE

Il tuo codice sopra sembra seguire la logica che sto descrivendo qui, ma qualcosa è invertito. Invece di fare questo:

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

Potresti provare che:

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

Penso che sia necessario passare dal proprio target alla propria sorgente prima di applicare l'animazione della sorgente, per ottenere lo stesso orientamento finale.


Il legame tra le fonti e gli obiettivi varierà, motivo per cui sto implementando questo :-). In effetti, moltiplicare per l'inverso della rotazione del bersaglio è stata la prima cosa che ho provato. Ho provato a ricalcolare le rotazioni ossee, come da tuo suggerimento, ma il risultato è stato lo stesso. Ecco un video di ciò che non va: youtube.com/watch?v=H6Qq37TM4Pg
Robert Fraser

Sei sicuro di esprimere sempre le tue rotazioni relativamente a un osso genitore? Vedendo il tuo video, sembra che tu stia usando una rotazione assoluta / mondiale da qualche parte, dove invece dovresti usare una rotazione da parent a parent.
Laurent Couvidou,

Sì, sono abbastanza sicuro che sto usando le relative trasformazioni qui (ho provato con gli assoluti, sembra molto più strano). Ho aggiornato l'OP con il codice che stavo usando per questo video. Invece di provare a eseguire il debug in questo modo, preferirei vedere alcuni codici sorgente o tutorial in cui è stato eseguito correttamente, quindi posso capire cosa sto facendo di sbagliato.
Robert Fraser,

Certo, ma forse non c'è nessun tutorial per fare esattamente questo :) Penso che tu abbia invertito qualcosa nel tuo codice sopra, modificherò la mia risposta.
Laurent Couvidou,

Ho provato in molti modi e nessuno ha funzionato. Proverò effettivamente a calcolare le rotazioni globali su una base per fotogramma per vedere cosa non va. Grazie per l'aiuto, però; Ti darò il rappresentante 100.
Robert Fraser,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.