これちのPost-it

技術ネタをペラペラと

Unity で地図が表示可能な mapbox を触ってみた(初級編)

はじめに

XR+位置情報なゲームを作りたいなーと思い Unity 向けにパッケージが公開されてる mapbox を触ってみました。

公式ドキュメントはこちら

docs.mapbox.com

今回は mapbox SDK を使って地図をUnity 上で出してみることを実現したいと思います。

環境

Maps SDK for Unity v2.0.0

Unity 2019.1.2f1

使い方

Unity でのセットアップ方法は↑に書いてあるため割愛。

基本的には unitypackage をインポートし Mapbox->Prefabs->Map プレハブを hierarchy 上に配置後 Play すれば地図が表示されます。

f:id:korechi:20190521101650p:plain

この Map プレハブについてる Abstract Map(Script) から地図のいろんなパラメータの調整や、レイヤの追加等が可能です。

ただし↑の公式ドキュメントは SDK バージョンが1.4.0対応っぽく少し古いため、一部 API 仕様が微妙に違うので注意。(2019/5/21現在)

Scene の中心に9マスに分割された地図が表示されています。しかしこのままでは画面をドラッグしてカメラ視点を変えたり、緯度経度を自由に指定する等できません。

SDKMapbox->Examples にあるサンプルを参考にいろんな操作ができることを確かめてみてください。

では実際にいろいろ触ってみようと思います。

その前に

Unity 2019 で SDK をインポートしたら下記のエラーをはいたので、

Library/PackageCache/com.unity.xr.arfoundation@1.0.0-preview.22/Runtime/AR/ARSessionOrigin.cs(3,19): error CS0234: The type or namespace name 'SpatialTracking' does not exist in the namespace 'UnityEngine' (are you missing an assembly reference?)

https://github.com/Unity-Technologies/arfoundation-samples/issues/79#issuecomment-450263059

どうもここが参考になりそうとのことで

"com.unity.xr.legacyinputhelpers": "1.0.0"

の設定を manifest.json の一番下に追記することで暫定対応。

理由を詳しく追えていませんが、どうも AR Foundation(ARKit/ARCore のマルチプラットフォーム AR 環境)関連の問題のようなので、Unity のアップデートでいずれ対応されると思います。

AR Foundation についてはこちらが参考になりました。

tsubakit1.hateblo.jp

それでは気を取り直して

特定の緯度経度にプレハブを配置

大崎駅付近 (35.619707, 139.7283495) に適当なプレハブを表示したいと思います。

f:id:korechi:20190521170006p:plain:h400

  1. GENERAL タブ内の Location を 35.619707, 139.7283495
  2. MAP LAYERSData SourceMapbox Streets With Building Ids
  3. POINTS OF INTEREST 内の Add Layer をクリックしてレイヤーを追加
  4. 追加されたレイヤを選択して Prefab を適当なものに設定
  5. Prefab LocationsFind byAddress Or Lat Lon にして Add Location をクリック
  6. Location 035.619707, 139.7283495 を指定
  7. 実行

f:id:korechi:20190521170603p:plain

大崎駅にピンをたたせることができました!

これを応用すればポケストップを表示する、なんてこともできそうですね。

建物を立体的に

3D地図っぽい感じに建物を立体的にのばしてみたいと思います。

f:id:korechi:20190521171402p:plain:h200

  1. MAP LAYERSData SourceMapbox Streets With Building Ids に(さっきのまま)
  2. FEATURESAdd Feature をクリックして Map Features が追加
  3. Data Layerbuilding
  4. 実行

f:id:korechi:20190521171457p:plain

デフォルトの設定でここまで表示できるのは良いですね。

建物の外観は TexturingStyle Type から変更可能です。例えば Style TypeColor にすると

f:id:korechi:20190521171811p:plain:h200

f:id:korechi:20190521171841p:plain

こんな風に変えることができます。

Mapbox の簡単な使い方はおさらいできたので、今回はここまで。次はもう少し詳しくドキュメントを読んで遊んでみたいと思います。

ProjectNorthStar を 6DoF 化してみる

もう他の記事でも紹介されているかもしれませんが自分のメモも兼ねてまとめておきます。

前回の記事で ProjectNorthStar を組み立てました。

korechipostit.hatenablog.com

