31
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ユニティちゃんの関節を動かしたい話

Last updated at Posted at 2018-06-16

はじめに

Unity歴2か月の素人の私が、3Dモデルの関節操作で躓いたのと、その解決方法の話です。

やりたいこと

3Dモデルキャラクター(ユニティちゃん)の姿勢を制御したい。

具体的なアイデア

3Dモデルキャラクターは、多重親子関係のボーンの組み合わせで成り立っています。
親のボーンを回転させれば、子のボーンもその回転方向に動いてくれます。
また、2点間のオブジェクトの回転制御は、LookRotation()関数を使うことで実現できるようです。

参考:@romaroma様
Unityで座標値からその方向を向く回転を取得する

つまり、親となる関節に、LookRotation()関数で導出できるクオータニオンを代入してやれば、
なんとなくうまくいくであろうことが予想されます。

とりあえずやってみた

@romaroma様の記事を参考にしながら、
2つのオブジェクト(箱と玉)と空のゲームオブジェクト、そしてユニティちゃんオブジェクトを配置。
箱から玉へのベクトルが、ユニティちゃんの右肩の動きと連動することを意図して下記のスクリプトを書きました。
このスクリプトは、空のゲームオブジェクトにアタッチします。

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

public class example1 : MonoBehaviour {

	public enum Parts{
		spine,chest,neck,rightShoulder,rightElbow,leftShoulder,leftElbow,rightHip,rightKnee,leftHip,leftKnee
	};

	private GameObject cube;
	private GameObject sphere;
	private Animator anim;
	public Parts target;

	private Transform from_bone;
	private Transform to_bone;
	private Quaternion rq;

	// Use this for initialization
	void Start () {
		// 必要なオブジェクトを取得
		cube = GameObject.Find ("Cube");
		sphere = GameObject.Find ("Sphere");
		anim = (Animator)FindObjectOfType (typeof(Animator));

		// Inspector上での入力をもとに操作する関節を決定
		// 本例では target=rightshoulder に設定されている
		AttachTarget ();
	}

	// Update is called once per frame
	void Update () {
		Quaternion c2sqt;
		Vector3 c2svec = cube.transform.position - sphere.transform.position;
		c2sqt = Quaternion.LookRotation (c2svec);
		// 回転処理
		from_bone.rotation = c2sqt;
	}

	void AttachTarget(){
		switch (target) {
		case Parts.rightShoulder:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.RightUpperArm);
				to_bone = anim.GetBoneTransform (HumanBodyBones.RightLowerArm);
				break;
			}
      // 以下、全ての関節について設定するが、中略  
		}
	}
}

画面はこんな感じ
fig1.png

で、これを実際に動かすとまぁうまく動かないわけです。

fig2.png

何故うまくいかないか

@romaroma様の別の記事を参考にすると、
どうやら3Dキャラクターのボーン間の初期クオータニオンを事前に保存しておいて、
回転に反映させる際に、その逆回転を加えてやれば良いことがわかりました。
(ちなみに、時系列的にはこちらの記事を先に読んだのですが、
話の進め方的にこちらを後に紹介することとなりました)

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

再チャレンジ

上記のアイデアをもとに、

  • Start()関数内で、ボーン間の初期クオータニオンの逆を記憶
  • Update()関数内での、回転処理に逆回転の乗算を追加

したのが以下のスクリプトです、変更差分のみ記述します。


// 中略

// Use this for initialization
void Start () {
	// 必要なオブジェクトを取得
	cube = GameObject.Find ("Cube");
	sphere = GameObject.Find ("Sphere");
	anim = (Animator)FindObjectOfType (typeof(Animator));

	// Inspector上での入力をもとに操作する関節を決定
	// 本例では target=rightshoulder に設定されている
	AttachTarget ();

	// 操作したい関節とその子の関節の初期クオータニオンの逆を保存
	rq = Quaternion.Inverse (Quaternion.LookRotation (from_bone.localPosition - to_bone.localPosition));
}

// Update is called once per frame
void Update () {
	Quaternion c2sqt;
	Vector3 c2svec = cube.transform.position - sphere.transform.position;
	c2sqt = Quaternion.LookRotation (c2svec);
	// 回転処理
	from_bone.rotation = c2sqt * rq;
}

// 中略

