¿Cómo rediseñar programáticamente animaciones de un esqueleto a otro?

23

Estoy tratando de escribir código para transferir animaciones que fueron diseñadas para que un esqueleto se vea correcto en otro esqueleto. Las animaciones de origen consisten solo en rotaciones, excepto las traducciones en la raíz (son las animaciones mocap de la base de datos de captura de movimiento de CMU ). Muchas aplicaciones 3D (por ejemplo, Maya) tienen esta facilidad incorporada, pero estoy tratando de escribir una versión (muy simple) para mi juego.

He trabajado un poco en el mapeo óseo, y debido a que los esqueletos son jerárquicamente similares (bípedos), puedo hacer un mapeo óseo 1: 1 para todo menos la columna vertebral (puede trabajar en eso más adelante). El problema, sin embargo, es que las posturas del esqueleto base / unión son diferentes, y los huesos son escalas diferentes (más cortas / más largas), por lo que si copio la rotación directamente, parece muy extraño.

He intentado una serie de cosas similares a la solución de lorancou a continuación en vano (es decir, multiplicando cada cuadro en la animación por un multiplicador específico de hueso). Si alguien tiene recursos sobre cosas como esta (documentos, código fuente, etc.), eso sería realmente útil.

Robert Fraser
fuente
¿Cómo esperas que ignore la cola y la cosa entre las piernas? : P
kaoD
2
@kaoD Si tiene que preguntar, el esqueleto está enraizado en (0,0), por lo que hay un hueso falso allí. En cuanto a la cola ... todos saben que la vida es mejor si tienes cola. Siempre pensé que sería eficiente para cosas como llevar tazas de café y equilibrar las ramas de los árboles.
Robert Fraser
He visto una demostración en tiempo real de esto donde se utilizó un kinect para animar un modelo que se muestra en xna. Piensa que el código estaba en un sitio de código abierto. Buscará ...
George Duckett
Sospecho que su problema es más con las distintas posturas de unión que con la escala de los huesos, podría intentar aislar eso. Por ejemplo, comience desde el esqueleto original, escale algunos huesos para crear un nuevo esqueleto y vea si su algoritmo se rompe con este. Si no es así, reinicie desde el esqueleto original, pero esta vez no escale los huesos, simplemente gírelos y vea si su algoritmo se rompe. Si lo hace, entonces sí, probablemente haya una transformación adicional para realizar en algún lugar.
Laurent Couvidou

Respuestas:

8

El problema era uno de estabilidad numérica. Aproximadamente 30 horas de trabajo en esto en el transcurso de 2 meses, solo para descubrir que lo estaba haciendo desde el principio. Cuando orto-normalicé las matrices de rotación antes de conectarlas al código de retarget, la solución simple de multiplicar fuente * inversa (objetivo) funcionó perfectamente. Por supuesto, hay mucho más para reorientar que eso (en particular, teniendo en cuenta las diferentes formas del esqueleto, es decir, el ancho de los hombros, etc.). Aquí está el código que estoy usando para el enfoque simple, ingenuo, si alguien tiene curiosidad:

    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
fuente
¿Se ha actualizado el código de esta página desde que se escribió? Tener dificultades para tratar de entender sin el contexto del motor que lo usa. Estoy tratando de reorientar la animación también. Sería genial tener un pseudocódigo de los pasos de cómo manejar el retargeting.
SketchpunkLabs
4

Creo que su opción más fácil es simplemente hacer coincidir la postura de enlace original con su nuevo esqueleto si tiene la posibilidad (si su nuevo esqueleto aún no está desollado).

Si no puedes hacer eso, aquí hay algo que puedes probar. Esto es solo intuición, probablemente estoy pasando por alto muchas cosas, pero podría ayudarte a encontrar la luz. Para cada hueso:

  • En su "viejo" pose se unen, usted tiene un cuaternión que describe la relación de rotación de este hueso en comparación con su matriz ósea. Aquí hay una pista sobre cómo encontrarlo. Digamos que es q_old.

  • Ibídem. para su "nueva" postura de enlace, llamémosla q_new.

  • Puede encontrar la rotación relativa de la pose de enlace "nueva" a la pose de bin "antigua", como se describe aquí . Eso es q_new_to_old = inverse(q_new) * q_old.

  • Luego, en una clave de animación, tienes tu quaternion que transforma ese hueso de la pose de enlace "antigua" a una pose animada. Llamemos a este q_anim.

En lugar de usar q_animdirectamente, intente usar q_new_to_old * q_anim. Esto debería "cancelar" las diferencias de orientación entre las posturas de enlace, antes de aplicar la animación.

Podría hacer el truco.

EDITAR

Su código anterior parece seguir la lógica que describo aquí, pero algo está invertido. En lugar de hacer esto:

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

Podrías probar eso:

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

Creo que debe transformarse de su destino a su fuente antes de aplicar la animación de origen, para obtener la misma orientación final.

Laurent Couvidou
fuente
Las posturas de enlace tanto de las fuentes como de los objetivos van a variar, por eso estoy implementando esto :-). De hecho, multiplicar por el inverso de la rotación del objetivo fue lo primero que intenté. Intenté volver a calcular las rotaciones óseas, según su sugerencia, pero el resultado fue el mismo. Aquí hay un video de lo que está mal: youtube.com/watch?v=H6Qq37TM4Pg
Robert Fraser
¿Estás seguro de que siempre estás expresando tus rotaciones relativamente a un hueso principal? Al ver su video, parece que está usando una rotación absoluta / mundial en algún lugar, donde debería usar una rotación relativa a la principal.
Laurent Couvidou
Sí, estoy bastante seguro de que estoy usando las transformaciones relativas aquí (lo he intentado con absolutos, parece mucho más extraño). Actualicé el OP con el código que estaba usando para este video. En lugar de tratar de depurarlo de esta manera, prefiero ver algún código fuente o tutoriales donde se realizó con éxito, luego puedo descubrir qué estoy haciendo mal.
Robert Fraser
Claro, pero tal vez no haya un tutorial para hacer exactamente eso :) Creo que invirtió algo en su código anterior, editaré mi respuesta.
Laurent Couvidou
Lo intenté de muchas maneras, y ninguna funcionó. Voy a tratar de calcular las rotaciones globales por cuadro para ver qué está pasando mal. Gracias por tu ayuda, sin embargo; Te daré las 100 repeticiones.
Robert Fraser