MRが楽しい

MRやVRについて学習したことを書き残す

ユニティちゃんと追いかけっこする

ユニティちゃんシリーズの続きとなります。
前回の記事でゲームパッドを元にユニティちゃんを動かしました。

【HoloLens開発】ユニティちゃんとHoloLensで戯れる - ゲームパッド編 -
 http://bril-tech.blogspot.jp/2016/11/hololens-gamepad.html


しかし、折角ハンズフリーのholosensでキャラクターの操作にパッドやキーボードが必要なのも悲しい話です。
今回はユニティちゃんの自動操作を試してみます。

理屈は簡単。hololensでは自分の座標はメインカメラの座標なので
ユニティちゃんは自動操作がオンになると、メインカメラの座標に向かって歩き続けます。

自動操作のトリガーに音声認識を用いて、完全にハンズフリーで操作できるようにします。
f:id:bluebirdofoz:20170414233345j:plain
「here」とユニティちゃんに声をかけると…
f:id:bluebirdofoz:20170414233358j:plain
こちらに向かって歩いてきてくれました。
カメラの座標に向かって歩き続けるので、自身が動くとユニティちゃんはそこに向かって歩きなおします。
鬼ごっこのような遊びが可能です。

XboxOneController.csのコードを参考に、以下のカメラ座標に向かって歩く関数を作成しました。

    // カメラ方向への移動関数
    void moveToMainCamera(float power)
    {
        // メインカメラ(HoloLensの視線)方向を取得
        Vector3 forward = Camera.main.transform.TransformDirection(Vector3.forward);
        Vector3 right = Camera.main.transform.TransformDirection(Vector3.right);
        //Debug.Log("forward:" + forward + ",right:" + right);

        float h;
        float v;
        Vector3 cPosition = Camera.main.transform.position;
        Vector3 uPosition = transform.position;
        float cameraX = cPosition.x;
        float cameraZ = cPosition.z;
        float unityX = uPosition.x;
        float unityZ = uPosition.z;
        float horizontalPos = ((cameraX - unityX) * (right.x)) + ((cameraZ - unityZ) * (right.z));
        float verticalPos = ((cameraZ - unityZ) * (forward.z)) + ((cameraX - unityX) * (forward.x));
        float horizontalRange = horizontalPos;
        float verticalRange = verticalPos;
        if (horizontalPos < 0) horizontalRange = horizontalRange * -1;
        if (verticalPos < 0) verticalRange = verticalRange * -1;
        float sum = horizontalRange + verticalRange;
        float hPer = Mathf.Floor((horizontalRange * 100.0f) / sum);
        float vPer = Mathf.Floor((verticalRange * 100.0f) / sum);
        h = power * (hPer / 100.0f);
        v = power * (vPer / 100.0f);
        if (horizontalPos < 0) h = h * -1;
        if (verticalPos < 0) v = v * -1;

        // カメラ座標による操作
        float speed = Mathf.Sqrt(v * v + h * h);        // vとhから左スティックの傾き具合を算出
        if (speed > 0.1)
        {
            anim.SetFloat("Speed", speed);

            if (Camera.main != null)
            {
                // メインカメラ(HoloLensの視線)方向を取得
                moveDirection = h * right + v * forward;
                velocity = moveDirection * forwardSpeed;

                Vector3 AnimDir = velocity;
                AnimDir.y = 0;
                if (AnimDir.sqrMagnitude > 0.001)
                {
                    // 前進する方向に旋回
                    Vector3 newDir = Vector3.RotateTowards(transform.forward, AnimDir, 10f * Time.deltaTime, 0f);
                    transform.rotation = Quaternion.LookRotation(newDir);
                }

                // 移動処理
                transform.position += velocity * Time.fixedDeltaTime;
            }
        }
    }

苦戦の程がうかがえます。。
メインカメラの座標、ゲームの絶対座標、ユニティちゃんの認識座標が絡むため、
座標の解釈と変換の式の正解を得るのにかなり時間を要しました。
(位置を移動すると意図しない方向へとあっちいったりこっちいったり…)

間違いなくシェイプアップできますが、記録として残しておきます。

ユニティちゃんが口パクを…できなかった

拘りだすと止まらない。喋ってるのに口閉じてるのが違和感あったので口パクをつけることにしました。

その前に一点、問題が発生しました。
これまでミスして古いバージョンのユニティちゃんを使っていました。
バージョンアップをしたところ、以下のエラーが発生しました。