ただし ProjectNorthStar で使われる LeapMotion は手の認識のみを行うため AR グラスを作ったからといって 6DoF 機能はありません。

つまり ProjectNorthStar だけでは AR のオブジェクトを現実空間に定位させることができないということですね。

でもそれだとできることに制限がありますし HoloLens みたいに空中に AR オブジェクトを固定したいなと。そこで SLAM が可能な RealSense T265 を組み合わせることで今回 ProjectNorthStar を 6DoF 化させてみました!

見た目はこんな感じ。上に乗っかっているのは RealSense。

f:id:korechi:20190310235358j:plain:h400

作ったデモ

費用

  • ProjectNorthStar
    • 23,176円
      • 内訳:9396(LeapMortion)+1480(ヘッドギア)+1080(ネジ)+2980*2(ディスプレイ)+5046(リフレクター)+547(HDMIケーブル)
  • realsense
    • 25,035円

これを安いと見るか高いと見るかは人それぞれですが、自分はこれで SLAM 可能な AR グラスが自作できるのなら安いかなと思います。

それでは実際に Unity での実装手順を紹介します。

ただしその前に Intel RealSense SDK を PC にインストールしておきましょう。現状 Mac 等の OS には対応していないため Windows マシンにインストールします。(LeapMotion の Orion SDKWindows のみサポートなのでどのみち Windows を使います)

software.intel.com

SDK をインストールすると RealSense 用 Viewer が使えるようになるため、まずはそれで RealSense の自己位置がとれているか確認してみましょう。

ちょっと触ってみましたが、これだけでもすごい!

実装手順

とりあえず詳しい説明は省き、とりあえず 6DoF 化させたい!という人のために、必要最小限の工数で 6DoF 化するための手順を紹介します。

  1. realsense.unitypackage を Unity へインポート github.com
  2. LeapAR.unitypackage を Unity へインポート github.com
  3. LeapAR のシーンをベースに作業を進めるため LeapMotion/Norrth Star/Scenes/NorthStar.scene シーンを開き、別名として保存(test.scene とでもしておきます)
  4. 次に realsense の自己位置を取得するため RealSenseSDK2.0/Scenes/Samples/SLAM.scene シーンを開き hierarchy にある RsDevicePose を Prefab 化 f:id:korechi:20190311002049p:plain
  5. 先ほど保存した test.scene を開き Prefab 化した RsDevicePose を hierarchy に配置
  6. ここの Pose オブジェクトの position と rotation は現実での LeapMotion と RealSense 間の相対位置に合うように調整
  7. 相対位置が良い感じになったら ARCameraRig を Pose オブジェクトの子オブジェクトに
  8. Pose オブジェクトの Inspector 内の RsPoseStreamTransform の Source に RsDevice オブジェクトをアタッチ
  9. 最後に適当なオブジェクト(今回は Cube)を目の前に表示されるように配置して実行 f:id:korechi:20190311002625p:plain

視界の目の間に Cube が表示され、移動すると Cube の見え方が変わるのが確認できるはずです!

Tips

RealSense は裏にねじ穴があるので、比較的折れ曲がりにくい紙を挟んで固定し、下をちょっとはみ出させればうまいこと LeapMotion の裏にさしこむことができます。(ネジは3M*5Bを使用)

f:id:korechi:20190312200147j:plain:h400

f:id:korechi:20190312200335j:plain:h400

ProjectNorthStar を組み立ててみた

公開されている 3D モデルを印刷し、いくつかの部品を揃えれば自作できる AR グラスこと、ProjectNorthStar を作ってみました!

github.com

完成させるために21000円程度かかりました。これで自作 AR グラスが作れると思えば意外と高くないかも?

完成品 f:id:korechi:20190307235346j:plain

手を認識するデモ

youtu.be

参考にした記事

tech.showroom.co.jp

exiii.jp

psychic-vr-lab.com

作り方はこれらの記事にわかりやすく書かれているため割愛します。

Tips

外部ディスプレイの設定は忘れがち&情報が少なかったのでメモ。

ProjectNorthStar に向かって左が第2ディスプレイ(縦)で右が第3ディスプレイ(縦の反転)です。

f:id:korechi:20190307235744j:plain

また、ProjectNorthStar をかけると前方部が垂れ下がってしまったため紐で縛りました。

f:id:korechi:20190308000348j:plain:h400

