C#
アニメーション
Unity
OpenPose
3d-pose-baseline-vmd

【Unity】OpenPose ==> 3d-pose-baseline-vmd で出力した関節座標値を回転に変換して骨格アニメーションさせる

b.gif

このプログラムを実行させるには、OpenPoseと3d-pose-baseline-vmdを使って出力した、pos.txtというファイルが必要になります。
詳しくは、
https://qiita.com/miu200521358/items/d00c3b6d1bc1b6e67480
の記事を参考にしてください。

pos.txtが用意で来たら、下のソースコードを空のゲームオブジェクトにアタッチして、インスペクタに表示されるAnimatorに適当なキャラクタのAnimatorをドラックして設定してください。

動画で使っているキャラクタはUnitychanの「藤原みさき」です。

時間的余裕ができたら、動きの考察や、ソースコードの修正などをしようと思っています。
ボーンが一本の場合での考え方を記事にしました13:26 2018/06/05
https://qiita.com/romaroma/items/c08ae02c21f1ca71c15e

モーション元の動画データは、Mixamoで作ったモーション(Samba Dancing)を画面キャプチャで撮影してOpenPoseに入力したものです。
bb.gif

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System;

// miuさんツイート
// https://twitter.com/Romast38/status/1001850558850859009?s=20

// 3d-pose-baseline-vmd/doc/Output.md
// https://github.com/miu200521358/3d-pose-baseline-vmd/blob/master/doc/Output.md
// 「smoothed.txt の 1フレーム(一行)内のデータ構造」おそらく、smoothed.txtは、pos.txtの間違い。

// 3d-pose-baseline-vmdで出力されたpos.txtには座標値xyzとインデックスが書かれている。インデックスの示す関節は、以下の通り。
// この情報は、3d-pose-baseline-vmd\src\data_utils.pyに書かれている。
// https://github.com/miu200521358/3d-pose-baseline-vmd/blob/master/src/data_utils.py
// 計17個のデータ。
// H36M_NAMES[0]  = 'Hip'
// H36M_NAMES[1]  = 'RHip'
// H36M_NAMES[2]  = 'RKnee'
// H36M_NAMES[3]  = 'RFoot'
// H36M_NAMES[6]  = 'LHip'
// H36M_NAMES[7]  = 'LKnee'
// H36M_NAMES[8]  = 'LFoot'
// H36M_NAMES[12] = 'Spine'
// H36M_NAMES[13] = 'Thorax'
// H36M_NAMES[14] = 'Neck/Nose'
// H36M_NAMES[15] = 'Head'
// H36M_NAMES[17] = 'LShoulder'
// H36M_NAMES[18] = 'LElbow'
// H36M_NAMES[19] = 'LWrist'
// H36M_NAMES[25] = 'RShoulder'
// H36M_NAMES[26] = 'RElbow'
// H36M_NAMES[27] = 'RWrist'

// 本プログラムでは、上のボーン構造のインデックスを0から16の配列の要素に変えて、扱う。
// 0.尻(尾てい骨)
// 1.右尻(右足付け根)
// 2.右ひざ
// 3.右足首
// 4.左尻(左足付け根)
// 5.左ひざ
// 6.左足首
// 7.脊椎
// 8.胸
// 9.首/鼻
// 10.頭
// 11.左肩
// 12.左ひじ
// 13.左手首
// 14.右肩
// 15.右ひじ
// 16.右手首

public class OpenPose_pos_txt_Reader : MonoBehaviour
{
    class Node
    {
        public Vector3 pos;
    }

    class Body
    {
        public Node[] nn;

        public Body()
        {
            nn = new Node[17];

            for (int i = 0; i < nn.Length; i++)
            {
                nn[i] = new Node();
            }
        }
    }

    class MotionData
    {
        public List<Body> frameData;

        public MotionData()
        {
            frameData = new List<Body>();
        }

        public void DebugShowMotionData()
        {
            print("フレーム数 : " + frameData.Count);
            print("関節数 : " + frameData[0].nn.Length);
        }
    }

    int ConvertPoseIndex(int origIndex)
    {
        switch (origIndex)
        {
            case 6:
                origIndex = 4;
                break;
            case 7:
                origIndex = 5;
                break;
            case 8:
                origIndex = 6;
                break;
            case 12:
                origIndex = 7;
                break;
            case 13:
                origIndex = 8;
                break;
            case 14:
                origIndex = 9;
                break;
            case 15:
                origIndex = 10;
                break;
            case 17:
                origIndex = 11;
                break;
            case 18:
                origIndex = 12;
                break;
            case 19:
                origIndex = 13;
                break;
            case 25:
                origIndex = 14;
                break;
            case 26:
                origIndex = 15;
                break;
            case 27:
                origIndex = 16;
                break;
        }

        return origIndex;
    }