・Assets\UnityChan\Scripts\SpringManager.cs 72行目
Assets\UnityChan\Scripts\SpringManager.cs(72,42): error CS1061: 'Type' does not contain a definition for 'GetField' and no extension method 'GetField' accepting a first argument of type 'Type' could be found (are you missing a using directive or an assembly reference?)

関数名は違えど、Type型というところがHoloToolKitで出たエラーと同じ感じ…。
こちらは解決方法が分からず、ひとまず当該コードを無効化して回避しました。
SpringManagerはキャラクターのポリゴンの揺れを表現するものらしいので、大きな影響はないはず。


さて、本題の口パクです。今回参考にしたのは以下のページです。
・Unity でリップシンクができる OVRLipSync を試してみた
 http://tips.hecomi.com/entry/2016/02/16/202634

紹介ページのリンクからはライブラリがダウンロードできませんでした。
今回は以下から取得しています。
・Oculus OVRLipSync for Unity 5
 https://developer.oculus.com/downloads/package/oculus-ovrlipsync-for-unity-5/1.0.1-beta/


説明の通り、セットアップすることで成功しました。
ユニティちゃんに関連付けられたAudioSourceを利用する必要があるため、LookColider.csを再修正しています。

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

public class LookCollider : MonoBehaviour
{
    AudioSource audioSource = null;

    public string searchTarget = "MainCamera";
    public GameObject messageTarget;

    public AudioClip impactClip = null;
    public float impactVolume = 0.5f;

    // Use this for initialization
    void Start ()
    {
    }

    void OnTriggerEnter(Collider col)
    {
        HeadLookController con;
        // 衝突対象が検索対象ならば実行
        if(col.tag == searchTarget)
        {
            //ルックコントローラを有効化
            con = messageTarget.GetComponent<HeadLookController>();
            con.enabled = true;
            //音声を再生
            audioSource = messageTarget.GetComponent<AudioSource>();
            audioSource.clip = impactClip;
            audioSource.Play();
        }
    }

    void OnTriggerExit(Collider col)
    {
        HeadLookController con;
        // 衝突対象が検索対象ならば実行
        if (col.tag == searchTarget)
        {
            //ルックコントローラを無効化
            con = messageTarget.GetComponent<HeadLookController>();
            con.enabled = false;
        }
    }
}

Unityでのデバック動作画面。セリフに合わせてユニティちゃんが口パクしてます。
f:id:bluebirdofoz:20170414003427j:plain
この通り、無事、口パクができました…が。
開発用PCのUnity上だとちゃんと動作するのに、hololensに搭載すると正常に動かない。口を閉じたままです。

OVRLipSyncはUWPアプリにはまだ対応していない?
口パクは一旦断念です。

ユニティちゃんがこっちを向く

MRの技術ではないですが、ユニティちゃんがこちらを見るようにします。
これでユニティちゃんがプレイヤーを認識しているように見え、より現実味が増すはずです。

参考にしたのはこちらです。
・こっち向いて機能の実装
 http://unimake2.blog.fc2.com/blog-entry-21.html

AssetStoreから『HeadLookController』をインポートします。

紹介ページにある通り、HeadLookController.csを少し改造してカメラの方を見るようにします。
設定時、カメラにアタリ判定(Collider)を付けるのを忘れないようにしましょう。

実行してみるとこんな感じ。
f:id:bluebirdofoz:20170413195414j:plain
横を向いているユニティちゃんに近づくと…
f:id:bluebirdofoz:20170413195431j:plain
こっちを見ました。HeadLookControllerの仕様上、瞳は動かないのが残念です。今後の課題とします。


今回は説明の通りに行わず、少しアレンジしてみました。
ユニティちゃん側で衝突判定を行うLookColider.csについて以下の実装コードとしました。

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

public class LookCollider : MonoBehaviour {
    AudioSource audioSource = null;

    public string searchTarget = "MainCamera";
    public GameObject messageTarget;

    public AudioClip impactClip = null;
    public float impactVolume = 0.5f;

	// Use this for initialization
	void Start () {
        // Add an AudioSource component and set up some defaults
        audioSource = gameObject.AddComponent<AudioSource>();
        audioSource.playOnAwake = false;
        audioSource.spatialize = true;
        audioSource.spatialBlend = 1.0f;
        audioSource.dopplerLevel = 0.0f;
        audioSource.rolloffMode = AudioRolloffMode.Logarithmic;
        audioSource.maxDistance = 5f;
        audioSource.volume = impactVolume;
	}
	