コード類もまず左右でまとめて、最後に後ろで縛ると取り扱いがしやすくなります。

f:id:korechi:20190308000724j:plain:h400

また、↓を買うと HDMI が固定されやすくなりおすすめです。

https://www.amazon.co.jp/gp/product/B0747675WN/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1

SharingWithUNET サンプルでクライアント側のみ弾が前へ進まない問題

HoloToolkit-Unity-Example にある SharingWithUNET サンプルを実機で動作させたらクライアント側でのみ弾が前に進まなず空中で静止するバグ?を発見(サーバ側では弾がちゃんと前に進む)

その原因となる問題のコードがこちら

  • MixedRealityToolkit-Unity/Assets/HoloToolkit-Examples/SharingWithUNET/Scripts/PlayerController.cs github.com
        [Command]
        void CmdFire()
        {
            Vector3 bulletDir = transform.forward;
            Vector3 bulletPos = transform.position + bulletDir * 1.5f;

            // The bullet needs to be transformed relative to the shared anchor.
            GameObject nextBullet = (GameObject)Instantiate(bullet, sharedWorldAnchorTransform.InverseTransformPoint(bulletPos), Quaternion.Euler(bulletDir));
            nextBullet.GetComponentInChildren<Rigidbody>().velocity = bulletDir * 1.0f;
            NetworkServer.Spawn(nextBullet);

            // Clean up the bullet in 8 seconds.
            Destroy(nextBullet, 8.0f);
        }

        public void OnInputClicked(InputClickedEventData eventData)
        {
            if (isLocalPlayer)
            {
                CmdFire();
            }
        }

NetworkServer.Spawn(nextBullet); でクライアント側でも球が表示されるわけですが、velocity はクライアント側に引き継がれませんのでクライアント側でも別に velocity を与えてあげる必要があります

そこでクライアント側ではクライアント側で Instantiate をした object に対して velocity を与えてあげる RpcShoot() をサーバでの処理である CmdFire() のあとに呼び出します

       [Command]
        void CmdFire()
        {
            Vector3 bulletDir = transform.forward;
            Vector3 bulletPos = transform.position + bulletDir * 1.5f;

            // The bullet needs to be transformed relative to the shared anchor.
            GameObject nextBullet = (GameObject)Instantiate(bullet, sharedWorldAnchorTransform.InverseTransformPoint(bulletPos), Quaternion.Euler(bulletDir));
            nextBullet.name = "Bullet";
            Vector3 velocity = bulletDir * 5.0f;
            nextBullet.GetComponentInChildren<Rigidbody>().velocity = velocity;
            NetworkServer.Spawn(nextBullet);
            RpcShoot(nextBullet, velocity);

            // Clean up the bullet in 8 seconds.
            Destroy(nextBullet, 8.0f);
        }

        [ClientRpc]
        void RpcShoot(GameObject obj, Vector3 velocity)
        {
            obj.GetComponentInChildren<Rigidbody>().velocity = velocity;
        }

これで無事にクライアント側でも弾が出現した後に前へ進むようになりました

Docker で openAI の gym 環境を作って強化学習サンプルを試してみた

目的

  • Docker のお勉強
  • openAI をお試し
    • とりあえず動くところまで!

開発環境

Windows 10 Pro

Docker 環境構築

qiita.com

ここを参考にしました。 ただし CPU 仮想化が無効になっていたので BIOS モードで起動して CPU 仮想化を有効にする必要がありました。

Docker を使った openAI/Gym の環境構築

https://hub.docker.com/r/eboraas/openai-gym/

今回これを使います。 スターもいくつかついているし良さそう。Windows PowerShell で以下のコマンドを叩いて Docker イメージをプル。

$ docker pull eboraas/openai-gym

Docker コマンドの簡単な使い方はここらへんを参考に。

qiita.com

qiita.com

Jupyter Notebook を使うことで インタラクティブに openAI のコードを試すことができるため、いくつか引数を指定します。

$ docker run -d -p 8888:8888 -p 6006:6006 -v /path/to/notebooks/:/mnt/notebooks/ eboraas/openai-gym

コンテナがたちあがったら http://localhost:8888 にブラウザでアクセスして Jupyter Notebook を閲覧できます。

python2 の notebook を作成して「Shift + Enter」で実行してみます。