    // オリジナルのファイルを読み、各行をリストで返す。
    List<string> ReadOrigFile(string fileName)
    {
        List<string> lines = new List<string>();

        StreamReader sr = new StreamReader(fileName);
        while (!sr.EndOfStream)
        {
            lines.Add(sr.ReadLine());
        }
        sr.Close();

        return lines;
    }

    // そのフレームでのポーズを読み込む。
    Body ReadOneFramePose(string line)
    {
        Body bb = new Body();

        line = line.Replace(",", ""); // カンマを取り除く
        string[] str = line.Split(new string[] { " " }, System.StringSplitOptions.RemoveEmptyEntries); // スペースで分割し、空の文字列は削除

        for (int i = 0; i < str.Length; i += 4)
        {
            int idx = int.Parse(str[i]);
            Vector3 v = new Vector3(float.Parse(str[i + 1]), float.Parse(str[i + 2]), float.Parse(str[i + 3]));

            bb.nn[ConvertPoseIndex(idx)].pos = v;
        }

        return bb;
    }

    MotionData GetMotionData(string filename)
    {
        MotionData mo = new MotionData();

        List<string> lines = ReadOrigFile(filename);

        foreach (var line in lines)
        {
            Body b = ReadOneFramePose(line);
            mo.frameData.Add(b);
        }

        return mo;
    }

    // デバック用cubeを生成する。生成済みの場合は位置を更新する。
    void GenCube_UpdateCube(MotionData mo, int frame)
    {
        // インデックスエラーの場合は、ワーニングを吐く。
        try
        {
            var a = mo.frameData[frame];
        }
        catch (ArgumentOutOfRangeException)
        {
            Debug.LogError("frame[" + frame + "] フレーム数が異常. 上限は" + mo.frameData.Count);
            return;
        }

        float omomi = 0.01f;

        if (tt == null)
        {
            // 初期化して、cubeを生成する。
            tt = new Transform[mo.frameData[frame].nn.Length];

            for (int i = 0; i < mo.frameData[frame].nn.Length; i++)
            {
                Transform t = GameObject.CreatePrimitive(PrimitiveType.Cube).transform;
                t.transform.parent = transform;
                t.localPosition = mo.frameData[frame].nn[i].pos * omomi;
                t.name = i.ToString();
                t.localScale = transform.localScale;
                tt[i] = t;

                Destroy(t.GetComponent<BoxCollider>());
                // t.gameObject.SetActive(false);
            }

            print("ボーンを生成しました。");
        }
        else
        {
            // 初期化済みの場合は、cubeの位置を更新する。
            for (int i = 0; i < mo.frameData[frame].nn.Length; i++)
            {
                tt[i].localPosition = mo.frameData[frame].nn[i].pos * omomi;
            }
        }
    }

    Vector3 TriangleNormal(Vector3 a, Vector3 b, Vector3 c)
    {
        Vector3 d1 = a - b;
        Vector3 d2 = a - c;

        Vector3 dd = Vector3.Cross(d1, d2);
        dd.Normalize();

        return dd;
    }