	void OnTriggerEnter(Collider col)
    {
        HeadLookController con;
        // 衝突対象が検索対象ならば実行
        if(col.tag == searchTarget)
        {
            //ルックコントローラを有効化
            con = messageTarget.GetComponent<HeadLookController>();
            con.enabled = true;
            //音声を再生
            audioSource.clip = impactClip;
            audioSource.Play();
        }
    }

    void OnTriggerExit(Collider col)
    {
        HeadLookController con;
        // 衝突対象が検索対象ならば実行
        if (col.tag == searchTarget)
        {
            //ルックコントローラを無効化
            con = messageTarget.GetComponent<HeadLookController>();
            con.enabled = false;
        }
    }
}

ユニティちゃんが離れたときはHeadLookControllerのスクリプト自体を無効化することで
視線の追跡を行わないようにしています。
HeadLookControllerの設定に変化が発生しないため、こちらの方が安全なはず?

後ついでに声も出るように…。やり方は前回勉強した立体音響の流用です。
・HOLOLENS 立体音響を再生する SPATIAL SOUND
 https://azure-recipe.kc-cloud.jp/2016/12/hololens-tutorial5/

変なところに拘りだしてしまう。楽しい。

ユニティちゃんが現実空間を歩く

さぁ今度は現実空間での動作確認です。
前回記事の続きとなります。参考ページは同じくこちらです。
【HoloLens開発】ユニティちゃんとHoloLensで戯れる - ゲームパッド編 -
 http://bril-tech.blogspot.jp/2016/11/hololens-gamepad.html


家にXbox Oneコントローラのゲームパッドがなかったので、キーボード入力に変更しました。
#if WINDOWS_UWPの部分を全て無効化してUnity上のテスト動作の操作をhololens上でも通るようにしただけです。
汎用性という意味ではこっちの方がよいかも。

起動してみます。ユニティちゃんが床に立つところまでは一緒です。
f:id:bluebirdofoz:20170413153711j:plain
試しにYキーを押してみると…
f:id:bluebirdofoz:20170413153720j:plain
休憩動作を行いました。他にも矢印キーを押すとそちらの方向へ歩き出します。
f:id:bluebirdofoz:20170413153730j:plain
因みに壁はきちんと認識されており、壁の向こうへは矢印キーを押しても行けません。

あっという間に自宅がアクションゲームのステージになりました。
段ボールの障害物を置けば、ユニティちゃんをジャンプさせて飛び越えさせたり、そのまま飛び乗らせたりできます。

少しの段差であれば、そのまま登れたりします。
f:id:bluebirdofoz:20170413153856j:plain
玄関の敷居くらいなら歩いて登れる。
f:id:bluebirdofoz:20170413153904j:plain
衝突判定が楕円形なのでおそらくそこに引っかからないものはそのまま登ることができる?要調査ですね。

しかし、自宅だと狭くて上手く遊べない。。どこかそれなりに広い場所を探す必要がありそうです。
だんだんとMRらしくなってきました。

ユニティちゃんがゲーム空間を歩く

さて今度はちょっぴりゲーム的要素を取り込んでみましょう。
今回はそれなりの作業量になります。

以下のページを参考に実施しますが、Unityに関してある程度の知識があることが前提の説明となっています。
【HoloLens開発】ユニティちゃんとHoloLensで戯れる - ゲームパッド編 -
 http://bril-tech.blogspot.jp/2016/11/hololens-gamepad.html

特にUnityにおけるアニメーション実装の知識が必要です。
問題を複雑化しないためにも、いきなり現実世界にユニティちゃんを歩き回らせるのは止めておき、
まずは仮想空間をユニティちゃんが歩き回るゲームを実装してみます。

よって、今回はMRは全く関係のない。ただのUnityゲーム開発となります。


最初に紹介ページと同じプロジェクト環境を作ります。
・【HoloLens開発】ユニティちゃんとHololensで戯れる - 表示編 -
 http://bril-tech.blogspot.jp/2016/09/hololenshololens.html

もし Holograms 101 のチュートリアルプロジェクトを作成済みであれば、AssetStoreからUnityChanのAssetをインポートしてprefabをセットするだけでもOKです。
準備できれば create内で右クリックを行い、3DObject->Planeから床を準備します。
f:id:bluebirdofoz:20170413134943j:plain
SpetialMappingの代わりにこの床の上で動作を確認しようという魂胆です。

ですので、XboxOneController.csに含まれる以下のSpatialMappingに関する処理は一旦コメントアウトします。
XboxOneController.cs