結果、、おぉ、うまくいきました!

fig3.png

今回は右肩RightUpperArmを回転させましたが、これ以外にも以下の関節でうまく動くことを確認しました。

  • 左肩LeftUpperArm
  • 左足股関節LeftUpperLeg
  • 左膝LeftLowerLeg
  • 右足股関節RightUpperLeg
  • 右膝RightLowerLeg

肘がうまく動かない

同じ手法で、右肘RightLowerArmも動かしてみたいと思います。

fig4.png

逆ゥ!!

肘を動かすには

なんでこんなことになるのかUnity素人の私にはわからないのですが、
対症療法的に、2点間のベクトルを反転させたらうまく動きました。
スクリプトは以下の通り。


//中略

// Update is called once per frame
void Update () {
	Quaternion c2sqt;
	Vector3 c2svec = cube.transform.position - sphere.transform.position;
	c2svec *= -1.0f;
	c2sqt = Quaternion.LookRotation (c2svec);
	// 回転処理
	from_bone.rotation = c2sqt * rq;
}

//中略

結果はこんな感じ。

fig5.png

右肘RightLowerArm以外に、左肘LeftLowerArmでもうまくいくことを確認しました。

胴体がうまく動かない

右肩の時と同じ手法で胴体系のパーツも動かそうとしましたが、まぁうまく動かないわけです。
今回はSpineを動かしてみようと思います。

fig6.png

人間じゃねぇ!!

胴体を動かすには

この辺から本格的になんでこんなことになるのかさっぱりわかってないのですが、
色々いじった結果下記のことがわかりました。

  • 後ろに反る姿勢に関しては、肘の時同様、ベクトルを反転してやれば良い
  • 前に倒れる姿勢に関しては全くもって謎だが、逆回転やら軸回転をしてやったらなんかうまく動く

というわけで以下のスクリプトができあがりました(白目)


// 中略

// Update is called once per frame
void Update () {
	Quaternion c2sqt;
	Vector3 c2svec = cube.transform.position - sphere.transform.position;
	if (c2svec.z > 0) {
		c2svec *= -1.0f;
		c2sqt = Quaternion.LookRotation (c2svec);
	} else {
		c2svec.z *= -1.0f;
		c2sqt = Quaternion.Inverse (Quaternion.LookRotation (c2svec));
		c2sqt = c2sqt * Quaternion.AngleAxis (180, Vector3.forward);		
	}
	// 回転処理
	from_bone.rotation = c2sqt * rq;
}

// 中略

ちゃんと動いてくれますね。かわいい。

fig7.png

Spine以外に、首Neckもうまく動きました

胸がうまく動かない

あとは胸Chestさえうまく動けば完成なのですが、これもまぁダメなわけで。

fig8.png

ただ、これは子のボーンをNeckに指定してるのが、そもそも見当違いなのかもしれませんが・・・。

胸を動かすには

これも私よくわかってないんですが以下のスクリプトでうまくいきました(ぶん投げ)


//中略

// Update is called once per frame
void Update () {
	Quaternion c2sqt;
	Vector3 c2svec = cube.transform.position - sphere.transform.position;
	if (c2svec.z > 0) {
		c2sqt = Quaternion.LookRotation (c2svec);
	} else {
		c2svec.x *= -1.0f;
		c2sqt = Quaternion.Inverse (Quaternion.LookRotation (c2svec));
		c2sqt = c2sqt * Quaternion.AngleAxis (180, Vector3.forward);		
	}
	// 回転処理
	from_bone.rotation = c2sqt * rq;
}

// 中略

かわいい(確信)

fig9.png

完成したスクリプト

というわけで、以上の結果をまとめたスクリプトがこちら。

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

public class UnityChanControllerv2 : MonoBehaviour {

	public enum Parts{
		spine,chest,neck,rightShoulder,rightElbow,leftShoulder,leftElbow,rightHip,rightKnee,leftHip,leftKnee
	};

	private GameObject cube;
	private GameObject sphere;
	private Animator anim;
	public Parts target;

	private Transform from_bone;
	private Transform to_bone;
	private Quaternion rq;

