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

## やりたいこと
動画からモーションを自動トレースして、Unityで好きなキャラクターに踊って（動いて）もらう

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

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

実行例）
![mjua9-enr2z.gif](https://qiita-image-store.s3.amazonaws.com/0/215982/ca987eae-4633-15fe-e1ac-21bc68f22bc9.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](https://qiita-image-store.s3.amazonaws.com/0/215982/58ff2ce4-516d-583e-ffb0-af22e0c3bcc1.png)

```C#: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;
	}
}
```