//manager = GameObject.Find("SpatialMapping").GetComponent<SpatialMappingManager>();

//中略

if (showMesh)
{
    // Wireframeを表示
    //manager.SetSurfaceMaterial(Wireframe);
}
else
{
    // Occlusionを表示
    //manager.SetSurfaceMaterial(Occlusion);
}

ユニティちゃんにコンポーネントとして追加して動きを見てみます。
f:id:bluebirdofoz:20170413135331j:plain
床の上を歩きました…特に何の修正も不要でした。
ユニティちゃんは歩く場所を意識しないので当然ですが、こうスムーズに行くとほっとします。

紹介ページに「アイドル状態でもジャンプできるように修正しています」とありますが、
おそらく下記の修正を実施していると思われます。
f:id:bluebirdofoz:20170413135455j:plain
Idle状態からJump状態にトランザクションを結ぶ。
このとき、Idle→Jumpの移行ではHasExitTimeをfalseにすること。
・【Unity】Animatorのモーション切り替えが即座に行われないときの対処
 http://tsubakit1.hateblo.jp/entry/2015/07/23/233000


最終的にユニティちゃんは現実世界を動き回るので、今のままだとジャンプ力がありすぎます。
ジャンプモーションを「JUMP00B」に変更して現実的なジャンプ力に変更しました。
f:id:bluebirdofoz:20170413135508j:plain
このとき、モーションだけでなく辺り判定も調整しておく必要があります。
0.5fを設定して再調整しています。
XboxOneController.cs

float adjCenterY = orgVectColCenter.y + (jumpHeight * 0.5f);

なお、JUMP00BモーションにはjumpHeight,GravityControlのカーブ設定がないので追加する必要があります。
この辺りは完全にユニティの知識です。自分は以下の動画で勉強しました。
・ゲームツクール! 第6回 Unity×Unity-Chanでアクションゲームをつくろう!
 https://www.youtube.com/watch?v=Qp53ua4eSQQ

何だかMR以外のところを拘り始めてしまったので、ひとまずここで打ち切ります。
次は現実世界を歩かせてみます。

hololensの立体音響

hololensには合計6つのスピーカが内蔵されており、立体音響が利用可能です。
仮想空間の右側からキャラクターが歩み寄れば、右側のスピーカから足音が聞こえるということができる訳です。
さて、今回はチュートリアルの続きです。
・HOLOLENS 立体音響を再生する SPATIAL SOUND
 https://azure-recipe.kc-cloud.jp/2016/12/hololens-tutorial5/


これまでと比べて、設定が少し複雑ですが、特に問題は発生しませんでした。

写真だと分かりませんが、折り紙のオブジェクトのある方から音楽が聞こえています。
f:id:bluebirdofoz:20170413013609j:plain
折り紙が左にあれば左から、右にあれば右のスピーカから音が鳴り、物体がどちらにあるか分かります。

因みにチュートリアルの中で追加する SphereSounds.cs はボールの落下音を追加するスクリプトです。
OnCollisionEnterはオブジェクト同士が衝突したときに呼ばれる関数とのことです。

// Occurs when this object starts colliding with another object
void OnCollisionEnter(Collision collision)
{
    // Play an impact sound if the sphere impacts strongly enough.
    if (collision.relativeVelocity.magnitude >= 0.1f)
    {
        audioSource.clip = impactClip;
        audioSource.Play();
    }
}

hololensのタップ操作で実装した、ボールの落下を発生させると、落下時の衝突音が再生されます。
なるほどなるほど。

しかし、気になった点としてスピーカから聞こえる音の音質はあまりよくなく感じました。
顔を振ると音楽にノイズが入る?
スピーカの問題ではなく、立体音響の解析処理が思った以上に重いのかもしれません。

ユニティちゃん大地に立つ

以前、紹介したユニティちゃんを表示するアプリは、指定の座標にユニティちゃんを表示しているだけです。
空間を認識して立っている訳ではないため、壁の前でアプリを起動すれば、ユニティちゃんは壁の中に表示されてしまいますし。
高いところで表示すれば、足は浮いたまんまです。
f:id:bluebirdofoz:20170412154501j:plain

これでは現実世界と仮想現実を複合するMRらしくありません。ポケモンGOなどで利用されるARをhololensで試しているだけです。
さて、今日のお遊び枠です。
・【HoloLens開発】ユニティちゃんとHoloLensで戯れる - 空間認識編 -
 http://bril-tech.blogspot.jp/2016/11/hololens-spatialmapping.html


