Как программно перенастроить анимацию от одного скелета к другому?

23

Я пытаюсь написать код для передачи анимации, которая была разработана для одного скелета, чтобы он выглядел правильно на другом скелете. Исходные анимации состоят только из вращений, за исключением переводов в корне (это анимации mocap из базы данных захвата движения CMU ). Многие 3D-приложения (например, Maya) имеют встроенную функцию, но я пытаюсь написать (очень простую) версию для моей игры.

Я проделал некоторую работу по картированию костей, и, поскольку скелеты иерархически похожи (двуногие), я могу сделать картирование костей 1: 1 для всего, кроме позвоночника (с этим можно будет поработать позже). Проблема, однако, заключается в том, что базовые позы скелета / связывания отличаются, а кости имеют разные масштабы (короче / длиннее), поэтому, если я просто скопирую вращение по прямой, это будет выглядеть очень странно.

Я попытался сделать несколько вещей, похожих на приведенное ниже решение Лоранку, но безрезультатно (то есть умножить каждый кадр в анимации на умножитель для кости). Если у кого-то есть ресурсы на такие вещи (документы, исходный код и т. Д.), Это было бы очень полезно.

Роберт Фрейзер
источник
Как вы ожидаете, что я проигнорирую хвост и предмет между ног? : P
kaoD
2
@kaoD Если вы спросите, скелет укоренен в (0,0), так что там есть поддельная кость. Что касается хвоста ... все знают, что жизнь лучше, если у тебя есть хвост. Я всегда думал, что это будет эффективно для таких вещей, как ношение кофейных чашек и балансирование на ветках деревьев.
Роберт Фрейзер
Я видел демонстрацию этого в реальном времени, где kinect использовался для анимации модели, отображаемой в xna. Думаю, код был на сайте с открытым исходным кодом. Будем искать ...
Джордж Дакетт
Я подозреваю, что ваша проблема больше связана с отдельными позами связывания, чем с масштабированием костей, вы можете попытаться изолировать это. Например, начните с исходного скелета, масштабируйте несколько костей на нем, чтобы создать новый скелет, и посмотрите, не сломается ли ваш алгоритм с этим. Если этого не произойдет, перезапустите исходный скелет, но на этот раз не масштабируйте кости, просто поверните их и посмотрите, не сломается ли ваш алгоритм. Если это так, то да, возможно, есть дополнительное преобразование для выполнения где-то.
Лоран Кувиду

Ответы:

8

Проблема заключалась в численной стабильности. Приблизительно 30 часов работы над этим в течение 2 месяцев, только чтобы понять, что я делал это правильно с самого начала. Когда я нормализовал матрицы вращения перед тем, как вставить их в код ретаргетинга, простое решение умножения исходного * обратного (целевого) отлично сработало. Конечно, есть еще кое-что для ретаргетинга (в частности, принимая во внимание различные формы скелета, например, ширину плеч и т. Д.). Вот код, который я использую для простого, наивного подхода, если кому-то интересно:

    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);
    }
Роберт Фрейзер
источник
обновлен ли код на этой странице с момента его написания? Трудно пытаться понять без контекста движка, который его использует. Я также пытаюсь сделать ретаргетинг анимации. Было бы здорово иметь некоторый псевдокод шагов для обработки ретаргетинга.
SketchpunkLabs
4

Я считаю, что ваш самый простой вариант - просто сопоставить исходную позу связывания с вашим новым скелетом, если у вас есть такая возможность (если ваш новый скелет еще не снят).

Если вы не можете сделать это, вот что вы можете попробовать. Это просто интуиция, я, вероятно, упускаю из виду многие вещи, но это может помочь вам найти свет. Для каждой кости:

  • В вашей «старой» позе связывания у вас есть один кватернион, который описывает относительное вращение этой кости по сравнению с ее родительской костью. Вот подсказка о том, как его найти. Давайте назовем это q_old.

  • Там же. для вашей "новой" позы связывания, давайте назовем это q_new.

  • Вы можете найти относительное вращение от «новой» позы привязки к «старой» позе бина, как описано здесь . Это q_new_to_old = inverse(q_new) * q_old.

  • Затем в одном ключе анимации вы получите один кватернион, который преобразует эту кость из «старой» позы связывания в анимированную позу. Давайте назовем это q_anim.

Вместо q_animнепосредственного использования , попробуйте использовать q_new_to_old * q_anim. Это должно «отменить» различия в ориентации между позами привязки перед применением анимации.

Это может помочь.

РЕДАКТИРОВАТЬ

Ваш код выше, кажется, следует логике, которую я здесь описываю, но что-то перевернуто. Вместо этого:

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

Вы можете попробовать это:

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

Я думаю, что вам нужно перейти от вашей цели к вашему источнику перед применением исходной анимации, чтобы получить ту же конечную ориентацию.

Лоран Кувиду
источник
Позиции привязки как источников, так и целей будут различаться, поэтому я и реализую это :-). Действительно, умножение на обратное вращение цели было первым, что я попробовал. Я пытался пересчитать вращения костей, как вы и предполагали, но результат был тот же. Вот видео о том, что происходит не так: youtube.com/watch?v=H6Qq37TM4Pg
Роберт Фрейзер,
Вы уверены, что всегда выражаете свои вращения относительно родительской кости? Смотря ваше видео, похоже, что вы используете абсолютное / мировое вращение где-то, где вместо этого вам следует использовать относительное вращение относительно родителя.
Лоран Кувиду
Да, я вполне уверен, что я использую относительные преобразования здесь (я пробовал с абсолютами, это выглядит намного более странно). Я обновил ОП кодом, который я использовал для этого видео. Вместо того, чтобы пытаться отлаживать его таким образом, я бы предпочел увидеть некоторый исходный код или учебники, где это было сделано успешно, тогда я могу понять, что я делаю неправильно.
Роберт Фрейзер
Конечно, но, может быть, нет учебника, чтобы сделать именно это :) Я думаю, что вы перевернули что-то в своем коде выше, я отредактирую свой ответ.
Лоран Кувиду
Я пробовал много способов, и ни один из них не работал. Я собираюсь попытаться вычислить глобальные повороты для каждого кадра, чтобы увидеть, что происходит не так. Спасибо за вашу помощь, хотя; Я дам вам 100 повторений.
Роберт Фрейзер