読者です 読者をやめる 読者になる 読者になる

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

MMD for Unity を使って Oculus 用アプリを作ってみた

Unity C# Oculus MMD

はじめに

先日、Oculus Rift 用のアプリを作って公開してみました。


ミクさんがミクさんになった自分についてくるアプリです。Oculus Rift では、下を向くと自分の身体が無いことに違和感を感じるので、何かキャラクターの身体を表示してみたいなと思って作ってみました。
作業の間にハマった点等をメモとして残しておきます。

お借りした素材

  • ステージ
    • Sci-Fi Level
      • Unity Asset Store より

首の動きと VMD との連動について

アイドル時や歩行時のアニメーションは MMD を使って作成した VMD を吐き出して、そのアニメーションをしながら頭だけはカメラの方を向くようにしたかったのですが、最初はホラーのように頭がガクガク動いてしまって困ってました。結論としては VMD に頭の座標も入っていたからだったようです。気づくまでに 2 日くらい掛かりました…。

なお、頭は以下の様なコードで動かすことができます。

// 何らかの方法で頭とカメラのゲームオブジェクトを取得
GameObject head   = GameObject.FindWithTag("MikuHead");
GameObject camera = GameObject.FindWithTag("MainCamera");

// クォータニオンと球面線形補間で滑らかに顔をカメラの方へ向かせる
Quaternion headRotFrom = head.transform.rotation;
Quaternion headRotTo   = Quaternion.LookRotation(head.transform.position - camera.transform.position);
head.transform.rotation = Quaternion.Slerp(headRotFrom, headRotTo, 0.04f);

そのままだと首があらぬ方向へ向いてしまうので、Lukalus のように前を向かせるか、本作のように歩いて自分の方を向き直すようにするのが吉だと思います。

コード全容は以下になります。

ミクさん
using UnityEngine;
using System.Collections;

public class MikuController : MonoBehaviour {
	
	private GameObject player_;
	private GameObject camera_;
	private GameObject head_;
	private GameObject miku_;
	private CharacterController controller_ = null;
	
	private bool isFollow_ = false;
	public float followDistance = 3.0f;
	public float waitDistance   = 1.5f;
	
	public float speed = 5.0f;
	public float gravity = 3.0f;
	public float rotationSpeed = 1.0f;
	
	void Awake() {
		// Get player gameObject
		player_ = GameObject.FindWithTag("Player");
		
		// Get Miku-san's head gameObject
		head_ = GameObject.FindWithTag("MikuHead");
		Debug.Log (head_.name);
		
		// Get Character Controller
		controller_ = gameObject.GetComponent<CharacterController>();
		
		// Get Miku
		miku_ = gameObject.transform.FindChild("Miku").gameObject;
		
		// Get main camera
		camera_ = GameObject.FindWithTag("MainCamera");
	}
	