利用を見送ったHoloToolkit-Unityを今回は利用します。
・HoloToolkit-Unity を使う
 http://www.naturalsoftware.jp/entry/2016/06/22/170925

紹介ページの通りですが、SpatialMappingというPrefabを利用することで、
作成した空間マッピングにオブジェクト(ここではユニティちゃん)を置けるようになります。


説明の通り実施してみましたが、今回は容易ではなく、2点の問題が発生しました。

まず最初に、HoloToolkit-Unityをプロジェクトに取り込んだところ、下記のエラーが発生しました。

・Assets\HoloToolkit\CrossPlatform\Scripts\Reflection\TypeUtils.cs 13行目
 'Type' does not contain a definition for 'GetTypeInfo' and no extension method 'GetTypeInfo' accepting a first argument of type 'Type' could be found (are you missing a using directive or an assembly reference?)

・Assets\HoloToolkit\CrossPlatform\Scripts\Reflection\ReflectionExtensions.cs 31行目
 'Type' does not contain a definition for 'GetBaseType' and no extension method 'GetBaseType' accepting a first argument of type 'Type' could be found (are you missing a using directive or an assembly reference?)

・Assets\HoloToolkit\CrossPlatform\Scripts\Reflection\ReflectionExtensions.cs 46行目
 'Type' does not contain a definition for 'GetBaseType' and no extension method 'GetBaseType' accepting a first argument of type 'Type' could be found (are you missing a using directive or an assembly reference?)

当該コードを以下の通り、修正することで対応
・TypeUtils.cs

//return type.GetTypeInfo().BaseType;
return type;

・ReflectionExtensions.cs

//var baseType = type.GetBaseType();
var baseType = type;

正直なところ、自身の環境では何故このエラーが発生したのか。
また、この修正方法で本当に問題ないのか。きちんと理解できていません。

・Type.BaseType in Portable Class Library
 http://stackoverflow.com/questions/25351860/type-basetype-in-portable-class-library
・A lot of reflection functionality like Type.BaseType is not available anymore in dnxcore50
 https://github.com/Microsoft/dotnet-apiport/issues/268

この辺りの情報を見るに、WinRTのAPIが変わった?
今回は深く追いませんが、何かあった時の備忘録として書き残しておきます。


さてビルドは成功して、アプリはきちんと動作しました。
しかし、今度はユニティちゃんが床を通り抜けて奈落の底へ落ちていく事象が発生しました。

これの原因は一目瞭然でした。
SpatialMappingの作成には十数秒ほど時間がかかります。
このため、床が作成される前にユニティちゃんが重力に引っ張られて床を通り抜けていってしまうのです。

折角なので、前回作成した機能を流用して対応します。
理屈は簡単、タップ操作を行って初めて、ユニティちゃんに重力属性が与えられるよう修正しました。

利用したのは前回のGazeGastureManager.csと、新たにUnityCommands.csという以下のスクリプトを自作しました。

using UnityEngine;

public class UnityCommands : MonoBehaviour {
    private Rigidbody rigidbody;                      // Rigidbodyへの参照

    // Use this for initialization
    void Start ()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    // タップ操作時に呼ばれるOnSelect関数
    void OnSelect () {
        // RigidbodyのuseGravity変数をtrueにして重力を発生させる
        rigidbody.useGravity = true;
    }
}

これらのスクリプトを以下の通り、ユニティちゃんのオブジェクトに設定します。
また、勝手に落ちないよう、重力のデフォルト値は無効にしておきます。
f:id:bluebirdofoz:20170412154730j:plain


さて、アプリを起動してみます。起動直後はこれまで通り、ユニティちゃんが宙に固定されています。
f:id:bluebirdofoz:20170412154800j:plain
空間マッピングで床が生成されたのを確認したら、タップ操作を実行すると…
f:id:bluebirdofoz:20170412154843j:plain
この通り、ユニティちゃんが少し落下してそのまま床の上に立ちました。成功です
f:id:bluebirdofoz:20170412154856j:plain


今回のプロジェクトで、仮想空間のオブジェクトが現実世界を認識して動きました。
ようやくMRらしいアプリが実装できたと言えます。


因みに今回のプロジェクトの副産物として現実世界の障害物や奥行きを認識できるようになるという効果があります。
(床が認識できているので当然と言えば当然ですが)

例えば、先ほどのユニティちゃんの手前に段ボールを置いてみると…
f:id:bluebirdofoz:20170412155103j:plain
障害物を認識し、3Dモデルが障害物の奥に隠れる形で描写されるようになっています。