Comment recibler par programmation des animations d'un squelette à un autre?

23

J'essaie d'écrire du code pour transférer des animations conçues pour qu'un squelette ait l'air correct sur un autre squelette. Les animations source sont constituées uniquement de rotations, à l'exception des traductions à la racine (ce sont les animations mocap de la base de données de capture de mouvement CMU ). De nombreuses applications 3D (par exemple Maya) ont cette fonctionnalité intégrée, mais j'essaie d'en écrire une version (très simple) pour mon jeu.

J'ai fait un peu de travail sur la cartographie osseuse, et parce que les squelettes sont hiérarchiquement similaires (bipèdes), je peux faire une cartographie osseuse 1: 1 pour tout sauf la colonne vertébrale (peut travailler dessus plus tard). Le problème, cependant, est que les poses de base squelette / liaison sont différentes, et les os ont des échelles différentes (plus courtes / plus longues), donc si je copie la rotation directement, cela semble très étrange.

J'ai essayé un certain nombre de choses similaires à la solution de lorancou ci-dessous en vain (c'est-à-dire en multipliant chaque image de l'animation par un multiplicateur spécifique à l'os). Si quelqu'un a des ressources sur des trucs comme ça (papiers, code source, etc.), ce serait vraiment utile.

Robert Fraser
la source
Comment voulez-vous que j'ignore la queue et la chose entre les jambes? : P
kaoD
2
@kaoD Si vous devez demander, le squelette est enraciné à (0,0) donc il y a un faux os là-bas. Quant à la queue ... tout le monde sait que la vie est meilleure si vous avez une queue. J'ai toujours pensé que ce serait efficace pour des choses comme le transport de tasses à café et l'équilibre sur les branches des arbres.
Robert Fraser
J'ai vu une démo en temps réel de cela où un kinect a été utilisé pour animer un modèle affiché dans xna. Pensez que le code était sur un site open source. Va chercher ...
George Duckett
Je soupçonne que votre problème est plus lié aux poses de liaison distinctes qu'à la mise à l'échelle des os, vous pouvez essayer d'isoler cela. Par exemple, partez du squelette d'origine, mettez à l'échelle quelques os pour créer un nouveau squelette et voyez si votre algorithme rompt avec celui-ci. Si ce n'est pas le cas, redémarrez à partir du squelette d'origine, mais cette fois ne redimensionnez pas les os, faites-les simplement pivoter et voyez si votre algorithme se casse. Si c'est le cas, alors oui, il y a probablement une transformation supplémentaire à effectuer quelque part.
Laurent Couvidou

Réponses:

8

Le problème était un problème de stabilité numérique. Environ 30 heures de travail sur cela en 2 mois, seulement pour comprendre que je le faisais dès le début. Lorsque j'ai ortho-normalisé les matrices de rotation avant de les brancher dans le code de reciblage, la solution simple de multiplier source * inverse (cible) fonctionnait parfaitement. Bien sûr, le reciblage est bien plus que cela (en particulier, en tenant compte des différentes formes du squelette, c'est-à-dire la largeur des épaules, etc.). Voici le code que j'utilise pour l'approche simple et naieve, si quelqu'un est curieux:

    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
la source
le code sur cette page a-t-il été mis à jour depuis sa rédaction? Avoir du mal à comprendre sans le contexte du moteur qui l'utilise. J'essaie également de recibler l'animation. Ce serait formidable d'avoir un pseudo-code des étapes de gestion du reciblage.
SketchpunkLabs
4

Je crois que votre option la plus simple est simplement de faire correspondre la pose de liaison d'origine avec votre nouveau squelette si vous en avez la possibilité (si votre nouveau squelette n'est pas encore skinné).

Si vous ne pouvez pas faire cela, voici quelque chose que vous pouvez essayer. Ce n'est qu'une intuition, j'oublie probablement beaucoup de choses, mais cela pourrait vous aider à trouver la lumière. Pour chaque os:

  • Dans votre "ancienne" pose de reliure, vous avez un quaternion qui décrit la rotation relative de cet os par rapport à son os parent . Voici un indice pour savoir comment le trouver. Appelons ça q_old.

  • Ibid. pour votre "nouvelle" pose de reliure, appelons-la q_new.

  • Vous pouvez trouver la rotation relative de la "nouvelle" pose de liaison à la "vieille" pose de bin, comme décrit ici . Voilà q_new_to_old = inverse(q_new) * q_old.

  • Ensuite, dans une clé d'animation, vous avez votre seul quaternion qui transforme cet os de la "vieille" pose de liaison en une pose animée. Appelons celui-ci q_anim.

Au lieu d'utiliser q_animdirectement, essayez d'utiliser q_new_to_old * q_anim. Cela devrait "annuler" les différences d'orientation entre les poses de reliure, avant d'appliquer l'animation.

Cela pourrait faire l'affaire.

MODIFIER

Votre code ci-dessus semble suivre la logique que je décris ici, mais quelque chose est inversé. Au lieu de faire ceci:

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

Vous pouvez essayer ça:

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

Je pense que vous devez passer de votre cible à votre source avant d'appliquer l'animation source, pour obtenir la même orientation finale.

Laurent Couvidou
la source
Les poses de liaison des sources et des cibles vont varier, c'est pourquoi j'implémente ceci :-). En effet, la multiplication par l'inverse de la rotation cible a été la première chose que j'ai essayée. J'ai essayé de recalculer les rotations osseuses, selon votre suggestion, mais le résultat était le même. Voici une vidéo de ce qui ne va pas: youtube.com/watch?v=H6Qq37TM4Pg
Robert Fraser
Êtes-vous sûr que vous exprimez toujours vos rotations par rapport à un os parent? En voyant votre vidéo, il semble que vous utilisiez une rotation absolue / mondiale quelque part, où vous devriez plutôt utiliser une rotation relative au parent.
Laurent Couvidou
Oui, je suis assez sûr d'utiliser les transformations relatives ici (j'ai essayé avec des absolus, ça a l'air beaucoup plus étrange). J'ai mis à jour l'OP avec le code que j'utilisais pour cette vidéo. Plutôt que d'essayer de le déboguer de cette façon, je préfère voir du code source ou des tutoriels où cela a été fait avec succès, alors je peux comprendre ce que je fais mal.
Robert Fraser
Bien sûr, mais il n'y a peut-être pas de tutoriel pour faire exactement cela :) Je pense que vous avez inversé quelque chose dans votre code ci-dessus, je vais modifier ma réponse.
Laurent Couvidou
J'ai essayé de nombreuses façons et aucune n'a fonctionné. Je vais essayer de calculer les rotations globales par image pour voir ce qui ne va pas. Merci quand même pour vôtre aide; Je vais vous donner les 100 rep.
Robert Fraser