LoginSignup
74
77

More than 3 years have passed since last update.

動画を自動トレース⇒Unityでキャラクターに踊ってもらう

Last updated at Posted at 2019-02-27

やりたいこと

動画からモーションを自動トレースして、Unityで好きなキャラクターに踊って(動いて)もらう

OpenPoseや3d-pose-baselineなどディープラーニング技術を組み合わせて、動画からMMDモーションを自動生成するプログラムを@miu200521358様が開発しています。ここではMMDの代わりに、Unityで動かします。

MMDモーショントレース自動化への挑戦【ver1.00】(ニコニコ動画)
https://www.nicovideo.jp/watch/sm34626229

実行例)
mjua9-enr2z.gif

自動トレース元: https://nico.ms/sm27620009?from=20 :やっこ様、まりやん様
3Dモデル:Masscat、Unity-Chan(© Unity Technologies Japan/UCL)

どうやって実現するか

MMDモーション自動生成の手順の途中で生成される3Dポーズデータ(pos.txt)を利用します。
@romaroma様が実現方法を以下の記事で紹介しています。ただ、なぜか私の環境(モデル:Unity-Chan, MassCat、Unity:ver.2017.4.19f1)で上手く動かなかったため、@romaroma様の記事を参考にてプログラムを作成しました。

参考ページ @romaroma様
https://qiita.com/romaroma/items/ffbdae4ecfc4c8ff31cd

1.3Dポーズデータの作成

以下の@miu200521358様の記事の参考にして動画から3Dポーズデータ(pos.txt)を作成してください。
クラウド(Google Colaboratory)上で実行することができます。実行には時間がかかります。最初は1人の動画で試してみることをお勧めします。

クラウド(colab)でMMD自動トレース
https://qiita.com/miu200521358/items/fb0a7bcf2764d7797e26

2.キャラクターを動かす

上記で作成されるpos.txtと下記のプログラム(Pos_txt_Reader.cs)を適当なフォルダに配置し、Pos_txt_Reader.csをキャラクターにアタッチ後、Pos_filenameにpos.txtのパスを指定し、再生してください。

プログラムはgithubにあげておきます。
https://github.com/kenkra/Unity-3d-pose-baseline
※追記 モーションファイル(BVH形式)の出力に対応しました(github参照)。

unitychan.png

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

// pos.txtのデータ
// https://github.com/miu200521358/3d-pose-baseline-vmd/blob/master/doc/Output.md
// 0 :Hip
// 1 :RHip
// 2 :RKnee
// 3 :RFoot
// 4 :LHip
// 5 :LKnee
// 6 :LFoot
// 7 :Spine
// 8 :Thorax
// 9 :Neck/Nose
// 10:Head
// 11:LShoulder
// 12:LElbow
// 13:LWrist
// 14:RShoulder
// 15:RElbow
// 16:RWrist

public class Pos_txt_Reader : MonoBehaviour
{
    float scale_ratio = 0.001f;  // pos.txtとUnityモデルのスケール比率
                                 // pos.txtの単位はmmでUnityはmのため、0.001に近い値を指定。モデルの大きさによって調整する
    float heal_position = 0.05f; // 足の沈みの補正値(単位:m)。プラス値で体全体が上へ移動する
    float head_angle = 15f; // 顔の向きの調整 顔を15度上げる

    public String pos_filename; // pos.txtのファイル名
    public Boolean debug_cube; // デバッグ用Cubeの表示フラグ
    public int start_frame; // 開始フレーム
    public String end_frame; // 終了フレーム  
    float play_time; // 再生時間 
    Transform[] bone_t; // モデルのボーンのTransform
    Transform[] cube_t; // デバック表示用のCubeのTransform
    Vector3 init_position; // 初期のセンターの位置
    Quaternion[] init_rot; // 初期の回転値
    Quaternion[] init_inv; // 初期のボーンの方向から計算されるクオータニオンのInverse
    List<Vector3[]> pos; // pos.txtのデータを保持するコンテナ
    int[] bones = new int[10] { 1, 2, 4, 5, 7, 8, 11, 12, 14, 15 }; // 親ボーン
    int[] child_bones = new int[10] { 2, 3, 5, 6, 8, 10, 12, 13, 15, 16 }; // bonesに対応する子ボーン
    int bone_num = 17;
    Animator anim;
    int s_frame;
    int e_frame;

    // pos.txtのデータを読み込み、リストで返す
    List<Vector3[]> ReadPosData(string filename) {
        List<Vector3[]> data = new List<Vector3[]>();

        List<string> lines = new List<string>();
        StreamReader sr = new StreamReader(filename);
        while (!sr.EndOfStream) {
            lines.Add(sr.ReadLine());
        }
        sr.Close();

        foreach (string line in lines) {
            string line2 = line.Replace(",", "");
            string[] str = line2.Split(new string[] { " " }, System.StringSplitOptions.RemoveEmptyEntries); // スペースで分割し、空の文字列は削除

            Vector3[] vs = new Vector3[bone_num];
            for (int i = 0; i < str.Length; i += 4) {
                vs[(int)(i/4)] = new Vector3(-float.Parse(str[i + 1]), float.Parse(str[i + 3]), -float.Parse(str[i + 2]));
            }
            data.Add(vs);
        }
        return data;
    }

