VR対応の昆虫体験シミュレータをUnityで制作

  • 4
    いいね
  • 0
    コメント

概要

VR対応の昆虫の動きを体験できるシミュレータをUnityを使用して制作をしました。
C#でスクリプトを書き、物理シミュレーションで昆虫に力を加えて動かしました。

kaken_2.jpg
ここで体験する昆虫は人間ができない動作をするものの方がシミュレータとして面白いと思ったので、人間より小さく、人間ができない動作である飛翔が可能な昆虫として蝶を選択しました。

VRに対応

Oculus Riftを使うためにパソコンにOculus Utilitiesをインストールしたり、アカウントを作ったりしました。ここでパソコンがOculusを認識できるようになります。
UnityでPlayer SettingsのOther Settingsにある「Virtual Reality Supported」にチェックを入れればOculus Riftのレンズに自分が作成した3D空間が映ります。

完成品

昆虫体験シミュレータ
実行ファイルとBX_d_Dataの両方をダウンロードして実行してください。
BX_d_DataはGoogledriveでダウンロードすると圧縮されているので解凍してください。

操作方法:

上昇 下降 前進 後退 左回転 右回転 オプション
Space c up arrow down arrow left arrow right arrow m
w s a d

実際に動かした時の映像

source.gif
右上に小さく映っている映像が実際に動かしたときに体験する視点で、それ以外は昆虫がどの様な動作をしているか三人称視点で見た映像です。

source.gif
このようにスライダーでMoveSpeed、TimeSpeed、FlyingPowerの値を設定してDecisiveをクリックします。

source.gif
Decisiveをクリックするとフィールドに移動し、好きなように動かせます。

source.gif
先ほどよりもMoveSpeedに高い値を設定すると速く移動します。

source.gif
同様にFlyingPowerに高い値を設定すると速く上昇、下降します。

source.gif
全部の値を最高値にするとかなりの速さで移動します。
VRの場合確実に酔うと思います。

オプション画面の使い方:

フィールドの画面の時にキーボードのmキーを押すとオプション画面に移動します。

kaken_6.jpg

MoveSpeed TimeSpeed FlyingPower Decisive
蝶の移動速度 移動 + 上昇と下降の速度 上昇と下降の速度 値の適用

マウスでそれぞれのボタンを押してスライダーにより蝶の移動や上昇と下降の速度を自由に変更できます。Decisiveを押すことで設定を適用し、シミュレーション画面へ移動します。

使用環境