	// Use this for initialization
	void Start () {
		// 必要なオブジェクトを取得
		cube = GameObject.Find ("Cube");
		sphere = GameObject.Find ("Sphere");
		anim = (Animator)FindObjectOfType (typeof(Animator));

		// Inspector上での入力をもとに操作する関節を決定
		AttachTarget ();

		// 操作したい関節とその子の関節の初期クオータニオンの逆を保存
		rq = Quaternion.Inverse (Quaternion.LookRotation (from_bone.localPosition - to_bone.localPosition));
	}

	// Update is called once per frame
	void Update () {
		Quaternion c2sqt;
		Vector3 c2svec = cube.transform.position - sphere.transform.position;
		// 胴体の関節を動かす
		if(target == Parts.neck||target == Parts.chest||target == Parts.spine){
			// 胸の関節を動かす
			if (target == Parts.chest) {
				if (c2svec.z > 0) {
					c2sqt = Quaternion.LookRotation (c2svec);
				} else {
					c2svec.x *= -1.0f;
					c2sqt = Quaternion.Inverse (Quaternion.LookRotation (c2svec));
					c2sqt = c2sqt * Quaternion.AngleAxis (180, Vector3.forward);		
				}
			}
			// 胸以外の関節を動かす
			else {
				if (c2svec.z > 0) {
					c2svec *= -1.0f;
					c2sqt = Quaternion.LookRotation (c2svec);
				} else {
					c2svec.z *= -1.0f;
					c2sqt = Quaternion.Inverse (Quaternion.LookRotation (c2svec));
					c2sqt = c2sqt * Quaternion.AngleAxis (180, Vector3.forward);		
				}
			}
		}
		// 腕または足の関節を動かす
		else {
			// 肘の場合
			if (target == Parts.leftElbow || target == Parts.rightElbow) {
				c2svec *= -1.0f;
				c2sqt = Quaternion.LookRotation (c2svec);
			}
			// 肘以外の場合
			else {
				c2sqt = Quaternion.LookRotation(c2svec);
			}
		}
		// 回転処理
		from_bone.rotation = c2sqt * rq;
	}

	void AttachTarget(){
		switch (target) {
		case Parts.neck:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.Neck);
				to_bone = anim.GetBoneTransform (HumanBodyBones.Head);
				break;
			}
		case Parts.chest:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.Chest);
				to_bone = anim.GetBoneTransform (HumanBodyBones.Neck);
				break;
			}
		case Parts.spine:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.Spine);
				to_bone = anim.GetBoneTransform (HumanBodyBones.Chest);
				break;
			}
		case Parts.rightShoulder:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.RightUpperArm);
				to_bone = anim.GetBoneTransform (HumanBodyBones.RightLowerArm);
				break;
			}
		case Parts.rightElbow:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.RightLowerArm);
				to_bone = anim.GetBoneTransform (HumanBodyBones.RightHand);
				break;
			}
		case Parts.leftShoulder:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.LeftUpperArm);
				to_bone = anim.GetBoneTransform (HumanBodyBones.LeftLowerArm);
				break;
			}
		case Parts.leftElbow:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.LeftLowerArm);
				to_bone = anim.GetBoneTransform (HumanBodyBones.LeftHand);
				break;
			}
		case Parts.rightHip:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.RightUpperLeg);
				to_bone = anim.GetBoneTransform (HumanBodyBones.RightLowerLeg);
				break;
			}
		case Parts.rightKnee:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.RightLowerLeg);
				to_bone = anim.GetBoneTransform (HumanBodyBones.RightFoot);
				break;
			}
		case Parts.leftHip:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.LeftUpperLeg);
				to_bone = anim.GetBoneTransform (HumanBodyBones.LeftLowerLeg);
				break;
			}
		case Parts.leftKnee:
			{
				from_bone = anim.GetBoneTransform (HumanBodyBones.LeftLowerLeg);
				to_bone = anim.GetBoneTransform (HumanBodyBones.LeftFoot);
				break;
			}
		}
	}
}

おわりに

ネットで調べても、意外とボーン制御のやり方の説明が書いてある記事が少なく、
ここまでたどりつくだけでも随分と苦労しました。
今回、なんとか気合でボーン制御ができてるように見えますが、なんでこれでうまくいってるのかわかってない状況です。
もし詳しい方がいたら教えてください(逃げ)


u_logo.png

この作品はユニティちゃんライセンス条項の元に提供されています

31
22
0

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
31
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?