    void GenInitRotQuat()
    {
        if (ze == null)
        {
            ze = new Quaternion[tt.Length]; // ttがnullだと駄目。
        }

        tp = new Transform[tt.Length];

        tp[0] = anim.GetBoneTransform(HumanBodyBones.Hips);

        // 右脚
        tp[1] = anim.GetBoneTransform(HumanBodyBones.RightUpperLeg);
        tp[2] = anim.GetBoneTransform(HumanBodyBones.RightLowerLeg);
        tp[3] = anim.GetBoneTransform(HumanBodyBones.RightFoot);

        // 左脚
        tp[4] = anim.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
        tp[5] = anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
        tp[6] = anim.GetBoneTransform(HumanBodyBones.LeftFoot);

        // 
        tp[7] = anim.GetBoneTransform(HumanBodyBones.Spine);
        tp[8] = anim.GetBoneTransform(HumanBodyBones.Chest);
        tp[9] = anim.GetBoneTransform(HumanBodyBones.Neck);
        tp[10] = anim.GetBoneTransform(HumanBodyBones.Head);

        // 左手
        tp[11] = anim.GetBoneTransform(HumanBodyBones.RightUpperArm);
        tp[12] = anim.GetBoneTransform(HumanBodyBones.RightLowerArm);
        tp[13] = anim.GetBoneTransform(HumanBodyBones.RightHand);

        // 左手
        tp[14] = anim.GetBoneTransform(HumanBodyBones.LeftUpperArm);
        tp[15] = anim.GetBoneTransform(HumanBodyBones.LeftLowerArm);
        tp[16] = anim.GetBoneTransform(HumanBodyBones.LeftHand);


        {
            tt_init_center = tt[0].position + tp[0].position;

            // 両肩とhipで三角形を作ってそれを前方向とする。
            ze[0] = Quaternion.Inverse(Quaternion.LookRotation(TriangleNormal(tp[0].position, tp[11].position, tp[14].position)));

            // 初期クオータニオンを取得。いわゆる初期TポーズでのLookRotationで取得できる値をここで格納し、Update関数でその差分を反映させている。
            ze[1] = Quaternion.Inverse(Quaternion.LookRotation(tp[1].position - tp[2].position));
            ze[2] = Quaternion.Inverse(Quaternion.LookRotation(tp[2].position - tp[3].position));

            ze[4] = Quaternion.Inverse(Quaternion.LookRotation(tp[4].position - tp[5].position));
            ze[5] = Quaternion.Inverse(Quaternion.LookRotation(tp[5].position - tp[6].position));

            {
                // 7.脊椎
                // 8.胸
                // 9.首/鼻
                // 10.頭
                //ze[7] = Quaternion.Inverse(Quaternion.LookRotation(tp[7].position - tp[0].position));
                //ze[8] = Quaternion.Inverse(Quaternion.LookRotation(tp[8].position - tp[7].position));
                //ze[9] = Quaternion.Inverse(Quaternion.LookRotation(tp[9].position - tp[8].position));
                //ze[10] = Quaternion.Inverse(Quaternion.LookRotation(tp[10].position - tp[9].position));

                ze[7] = Quaternion.Inverse(Quaternion.LookRotation(tp[0].position - tp[7].position));
                ze[8] = Quaternion.Inverse(Quaternion.LookRotation(tp[7].position - tp[8].position));
                ze[9] = Quaternion.Inverse(Quaternion.LookRotation(tp[8].position - tp[9].position));
                ze[10] = Quaternion.Inverse(Quaternion.LookRotation(tp[9].position - tp[10].position));
            }

            ze[11] = Quaternion.Inverse(Quaternion.LookRotation(tp[11].position - tp[12].position));
            ze[12] = Quaternion.Inverse(Quaternion.LookRotation(tp[12].position - tp[13].position));

            ze[14] = Quaternion.Inverse(Quaternion.LookRotation(tp[14].position - tp[15].position));
            ze[15] = Quaternion.Inverse(Quaternion.LookRotation(tp[15].position - tp[16].position));
        }

        //zero = Quaternion.LookRotation(tp[4] - tp[5]);
        //zero = Quaternion.Inverse(zero);
    }

    MotionData mo;
    int cnt;
    Transform[] tt; // デバック表示させているCubeの位置。
    Vector3 tt_init_center; // 初期のセンター位置。
    Transform[] tp; // Animatorのanim.GetBoneTransform(HumanBodyBonesで取得できるボーンへの参照。

    public Animator anim;

    //public Transform hip;
    //public Transform upleg;
    //public Transform lowleg;
    //public Transform foot;

    //Quaternion zero;
    //Quaternion zero2;

    Quaternion[] ze; // 初期の子ボーンへのクオータニオン。