    // BoneTransformの取得。回転の初期値を取得
    void GetInitInfo()
    {
        bone_t = new Transform[bone_num];
        init_rot = new Quaternion[bone_num];
        init_inv = new Quaternion[bone_num];

        bone_t[0] = anim.GetBoneTransform(HumanBodyBones.Hips);
        bone_t[1] = anim.GetBoneTransform(HumanBodyBones.RightUpperLeg);
        bone_t[2] = anim.GetBoneTransform(HumanBodyBones.RightLowerLeg);
        bone_t[3] = anim.GetBoneTransform(HumanBodyBones.RightFoot);
        bone_t[4] = anim.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
        bone_t[5] = anim.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
        bone_t[6] = anim.GetBoneTransform(HumanBodyBones.LeftFoot);
        bone_t[7] = anim.GetBoneTransform(HumanBodyBones.Spine);
        bone_t[8] = anim.GetBoneTransform(HumanBodyBones.Neck);
        bone_t[10] = anim.GetBoneTransform(HumanBodyBones.Head);
        bone_t[11] = anim.GetBoneTransform(HumanBodyBones.LeftUpperArm);
        bone_t[12] = anim.GetBoneTransform(HumanBodyBones.LeftLowerArm);
        bone_t[13] = anim.GetBoneTransform(HumanBodyBones.LeftHand);
        bone_t[14] = anim.GetBoneTransform(HumanBodyBones.RightUpperArm);
        bone_t[15] = anim.GetBoneTransform(HumanBodyBones.RightLowerArm);
        bone_t[16] = anim.GetBoneTransform(HumanBodyBones.RightHand);

        // Spine,LHip,RHipで三角形を作ってそれを前方向とする。
        Vector3 init_forward = TriangleNormal(bone_t[7].position, bone_t[4].position, bone_t[1].position);
        init_inv[0] = Quaternion.Inverse(Quaternion.LookRotation(init_forward));

        init_position = bone_t[0].position;
        init_rot[0] = bone_t[0].rotation;
        for (int i = 0; i < bones.Length; i++) {
            int b = bones[i];
            int cb = child_bones[i];

            // 対象モデルの回転の初期値
            init_rot[b] = bone_t[b].rotation;
            // 初期のボーンの方向から計算されるクオータニオン
            init_inv[b] = Quaternion.Inverse(Quaternion.LookRotation(bone_t[b].position - bone_t[cb].position,init_forward));
        }
    }

    // 指定の3点でできる三角形に直交する長さ1のベクトルを返す
    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;
    }

    // デバック用cubeを生成する。生成済みの場合は位置を更新する
    void UpdateCube(int frame)
    {
        if (cube_t == null) {
            // 初期化して、cubeを生成する
            cube_t = new Transform[bone_num];

            for (int i = 0; i < bone_num; i++) {
                Transform t = GameObject.CreatePrimitive(PrimitiveType.Cube).transform;
                t.transform.parent = this.transform;
                t.localPosition = pos[frame][i] * scale_ratio;
                t.name = i.ToString();
                t.localScale = new Vector3(0.05f, 0.05f, 0.05f);
                cube_t[i] = t;

                Destroy(t.GetComponent<BoxCollider>());
            }
        }
        else {
            // モデルと重ならないように少しずらして表示
            Vector3 offset = new Vector3(1.2f, 0, 0);

            // 初期化済みの場合は、cubeの位置を更新する
            for (int i = 0; i < bone_num; i++) {
                cube_t[i].localPosition = pos[frame][i] * scale_ratio + new Vector3(0, heal_position, 0) + offset;
            }
        }
    }

    void Start()
    {
        anim = GetComponent<Animator>();
        play_time = 0;
        if (System.IO.File.Exists (pos_filename) == false) {
            Debug.Log("<color=blue>Error! Pos file not found(" + pos_filename + "). Check Pos_filename in Inspector.</color>");
        }
        pos = ReadPosData(pos_filename);
        GetInitInfo();
        if (pos != null) {
            // inspectorで指定した開始フレーム、終了フレーム番号をセット
            if (start_frame >= 0 && start_frame < pos.Count) {
                s_frame = start_frame;
            } else {
                s_frame = 0;
            }
            int ef;
            if (int.TryParse(end_frame, out ef)) {
                if (ef >= s_frame && ef < pos.Count) {
                    e_frame = ef;
                } else {
                    e_frame = pos.Count - 1;
                }
            } else {
                e_frame = pos.Count - 1;
            }
            Debug.Log("End Frame:" + e_frame.ToString());
        }
    }

    void Update()
    {
        if (pos == null) {
            return;
        }
        play_time += Time.deltaTime;

        int frame = s_frame + (int)(play_time * 30.0f);  // pos.txtは30fpsを想定
        if (frame > e_frame) {
            play_time = 0;  // 繰り返す
            frame = s_frame;
        }

        if (debug_cube) {
            UpdateCube(frame); // デバッグ用Cubeを表示する
        }

        Vector3[] now_pos = pos[frame];

        // センターの移動と回転
        Vector3 pos_forward = TriangleNormal(now_pos[7], now_pos[4], now_pos[1]);
        bone_t[0].position = now_pos[0] * scale_ratio + new Vector3(init_position.x, heal_position, init_position.z);
        bone_t[0].rotation = Quaternion.LookRotation(pos_forward) * init_inv[0] * init_rot[0];

        // 各ボーンの回転
        for (int i = 0; i < bones.Length; i++) {
            int b = bones[i];
            int cb = child_bones[i];
            bone_t[b].rotation = Quaternion.LookRotation(now_pos[b] - now_pos[cb], pos_forward) * init_inv[b] * init_rot[b];
        }

        // 顔の向きを上げる調整。両肩を結ぶ線を軸として回転
        bone_t[8].rotation = Quaternion.AngleAxis(head_angle, bone_t[11].position - bone_t[14].position) * bone_t[8].rotation;
    }
}
74
77
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
74
77