使用した環境
Unity(version 5.3.2)
Blender
Oculus(ヘッドマウントディスプレイ)
VisualStudio(C#)
3Dモデル(昆虫,背景) 

蝶や背景の森の3DCGはAseetStoreで配布されているフリー素材を使用し、スクリプトはC#で書き、Oculus(DK1)でVRで体験できるようにしました。

オブジェクト(蝶)を動かすのに使用したスクリプト(C#)

B_d_ac3.cs
using UnityEngine;
using UnityEngine.SceneManagement; //SceneManager.LoadSceneの使用に必要
using System.Collections;

public class B_d_ac3 : MonoBehaviour
{

    private float speed = 0.4f;     //移動速度を格納するための変数
    private float fly = 0.4f;     //上昇と下降速度を格納するための変数
    private float f_t = 1;       //移動+上昇下降速度を格納するための変数
    private float torque = 0.2f;   //回転速度を格納するための変数
    private int[] i = new int[2];  //進行許可をするための判定に使用

    private float x = 0; //オブジェクトの座標のx軸に関する情報を格納
    private float y = 0; //同様にy軸
    private float z = 0; //同様にz軸

    public float y_t = 0;

    private GameObject d; //オブジェクトの情報を格納
    private Rigidbody rb; //リキッドボディの情報を格納
    public Vector3 btf;   //オブジェクトの座標の情報を格納するための変数

    void Start()
    {
        d = GameObject.Find("Butterfly"); //使用するオブジェクトを探索
        rb = d.GetComponent<Rigidbody>(); //rbにリキッドボディを格納
  
      fly = f_save.f_sd(); //オプション画面で設定した上昇と下降速度を格納
        if (fly == null || fly == 0) fly = 0.4f; //値を設定していなかった場合デフォルトの値を格納
        speed = f_save.f_sd(); //オプション画面で設定した移動速度を格納
        if (speed == null || speed == 0) speed = 0.4f; //値を設定していなかった場合デフォルトの値を格納
        f_t = t_save.t_sd();          //オプション画面で設定した移動+上昇下降速度を格納
        if (f_t != null && f_t != 0) //値を設定していた場合設定した値を適用
        {
             speed = speed * f_t;
             fly = fly * f_t;
         }

        rb.maxAngularVelocity = 2; //回転速度の上限を設定
        i[0] = 0; //値の初期化
        i[1] = 0; //値の初期化
    }


    void Update()
    {
        float y_k = 0;
        y_t = rb.transform.localEulerAngles.y * Mathf.Deg2Rad; //オブジェクトのラジアン角度取得
        if (y_t >= 0 && y_t <= 90) //オブジェクトの角度が0から90の場合
        {
            btf.x = Mathf.Cos(y_t);      //btfのxに角度のコサインを格納
            btf.y = -1 * Mathf.Sin(y_t); //btfのyに角度のサインを格納

        }
        else if (y_t > 90 && y_t <= 180) //オブジェクトの角度が90より上から180の場合
        {
            y_k = y_t - 90;
            btf.x = -1 * Mathf.Sin(y_t);
            btf.y = Mathf.Cos(y_t);
        }
        else if (y_t > 180 && y_t < 270) //オブジェクトの角度が180より上から270の場合
        {
            y_k = y_t - 180;
            btf.x = Mathf.Cos(y_t);
            btf.y = -1 * Mathf.Sin(y_t);
        }
        else if (y_t >= 270 && y_t <= 360) //オブジェクトの角度が270より上から360の場合
        {
            y_k = y_t - 270;
            btf.x = -1 * Mathf.Sin(y_t);
            btf.y = Mathf.Cos(y_t);
        }
        if (i[0] == 1 && i[1] == 1)
        { //キーが一度押されていない状態になりかつ移動ボタンが押された場合
            i[0] = 0;
            bot_imp(); //回転の停止
        }
    }

    void FixedUpdate() //物理演算でキャラクターが動く度に呼ばれる処理を設定
    {

        x = Input.GetAxis("Vertical");
        y = Input.GetAxis("Horizontal");
        z = Input.GetAxis("Jump"); //上昇 スペース 下降 c
        if (Input.anyKey)
        {
            rb.AddTorque(0, y * torque, 0, ForceMode.Impulse); //オブジェクトの回転
            rb.AddForce(0, z * fly, 0, ForceMode.Impulse);    //オブジェクトの上昇と下降
            i[1] = 1;//オブジェクトが現在移動中と判定

            if (btf.x > 1.0 || btf.x < -1.0) btf.x = 0; //角度情報が正規の値でないなら0
            if (btf.y > 1.0 || btf.y < -1.0) btf.y = 0; //角度情報が正規の値でないなら0
            rb.AddForce(btf.x * x * speed, 0, btf.y * x * speed, ForceMode.Impulse); //オブジェクトの移動
        }
        if (Input.GetKey(KeyCode.M))
        { //Mキーを押した場合
            SceneManager.LoadScene("キーコンフィグ"); //別のシーンに移動
        }
        if (Input.GetKey(KeyCode.N))
        { //Nキーを押した場合
            SceneManager.LoadScene("example_1");
        }

        else
        {
            i[0] = 1; //何のキーも押されていないことを判定
        }

    }
    void bot_imp()
    {
        rb.angularVelocity = Vector3.zero; //回転運動の停止
        i[1] = 0;
    }
}

シミュレータの仕様

物理エンジンやスクリプトを用いて重力や慣性など現実に近い環境を再現しました。
背景は現実に蝶が存在するであろう森を背景に設定しました。
昆虫視点にするために生物の視点をメインカメラに設定して昆虫の動き自由に操作できるようにしました。
さらに昆虫の移動速度や時間の流れの速さなどをスクリプトで設定し、体験者が任意に操作できるようにしました。

動きが重くなってしまう場合

森の背景はパソコンのグラフィックボードに負荷をかけるので動きがカクカクしてしまう場合があります。
その場合フィールドの画面でnキーを押すと背景を森から簡素で比較的に負荷が軽い背景に変わるように設定してるのでそちらのフィールドを使用してください。

苦労した点

蝶のモーションをBlenderで作成

蝶を動かすなら羽ばたくモーションも必要だと思いAseetStoreで無料のモーションを探しましたが見つかりませんでした。
「VRなら一人称視点だしモーションいらないんじゃ?」という事実にこの時の自分は気づきませんでした。
そこで自分がインポートした蝶の3DモデルのAseetにモーションが付属されていたのを発見しました。
しかしこれを使用しても決まった位置で羽ばたいているだけで自分の思い通りに動かせませんでした。
ここでBlenderでAseetを読み込んで羽のモーションの座標と角度をフレームごとに抜き出しました。
この時フリーの変換ツールを使ってファイルをBlenderが読み込める形式に変換しました。

左羽:

座標 角度
X:2.18 Y:-5.15 Z:10.76 X:36.05 Y:54.46 Z:120.6
X:1.45545 Y:-5.84428 Z:7.21637 X:24.035d Y:22.979d Z:110.428d
X:0.72773 Y:-6.53259 Z:3.66679 X:12.017d Y:-8.51d Z:100.214d
X:0 Y:-7.2209 Z:0.11722 X:0 Y:-40d Z:90d
X:0.72773 Y:-6.53259 Z:3.66679 X:12.017d Y:-8.51d Z:100.214d
X:1.45545 Y:-5.84428 Z:7.21637 X:24.035d Y:22.979d Z:110.428d
X:2.18318 Y:-5.15597 Z:10.76595 X:36.052d Y:54.469d Z:120.642d

右羽:

座標 角度
X:1.73458 Y:4.8521 Z:11.5485 X:32.376d Y:-61.095d Z:60.968d 
X:1.15639 Y:5.6453 Z:7.76533 X:21.584d Y:-27.397d Z:70.645d 
X:0.57819 Y:6.4385 Z:3.98216 X:10.792d Y:6.302d Z:80.323d 
X:0 Y:7.2317 Z:0.19899 X:0 Y:40d Z:90d 
X:0.57819 Y:6.4385 Z:3.98216 X:10.792d Y:6.302d Z:80.323d 
X:1.15639 Y:5.6453 Z:7.76533 X:21.584d Y:-27.397d Z:70.645d 
X:1.73458 Y:4.8521 Z:11.5485 X:32.376d Y:-61.095d Z:60.968d 

自分のスクリプトで羽のパーツの座標と角度をキー入力が検知される毎に表でまとめた値をループするようにしました。

left_wing.cs
using UnityEngine;
using System.Collections;

public class left_wing_action : MonoBehaviour {

    public int x = 0;
    private Vector3[] p,r;

    void Start()
    {
        p = new Vector3[] {new Vector3(2.18, -5, 10.76),new Vector3(1.45, -5.84, 7.21),new Vector3(0.72, -6.53, 3.66),new Vector3(0, -7.22, 0.11),};
        r = new Vector3[] {new Vector3(36.05, 54.46, 120.6),new Vector3(24.03, 22.97, 110.4),new Vector3(12.01, -8.51, 100.2),new Vector3(0, -40, 90),};
    }

    void Update()
    {

        if (Input.GetKey(KeyCode.A)){
            if(x < 3){
                x++;
            }
            else{
                x = 0;
            }
        }
        if (Input.GetKey(KeyCode.S)){
            if (x < 3){
                x++;
            }
            else{
                x = 0;
            }
        }
        if (Input.GetKey(KeyCode.RightArrow)){
            if (x < 3){
                x++;
            }
            else{
                x = 0;
            }
        }
        if (Input.GetKey(KeyCode.LeftArrow)){
            if (x < 3){
                x++;
            }
            else{
                x = 0;
            }
        }
        if (Input.GetKey(KeyCode.UpArrow)){
            if (x < 3){
                x++;
            }
            else{
                x = 0;
            }
        }
        if (Input.GetKey(KeyCode.DownArrow)){
            if (x < 3){
                x++;
            }
            else{
                x = 0;
            }
        }
        transform.Translate(p[x], Space.Self);
        transform.Rotate(r[x],Space.Self);
    }
}

実行結果

source.gif
左羽にスクリプトを適用した瞬間にものすごい速さで空中分解して飛んでいきました。失敗です。

地面に対するあたり判定

蝶のオブジェクトにRigidbodyを適用し、RigidbodyのUse Gravityにチェックを入れて蝶の3Dモデルに重力を設定しました。
ここで蝶のオブジェクトと地面のオブジェクトにColliderを設定し、[Inspector]タブでRigidbodyのCollision DetectionContinuosに変えます。
この処理をしないと速い速度での衝突検知の処理が間に合わなくなりすり抜け現象が発生してしまいます。

座標指定による移動

B_d_ac2.cの前に書いたtransform.Translateやtransform.Rotateを使用した座標指定で移動するスクリプトです。

B_d_ac.cs
using UnityEngine;
using System.Collections;

public class B_d_action : MonoBehaviour {

    public float x = 0.0f;
    public float y = 0.0f;
    public float z = 0.0f;

    public float speed = 0.1f;
    public float torque = 0.1f;

    public GameObject d;
    public Rigidbody rb;

    void Start()
    {
        d = GameObject.Find("Butterfly");
        rb = d.GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (Input.anyKey)
        {

            if (Input.GetKey(KeyCode.A))
            {
                z = 0.0f;
                x = speed;
            }
            if (Input.GetKey(KeyCode.S))
            {
                z = 0.0f;
                x = -1 * speed;
            }

            transform.Translate(x, 0, 0, Space.Self);

            if (Input.GetKey(KeyCode.RightArrow))
            {
                z = 0.0f;
                y = torque;
            }
            if (Input.GetKey(KeyCode.LeftArrow))
            {
                z = 0.0f;
                y = -1 * torque;
            }

            transform.Rotate(0, 0, y, Space.Self);

            if (Input.GetKey(KeyCode.UpArrow))
            {
                y = 0.0f;
                z = speed;
            }
            if (Input.GetKey(KeyCode.DownArrow))
            {
                y = 0.0f;
                z = -1 * speed;
            }
            if (Input.GetKey(KeyCode.D))
            {
                x = 0.0f;
                y = 0.0f;
                z = 0.0f;
            }

            transform.Translate(0, 0, z, Space.Self);
        } else {
            x = 0.0f;
            y = 0.0f;
            z = 0.0f;
        } 
    }
}

今見ると変数が全部publicだったりInputManagerを使用してない等の色々な無駄が見受けられますが、今回特に問題なのはtransformによる移動だと移動したときに地面のColliderの衝突判定をすり抜けて移動してしまうことです。
さらにtransformによる移動はオブジェクトの質量を考慮しない移動になるのでシミュレーションとして不適切だと判断しAddForce による移動方法にしました。

力積による移動

AddForceのForceMode.Impulseは質量を考慮した計算を行って物体に力を加えるのでシミュレータとしてこちらを使用した方が現実に近い環境を再現できると思い、他にも衝突判定のすり抜けが起きないという利点もあったためオブジェクトの移動にAddForceを使用しました。

角度を変えても進行方向が変わらない:

しかしここで角度を変えても進行方向が変わらないという問題が発生しました。
自分は現在オブジェクトが向いている方向に進行するスクリプトを欲していたのでこのままでは失敗です。

source.gif
前進、後退の入力をしているのに蟹歩きみたいになってます。

そこで解決策として現在オブジェクトが向いている角度の情報を取得し、コサイン成分とサイン成分に分割してAddForceで力を加えることにしました。三角関数を習っててよかったと思いました。

まとめるとこんな感じです。
zu_4.jpg

オプション画面

オプション画面は自分が以前制作したゲームのキーコンフィグを流用して作りました。
こうして一度作った物を使い回しできるのもUnityのいいところだと思います。

まとめ

VR対応のアプリケーションを作るのはとても大変だしお金がかかりそうだと思っていましたが、Unityを使えば簡単なものなら無料で作ることができました。 Oculus(HMD)を買わないとデバッグできないけど
次に作る時は風や雨などの自然現象を実装してよりリアリティーあふれるシミュレータを作りたいと思います。