Jupyter の使い方は以下を参考にしました。

Jupyterを使ってみた - Qiita

f:id:korechi:20181105113508p:plain

動いた!!!!

公式ドキュメントにあがっているサンプルコードだと Jupyter 上で実行してもウインドウが立ち上がらないため、一部書き換えました。

import gym
from IPython import display
import matplotlib.pyplot as plt
%matplotlib inline

env = gym.make('Breakout-v0')
env.reset()
for _ in range(1000):
    plt.imshow(env.render(mode='rgb_array'))
    display.clear_output(wait=True)
    display.display(plt.gcf())
    env.step(env.action_space.sample())

python3 の Notebook が作れないのが気になりますが今回はここまでとします。

ARKit 2.0 のサンプルアプリ SwiftShot で遊んで少し調べてみた

公式に SwiftShot(WWDC でデモされたアプリ) のサンプルコードがあったので DL して手元でビルドして遊んでみました。 (アプリに関する説明はリンク先に書かれているため割愛します。)

SwiftShot: Creating a Game for Augmented Reality | Apple Developer Documentation

f:id:korechi:20181003143120p:plain

少し遊んでみて分かったのは、まさに(ARKit 2.0 の目玉である)「複数端末間で AR 空間を共有」する機能を紹介するためのサンプルアプリだなという感じです。

ただ「ワールドの保存」ができないのは気になりました。これも ARKit 2.0 の目玉機能のはずなのに。 アプリを再起動したらまた0からゲームを始めることになります。(当然ゲームとしては問題ないのですが、デモアプリならこの機能も使ってほしかった)

では SwiftShot がどうやって実現されているか(主に通信まわり)簡単に調べてみます。

複数端末接続には MultipeerConnectivity を使用

SwiftShot は同じネットワーク内の端末と接続するために MultipeerConnectivity フレームワークを使っています。 (ARKit が収集した空間マップデータはピア・ツー・ピアのデバイス間のみで共有されるためデータが外に漏れるのを防ぐことが可能)

MultipeerConnectivity についてここで深掘りはしませんがもし興味があれば調べてみてください。

ARKit の spatial understanding(すみませんこの意味はまだ分かりません) を含む ARWorldMap 作成及び送信は Host の役割です。

Client はワールドのコピーを受信し Host 視点からのカメラ画像を見ることでマルチプレイが可能となります。 なので Host がゲームを開始した時の端末とだいたい同じ場所・向きに Client 端末を重ねないとゲームは開始しません。 (特徴点である ARAnchor を含む ARWorldMap を渡せば別に同じ画像でなくともワールドが復元できそうな気もしますが、どうなんだろう。正確なゲーム空間の座標共有のための安全策かな?)

ARSession のマルチプレイについては Creating a Multiuser AR Experience を見ると良さそう。 簡単にサマってみます。

ARWorldMap の送信

ARWorldMap は getCurrentWorldMap を呼ぶことで ARSession からキャプチャできます。

その際キャプチャするのに適したタイミングかを worldMappingStatus から知ることができます。

switch frame.worldMappingStatus {
case .notAvailable, .limited:
    sendMapButton.isEnabled = false
case .extending:
    sendMapButton.isEnabled = !multipeerSession.connectedPeers.isEmpty
case .mapped:
    sendMapButton.isEnabled = !multipeerSession.connectedPeers.isEmpty
}
mappingStatusLabel.text = frame.worldMappingStatus.description

その後 NSKeyedArchiver を使って Data オブジェクトに変換し multipeer セッションを介して他デバイスに送信されます。

sceneView.session.getCurrentWorldMap { worldMap, error in
    guard let map = worldMap
        else { print("Error: \(error!.localizedDescription)"); return }
    guard let data = try? NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
        else { fatalError("can't encode map") }
    self.multipeerSession.sendToAllPeers(data)
}

Shared Map の受信

session(_:didReceive:fromPeer:) デリゲートから data を受信。その後 ARWorldMap オブジェクトにデシリアライズして configuration に登録します。

if let worldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data) {
    // Run the session with the received world map.
    let configuration = ARWorldTrackingConfiguration()
    configuration.planeDetection = .horizontal
    configuration.initialWorldMap = worldMap
    sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    
    // Remember who provided the map for showing UI feedback.
    mapProvider = peer
}

