Como redirecionar programaticamente animações de um esqueleto para outro?

23

Estou tentando escrever código para transferir animações que foram projetadas para um esqueleto parecer correto em outro esqueleto. As animações de origem consistem apenas em rotações, exceto as traduções na raiz (são as animações mocap do banco de dados de captura de movimento da CMU ). Muitos aplicativos 3D (por exemplo, o Maya) possuem esse recurso embutido, mas estou tentando escrever uma versão (muito simples) dele para o meu jogo.

Eu fiz algum trabalho no mapeamento ósseo e, como os esqueletos são hierarquicamente semelhantes (bípedes), eu posso fazer o mapeamento ósseo 1: 1 para tudo, exceto a coluna vertebral (pode trabalhar nisso posteriormente). O problema, no entanto, é que as posições base do esqueleto / atadura são diferentes e os ossos têm escalas diferentes (mais curtas / mais longas), por isso, se eu apenas copiar a rotação diretamente, parecerá muito estranho.

Tentei várias coisas semelhantes à solução de lorancou abaixo sem sucesso (ou seja, multiplicar cada quadro na animação por um multiplicador específico de osso). Se alguém tiver algum recurso em coisas como esta (documentos, código fonte, etc), isso seria realmente útil.

Robert Fraser
fonte
Como você espera que eu ignore a cauda e a coisa entre as pernas? : P
kaoD
2
@kaoD Se você precisar perguntar, o esqueleto está enraizado em (0,0), então existe um osso falso lá. Quanto à cauda ... todo mundo sabe que a vida é melhor se você tiver uma cauda. Eu sempre pensei que seria eficiente para coisas como carregar xícaras de café e equilibrar nos galhos das árvores.
31412 Robert
Eu vi uma demonstração em tempo real disso, onde um kinect foi usado para animar um modelo exibido no xna. Pense que o código estava em um site de código aberto. Será que procuraria ... #
11334 George Duckett
Suspeito que seu problema seja mais com as poses distintas de vinculação do que com a descamação dos ossos, você pode tentar isolar isso. Por exemplo, comece do esqueleto original, dimensione alguns ossos para criar um novo esqueleto e veja se o seu algoritmo quebra com esse. Caso contrário, reinicie a partir do esqueleto original, mas desta vez não dimensione ossos, apenas gire-os e veja se o seu algoritmo quebra. Se isso acontecer, sim, provavelmente há uma transformação adicional para executar em algum lugar.
Laurent Couvidou

Respostas:

8

O problema era de estabilidade numérica. Aproximadamente 30 horas de trabalho nisso ao longo de 2 meses, apenas para descobrir que eu estava fazendo isso desde o início. Quando orto-normalizei as matrizes de rotação antes de conectá-las ao código de retarget, a solução simples de multiplicar a origem * inversa (destino) funcionou perfeitamente. Obviamente, há muito mais para redirecionar do que isso (em particular, levando em consideração as diferentes formas do esqueleto, como largura dos ombros, etc.). Aqui está o código que estou usando para a abordagem simples e ingênua, se alguém estiver 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);
    }
Robert Fraser
fonte
O código desta página foi atualizado desde que foi escrito? Dificuldade em tentar entender sem o contexto do mecanismo que o utiliza. Também estou tentando redirecionar a animação. Seria ótimo ter algum pseudo-código das etapas de como lidar com o redirecionamento.
SketchpunkLabs
4

Acredito que a sua opção mais fácil é simplesmente combinar a pose de ligação original com o seu novo esqueleto, se você tiver a possibilidade (se o seu novo esqueleto ainda não estiver esfolado).

Se você não pode fazer isso, aqui está algo que você pode tentar. Isso é apenas intuição, provavelmente estou ignorando muitas coisas, mas isso pode ajudá-lo a encontrar a luz. Para cada osso:

  • Em seu "velho" pose bind, você tem um quaternion que descreve a relação de rotação deste osso em comparação com seu pai osso. Aqui está uma dica de como encontrá-lo. Vamos chamá-lo q_old.

  • Ibid. para a sua "nova" pose de ligação, vamos chamá-la q_new.

  • Você pode encontrar a rotação relativa da "nova" pose de ligação à "velha" posição de posição, conforme descrito aqui . É isso q_new_to_old = inverse(q_new) * q_old.

  • Então, em uma chave de animação, você tem seu único quaternion que transforma esse osso da pose de ligação "antiga" em uma pose animada. Vamos chamar este aqui q_anim.

Em vez de usar q_animdiretamente, tente usar q_new_to_old * q_anim. Isso deve "cancelar" as diferenças de orientação entre as poses de ligação, antes de aplicar a animação.

Pode fazer o truque.

EDITAR

Seu código acima parece seguir a lógica que estou descrevendo aqui, mas algo está invertido. Em vez de fazer isso:

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

Você pode tentar isso:

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

Eu acho que você precisa transformar do seu destino na sua fonte antes de aplicar a animação de origem, para obter a mesma orientação final.

Laurent Couvidou
fonte
As poses de ligação das fontes e dos destinos variam, e é por isso que estou implementando isso :-). De fato, multiplicar pelo inverso da rotação do alvo foi a primeira coisa que tentei. Tentei recalcular as rotações ósseas, conforme sua sugestão, mas o resultado foi o mesmo. Aqui está um vídeo do que está acontecendo de errado: youtube.com/watch?v=H6Qq37TM4Pg
Robert Fraser
Tem certeza de que está sempre expressando suas rotações relativamente ao osso parental? Ao ver seu vídeo, parece que você está usando uma rotação absoluta / mundial em algum lugar, onde você deve usar uma rotação relativa a pai.
Laurent Couvidou
Sim, tenho certeza de que estou usando as transformações relativas aqui (tentei com absolutos, parece muito mais estranho). Atualizei o OP com o código que estava usando para este vídeo. Em vez de tentar depurá-lo dessa maneira, prefiro ver alguns códigos-fonte ou tutoriais onde isso foi feito com sucesso, para que eu possa descobrir o que estou fazendo de errado.
31412 Robert
Claro, mas talvez não exista um tutorial para fazer exatamente isso :) Acho que você inverteu algo no seu código acima, editarei minha resposta.
Laurent Couvidou
Eu tentei de várias maneiras, e nenhuma delas funcionou. Vou tentar calcular as rotações globais por quadro para ver o que está acontecendo de errado. Obrigado pela sua ajuda, no entanto; Eu vou te dar os 100 representantes.
Robert Fraser