GAGEX BLOG

GAGEXの業務内容や取り組みを公式ブログで情報発信中!

イベント展示におすすめ! 裸眼で3D映像を楽しめる「Looking Glass」

イベント展示におすすめ! 裸眼で3D映像を楽しめる「Looking Glass」

こんにちは、エンジニアのかわかみひろきです。
インディーゲームイベント「Bitsummit 7 spirits」で展示した 「忘れないで、おとなになっても。」の Looking Glass 対応デモアプリについて紹介します。この記事の後半にサンプルコードを掲載しますので、よかったら参考にしてください! 開発は弊社インターンの中島さんと一緒に行いました。

当日はこちらの映像のように展示していました!
Looking Glassでアプリ映像を映し、Leap Motion(※)を使って手のジェスチャーに合わせてカメラ操作を行えるよう実装しています。

※手のジェスチャーでコンピュータの操作ができる入力デバイス。マウス動作や画面タッチしないで直感的に操作ができるのが特徴

Looking Glassとは


Looking Glassは、ホログラムディスプレイです。
およそ45度分の視点を表示可能なディスプレイで、上図のようにディスプレイを見る角度に応じて、裸眼で映像に角度が付いて見えます。

視野角数分のレンダリングを行い、その映像をレンチキュラーレンズを通して目の入射角度先にある映像が見える、という仕組みです。「レンチキュラーレンズ」でググると分かりやすい解説ページがヒットしますので、より詳しく知りたい方はそちらで確認ください。

購入はこちらの公式サイトから行えます。8.9インチディスプレイが$599、送料が$80、+関税となります(2019/6/14現在)。届くまで1ヶ月前後かかる事にご注意ください。

既存アプリへのLoogking Glass対応

Unity製であればLooking Glassへ映すだけなら簡単で、公式HPからHoloPlay SDKをプロジェクトへimportして、Main CameraをHoloPlay SDKに同梱されているHoloplay Captureに差し替えるだけでOKです。

Looking Glassへ映す上で重要になるのが「近距離と遠距離をどの程度見せるか」になります。
具体的な設定値はHoloplay ScriptのNear Clip FactorFar Clip Factorになります。Looking Glassの弱点は奥行きレンダリングで、奥行きのレンダリング幅が広ければ広いほど映像は荒くなります。
そのため先程のClip Factorで幅の調整を行い、カメラ角度を奥にあるモデルが表示されなくても違和感無いよう調整します。カメラを固定する場合だと、奥にある背景モデルは1枚絵にしてビルボードへ貼っておくという手もあります。

また、Unity製のPost Processing StackのBloom等を使用すると解像度が大幅に落ちてしまいます。そのためゲーム本編で使用していたエフェクトは切りました。

Leep Motionを使ったジェスチャー対応

展示用のデモアプリでは、Looking Glassと相性の良いLeep Motionを入力デバイスとして用意しました。最新のLeap Motion SDKだとmacOSでは初期化時にエラーとなってしまったので v2.3.0をimportしました。

今回は以下の4つのジェスチャーを対応させる事にしました。

  • 手を回転:カメラがY軸回転する
  • 手を上げ下げ:カメラがズームイン/アウトする
  • 両手をかざす:カメラをリセットする
  • 両手をかざして横に移動:表示マップを切り替える

それぞれの対応したコードを抜粋してご紹介します。長いので実装雰囲気を感じ取って頂くだけで良いかと思います。(本来のコードではなく可能な限り行数を抑えるよう調整しています)

private Holoplay _holoplay;     // Holoplay Captureにアタッチされている設定Script
private Controller _controller; // Leap Motionのコントローラー
private bool _isTracking = false;
private float _startYaw, _beforeYaw, _startPosY, _beforePosY, _beforePosX;

void Awake() {
    _holoplay = GetComponent<Holoplay>();
    _controller = new Controller();
}