    void Start()
    {
        cnt = 0;
        mo = GetMotionData("Assets\\pos_SambaDancing.txt");
        mo.DebugShowMotionData();
        GenCube_UpdateCube(mo, cnt);

        GenInitRotQuat();

        {
            // upleg.rotation = anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg).rotation;

            // zero = Quaternion.LookRotation(upleg.position, lowleg.position);

            //upleg.position = anim.GetBoneTransform(HumanBodyBones.LeftUpperLeg).position;
            //lowleg.position = anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg).position;
            //foot.position = anim.GetBoneTransform(HumanBodyBones.LeftFoot).position;

            //zero = Quaternion.LookRotation(upleg.position - lowleg.position);
            //zero = Quaternion.Inverse(zero);

            //zero2 = Quaternion.LookRotation(lowleg.position, foot.position);
            //zero2 = Quaternion.Inverse(zero2);
        }
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.E))
        {
            cnt++;
        }
        else if (Input.GetKeyDown(KeyCode.Q))
        {
            cnt--;
        }

        if (Time.frameCount % 2 == 0)
        {
            cnt++;
            if (cnt >= mo.frameData.Count)
            {
                cnt = 0;
            }
        }

        GenCube_UpdateCube(mo, cnt);

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit raycast;
        if (Physics.Raycast(ray, out raycast))
        {

        }

        //testTrans_1.LookAt(testTrans_2);
        //Quaternion q = testTrans_1.rotation;

        //{
        //    Transform t1 = transform.GetChild(11);
        //    Transform t2 = transform.GetChild(12);
        //    Transform t3 = transform.GetChild(13);

        //    anim.GetBoneTransform(HumanBodyBones.RightUpperArm).rotation = Quaternion.LookRotation(t1.position, t2.position);
        //    anim.GetBoneTransform(HumanBodyBones.RightLowerArm).rotation = Quaternion.LookRotation(t2.position, t3.position);
        //}

        //{
        //    Transform t1 = transform.GetChild(14);
        //    Transform t2 = transform.GetChild(15);
        //    Transform t3 = transform.GetChild(16);

        //    anim.GetBoneTransform(HumanBodyBones.LeftUpperArm).rotation = Quaternion.LookRotation(t1.position, t2.position);
        //    anim.GetBoneTransform(HumanBodyBones.LeftLowerArm).rotation = Quaternion.LookRotation(t2.position, t3.position);
        //}
        // anim.GetBoneTransform(HumanBodyBones.RightHand).rotation = q;

        {

            // 単体テスト
            // 回転が分かる場合はこれでうまくいく。
            // anim.GetBoneTransform(HumanBodyBones.LeftUpperLeg).rotation = upleg.rotation;
            // anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg).rotation = lowleg.rotation;//Quaternion.LookRotation(t2.position, t3.position);

            //anim.GetBoneTransform(HumanBodyBones.LeftUpperLeg).rotation = tt[4].rotation;// upleg.rotation;
            //anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg).rotation = tt[5].rotation;// lowleg.rotation;



            tp[1].rotation = Quaternion.LookRotation(tt[1].position - tt[2].position) * ze[1];
            tp[2].rotation = Quaternion.LookRotation(tt[2].position - tt[3].position) * ze[2];
            tp[4].rotation = Quaternion.LookRotation(tt[4].position - tt[5].position) * ze[4];
            tp[5].rotation = Quaternion.LookRotation(tt[5].position - tt[6].position) * ze[5];

            {
                // ここに問題がある。

                Vector3 nor = TriangleNormal(tt[7].position, tt[11].position, tt[14].position);
                Debug.DrawLine(transform.position, (transform.position + nor) * 1);
                // print(nor);

                tp[0].rotation = Quaternion.LookRotation(nor); // * ze[0];

                //tp[7].rotation = Quaternion.LookRotation(tt[7].position - tt[0].position) * ze[7];
                //tp[8].rotation = Quaternion.LookRotation(tt[8].position - tt[7].position) * ze[8];
                //tp[9].rotation = Quaternion.LookRotation(tt[9].position - tt[8].position) * ze[9];
                //tp[10].rotation = Quaternion.LookRotation(tt[10].position - tt[9].position) * ze[10];

                ze[7] = Quaternion.Inverse(Quaternion.LookRotation(tp[0].position - tp[7].position));
                ze[8] = Quaternion.Inverse(Quaternion.LookRotation(tp[7].position - tp[8].position));
                ze[9] = Quaternion.Inverse(Quaternion.LookRotation(tp[8].position - tp[9].position));
                ze[10] = Quaternion.Inverse(Quaternion.LookRotation(tp[9].position - tp[10].position));
            }


            tp[11].rotation = Quaternion.LookRotation(tt[11].position - tt[12].position) * ze[11];
            tp[12].rotation = Quaternion.LookRotation(tt[12].position - tt[13].position) * ze[12];

            tp[14].rotation = Quaternion.LookRotation(tt[14].position - tt[15].position) * ze[14];
            tp[15].rotation = Quaternion.LookRotation(tt[15].position - tt[16].position) * ze[15];
            //Quaternion q2 = Quaternion.LookRotation(lowleg.position, foot.position);
            //anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg).rotation = q2 * zero2;

            // センター移動を制御
            tp[0].position = -tt[0].position + tt_init_center;
        }
    }
}