AR コンテンツとユーザ操作の同期(共有)

ARAnchor などの AR オブジェクトは Host がワールドを送信する前の状態は当然 ARWorldMap に含まれるためそのまま他端末へ共有されます。 しかし、それ以降追加された AR オブジェクトは当然自動的に共有されないため随時ブロードキャストしていく必要があります。 SwiftShot では一例として、共有する情報をすべて ARAnchor オブジェクトに変換し共有しています。

// Place an anchor for a virtual character. The model appears in renderer(_:didAdd:for:).
let anchor = ARAnchor(name: "panda", transform: hitTestResult.worldTransform)
sceneView.session.add(anchor: anchor)

// Send the anchor info to peers, so they can place the same content.
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
    else { fatalError("can't encode anchor") }
self.multipeerSession.sendToAllPeers(data)

まず ARAnchor オブジェクトを作成しそれを sceneView.session.add(anchor:anchor) でシーンに追加。 その後先程と同様に NSKeyedArchiver.archivedData で data オブジェクトへシリアライズして NSKeyedArchiver.archivedData で送信します。

ARWorldMap を送信する手順とそんなに変わらないです。

受信側も同様に multipeer session から data を受信し ARAnchor を含んでいたらデシリアライズし、シーンに追加します。

if let anchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARAnchor.self, from: data) {
    // Add anchor to the session, ARSCNView delegate adds visible content.
    sceneView.session.add(anchor: anchor)
}

ただこれはあくまで多くある実現手段の一つにすぎないです。別の通信手段、別の送受信フォーマットを使うことも当然可能でしょう。 例えばゲームは様々なデータ型(例えば初速や加速度など)を含むオブジェクトを共有したい場合があります。 そんなときは Swift の Codable プロトコルを使ってバイナリにシリアライズしてネットワークに送信することも可能です。

Unity で ARKit 2.0 のサンプルアプリを実機動作させる方法(β版)

ARKit 2.0 を Unity でいち早く試したい人向けの記事です。

(注意)XCode も Unity plugin for ARKit も iOS もベータ版ですので自己責任でお願いします。 ベータ版が嫌という方は秋の正式リリースを待ちましょう。(ただし Unity plugin for ARKit もすぐにリリースされるかは不明)

また、全てベータ版なので正式版では一部変更になることも十分にあり得ます。悪しからず。

はじめに

ARKit 2.0 で何が追加されたか、以下の記事でまとめてあります。 korechipostit.hatenablog.com

Unity でまずはアプリを実機で動かしてみたいんだ!という方はこの記事は後回しでも良いです。

必要なもの

以下の4つが必要です。各自無いものがあればダウンロードしましょう。

  1. Unity
    • Unity 2017.4 より新しいバージョン
  2. XCode 10.0 Beta
  3. iOS 12.0 beta がインストールされた端末
    • やり方はこちらを参考にしました
  4. Unity plugin for ARkit 2 Beta
    • ここ から DL できます

Unity plugin for ARkit 2 Beta

Unity で ARKit の機能を扱うための SDK です。以下のファイルに使い方なり機能が書かれています。

Unity-Technologies / Unity-ARKit-Plugin / source / docs / WhatsNewInARKit2_0.md — Bitbucket

ビルド手順

Unity での操作

  1. 上の手順4 で DL した Unity 用プロジェクトディレクトリを Unity で開きます
  2. UnityARKitPlugin -> Examples -> ARKit 2.0 にある適当なサンプルシーンを開きます
    • 今回は Unity ARWorldMap を選択 f:id:korechi:20180610234019p:plain:h200
  3. Build Settings から Platform を iOS へ変更
  4. Build
  5. XCode 用プロジェクトが生成

実際に iPhone へアプリをビルドするのは XCode からになるので XCode での操作にうつります。

XCode 10.0 beta での操作

  1. XCode 用に生成したプロジェクトの *.xcodeprojectXCode 10.0 beta で開きます
  2. Team を適当なものに設定
  3. Deployment Target を念の為 12.0 に設定
  4. iOS 12.0 beta がインストールされた端末を接続しビルド

成功!

f:id:korechi:20180610234608p:plain:h300

画面上の Save ボタンが想定どおりの挙動をしているのかは怪しいですが、ひとまず Unity で ARKit 2.0 のサンプルを動作させることができました!