はじめに
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;
}
// 以下、全ての関節について設定するが、中略
}
}
}
で、これを実際に動かすとまぁうまく動かないわけです。
何故うまくいかないか
@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;
}
// 中略
結果、、おぉ、うまくいきました!
今回は右肩RightUpperArm
を回転させましたが、これ以外にも以下の関節でうまく動くことを確認しました。
- 左肩
LeftUpperArm
- 左足股関節
LeftUpperLeg
- 左膝
LeftLowerLeg
- 右足股関節
RightUpperLeg
- 右膝
RightLowerLeg
肘がうまく動かない
同じ手法で、右肘RightLowerArm
も動かしてみたいと思います。
逆ゥ!!
肘を動かすには
なんでこんなことになるのか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;
}
//中略
結果はこんな感じ。
右肘RightLowerArm
以外に、左肘LeftLowerArm
でもうまくいくことを確認しました。
胴体がうまく動かない
右肩の時と同じ手法で胴体系のパーツも動かそうとしましたが、まぁうまく動かないわけです。
今回はSpine
を動かしてみようと思います。
人間じゃねぇ!!
胴体を動かすには
この辺から本格的になんでこんなことになるのかさっぱりわかってないのですが、
色々いじった結果下記のことがわかりました。
- 後ろに反る姿勢に関しては、肘の時同様、ベクトルを反転してやれば良い
- 前に倒れる姿勢に関しては全くもって謎だが、逆回転やら軸回転をしてやったらなんかうまく動く
というわけで以下のスクリプトができあがりました(白目)
// 中略
// 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;
}
// 中略
ちゃんと動いてくれますね。かわいい。
腰Spine
以外に、首Neck
もうまく動きました
胸がうまく動かない
あとは胸Chest
さえうまく動けば完成なのですが、これもまぁダメなわけで。
ただ、これは子のボーンを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;
}
// 中略
かわいい(確信)
完成したスクリプト
というわけで、以上の結果をまとめたスクリプトがこちら。
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;
}
}
}
}
おわりに
ネットで調べても、意外とボーン制御のやり方の説明が書いてある記事が少なく、
ここまでたどりつくだけでも随分と苦労しました。
今回、なんとか気合でボーン制御ができてるように見えますが、なんでこれでうまくいってるのかわかってない状況です。
もし詳しい方がいたら教えてください(逃げ)
この作品はユニティちゃんライセンス条項の元に提供されています