void Update() {
    var hands = _controller.Frame().Hands;
    if (!_isTracking && hands.Count > 0) {
        // ジェスチャー受付を開始
        _isTracking = true;

        _startYaw = _beforeYaw = hands[0].Direction.Yaw;
        _startPosY = _beforePosY = hands[0].PalmPosition.y;
        _beforePosX = hands[0].PalmPosition.x;
    } else if (_isTracking && hands.Count == 0) {
        // ジェスチャー受付を終了
        _isTracking = false;
    }
    if (!_isTracking || hands.Count == 0) return;

    var isGesture = false;
    if (hands.Count >= 2) {
        // 2本の手をかざす操作
        // 横に移動したら画面遷移、しなければカメラリセット
        isGesture = DisplayNextScene(hands[0]);
        if (!isGesture) {
            // カメラをリセット
            // TODO:ここにリセット内容を書く
        }
    } else {
        // 1本の手をかざす操作
        // 手が上下移動したか
        isGesture = GestureHeight(hands[0]);

        if (!isGesture) {
            // 手を回転させたか
            isGesture = GestureRotate(hands[0]);
        }
    }
}

// 次のシーンへ遷移するジェスチャー
private bool DisplayNextScene(Hand currentHand) {
    bool isGesture = false;
    var currentPosX = currentHand.PalmPosition.x;
    if (currentPosX - _beforePosX > 60.0f) {
        // TODO:画面遷移
        isGesture = true;
    } else if (_beforePosX - currentPosX > 60.0f) {
        // TODO:画面遷移
        isGesture = true;
    }
    _beforePosX = currentPosX;

    return isGesture;
}

// 手を挙げるジェスチャーの検知
private bool GestureHeight(Hand currentHand) {
    bool isGesture = false;

    var currentPosY = currentHand.PalmPosition.y;
    if (_startPosY + 60.0f < currentPosY) {
        if (_beforePosY + 0.01f < currentPosY) {
            // TODO:ズームアウトする

            _beforePosY = currentPosY;
            isGesture = true;
        }
    }
    else if (_startPosY - 60.0f > currentPosY) {
        if (_beforePosY - 0.01f > currentPosY) {
            // TODO:ズームインする

            _beforePosY = currentPosY;
            isGesture = true;
        }
    }

    return isGesture;
}

// 回転ジェスチャーの検知
private bool GestureRotate(Hand currentHand) {
    bool isGesture = false;

    var currentYaw = currentHand.Direction.Yaw;
    if (_startYaw + 0.6f < currentYaw) {
        // 時計回り処理
        if (_beforeYaw + 0.01f < currentYaw) {
            // TODO:時計回りにカメラを回転

            _beforeYaw = currentYaw;
            isGesture = true;
        }
    } else if (_startYaw - 0.6f > currentYaw) {
        // 反時計回り処理
        if (_beforeYaw - 0.01f > currentYaw) {
            // TODO:反時計回りにカメラを回転

            _beforeYaw = currentYaw;
            isGesture = true;
        }
    } else {
        // 初期位置に手を戻したらbefore値はリセットしておく
        _beforeYaw = _startYaw;
    }

    return isGesture;
}

イベント会場での反響

「存在自体知らない」 or 「名前は知っているけど実物は初めて見た」と仰られる方がほとんどで、新鮮さのお蔭もあり好評でした。

ディスプレイを見ながら通路を歩いた時に立体的に見え、そこでブースに近付いて見て頂ける。イベントにおいて非常に求心力のあるディスプレイだと実感できました。

総括

デバイスコストは掛かりますが、実装自体はそれほどコストが掛かりません。
こういったイベントには打って付けのディスプレイだと思いますので、イベント出展の際にはこの記事を思い出して検討して頂ければと思います。

それと、もし弊社に少しでも興味を持って頂けた方は是非採用ページからご応募ください!

GAGEXの求人情報

詳しくはコチラ

kawakami

かわかみひろき

エンジニアです