	void Update() {
		// Walk toward camera
		// --------------------------------------------------------------
		float distance = Vector3.Distance(transform.position, player_.transform.position);
		if (distance > followDistance) {
			isFollow_ = true;
			miku_.animation.CrossFade("miku_walk");
		}
		if (distance < waitDistance) {
			isFollow_ = false;
			miku_.animation.CrossFade("miku_idle");
		}
		
		// Direction
		// --------------------------------------------------------------
		Vector3 playerDirection = player_.transform.position - transform.position; 
		float diffAng = Vector3.Angle(transform.forward, playerDirection);
		if (diffAng > 70) {
			isFollow_ = true;
			miku_.animation.CrossFade("miku_walk");
		}
		
		// Face to camera position
		// --------------------------------------------------------------
		Quaternion headRotFrom = head_.transform.rotation;
		Quaternion headRotTo   = Quaternion.LookRotation(head_.transform.position - camera_.transform.position);
		head_.transform.rotation = Quaternion.Slerp(headRotFrom, headRotTo, 0.04f);
		
		// Follow
		// --------------------------------------------------------------
		if (isFollow_) {
			Quaternion mikuRotFrom = transform.rotation; 
			Quaternion mikuRotTo   = Quaternion.LookRotation(player_.transform.position - transform.position);
			mikuRotFrom.x = 0; mikuRotFrom.z = 0;
			mikuRotTo.x   = 0; mikuRotTo.z   = 0;
			transform.rotation = Quaternion.Slerp(mikuRotFrom, mikuRotTo, 0.05f); 
			controller_.Move(transform.forward * speed * Time.deltaTime);
		}
	}
}
プレイヤー
using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour {

	public float speed = 5.0f;
	public float gravity = 3.0f;
	public float rotationSpeed = 1.0f;
	private OVRCameraController camera_ = null;
	private CharacterController controller_ = null;
	private GameObject miku_ = null;

	void Start() {
		// Get Oculus camera information (Ref: OVRPlayerController.cs)	
		OVRCameraController[] CameraControllers;
		CameraControllers = gameObject.GetComponentsInChildren<OVRCameraController>();
		if(CameraControllers.Length == 0) {
			Debug.LogWarning("OVRPlayerController: No OVRCameraController attached.");
		} else if (CameraControllers.Length > 1) {
			Debug.LogWarning("OVRPlayerController: More then 1 OVRCameraController attached.");
		} else {
			camera_ = CameraControllers[0];
		}
		
		// Get character controller
		controller_ = gameObject.GetComponent<CharacterController>();
		if (controller_ == null) Debug.LogError("No CharacterController detected!");
		
		// Get Miku-san
		miku_ = gameObject.transform.FindChild("Miku").gameObject;
		if (miku_ == null) Debug.LogError("No MIKU found!");
	}
	
	void Update() {
		// Get degrees from Oculus
		Vector3 angles = Vector3.zero;
		camera_.GetCameraOrientationEulerAngles(ref angles);
		
		// player direction
		bool moveForward = false;
		bool moveBack    = false;
		bool moveLeft    = false;
		bool moveRight   = false;
		bool rotateLeft  = false;
		bool rotateRight = false;
		Vector3 moveDirection = Vector3.zero;
		float rotation = 0.0f;
		
		if (Input.GetKey(KeyCode.W)) {
			moveForward = true;
			moveDirection += Vector3.forward;
		}
		if (Input.GetKey(KeyCode.S)) {
			moveBack = true;
			moveDirection += Vector3.back;
		}
		if (Input.GetKey(KeyCode.A)) {
			moveLeft = true;
			moveDirection += Vector3.left;
			moveDirection *= Mathf.Sin(Mathf.PI / 4);
		}
		if (Input.GetKey(KeyCode.D)) {
			moveRight = true;
			moveDirection += Vector3.right;
			moveDirection *= Mathf.Sin(Mathf.PI / 4);
		}
		if (Input.GetKey(KeyCode.Q)) {
			rotateLeft = true;
			rotation -= rotationSpeed;
		}
		if (Input.GetKey(KeyCode.E)) {
			rotateRight = true;
			rotation += rotationSpeed;
		}
		
		// speed
		moveDirection = transform.TransformDirection(moveDirection);
		
		// gravity
		moveDirection += Vector3.down * gravity;
		
		// animation
		if (moveForward || moveBack || moveLeft || moveRight || rotateLeft || rotateRight) {
			miku_.animation.CrossFade("miku_walk");
		} else {
			miku_.animation.CrossFade("miku_idle");
		}
		
		// move
		controller_.Move(moveDirection * speed * Time.deltaTime);
		
		// rotate
		transform.Rotate(Vector3.up * rotation * 100.0f * Time.deltaTime);
	}
}

高速化について

GPUQuadro 600 を使っているのですが、フレームレートが 40 FPS 強程度しか出なかったので調べてみた所、IK の処理が結構重くなっているようです。

そこで、髪の毛やネクタイ、足など IK を使用している所の「iteration」の値をできるだけ下げてみたところ、60 fps 出るようになりました。後は見えないプレイヤー側のミクさんの「Expression」(表情データ)のチェックボックスを OFF にして、少し早くなった気がします。

おわりに

次は Kinect 連動とか Razer Hydra 連動をやってみます。
Unity でゲーム創ってると、ロジックで動かしたいトコだけコード書けば良いのが楽で良いですね。