THETAの360度カメラの映像をリアルタイムでOculus Quest2に表示する


この記事でやること

  • THETAの映像をTHETA Web APIの一つ、camera.getLivePreviewを使用してリアルタイムな映像を取得する
  • 取得した画像データをUnityで球の表面にテクスチャとして適用
  • 球をOculus Quest2で見ることでTHETAの映像をリアルタイムで閲覧!

Quest2にビルドして単体で動かすことが可能です。

発端

  • RICOH THETA SC2を購入したらUSBケーブルでのライブストリーミングに非対応だった
  • でもそもそもQuest2単体でUSB接続のWebカメラとの通信はできなさそうだった
  • (追記)OSがAndroidベースなのでUVCで有線接続可能でした
  • THETA Web APIなら動いたのでヨシ!

開発環境

Unity 2019.4.26f1を使用しています。
AndroidのビルドツールはUnityと一緒にインストールできるやつです。

Sphereの準備

こちらのサイトによると、単純に360度画像を表示する分にはICO球が一番きれいに表示できるようなので、ここのサイトからoctahedron-sphere-meshes.unitypackageをお借りします。

ICO Sphereの配置

Unityを起動し、Assets → Import Package → Custom Packageから先ほどダウンロードしたunitypackageファイルを選択し、プロジェクトにインポートします。

インポートが完了したら、Assetsフォルダ内にOctahedron Sphereフォルダが増えているので、その中のMeshes → Radius 1 → Octahedron Sphere 5 R1をHierarchyにドラッグ&ドロップします。

Sphereが配置出来たらScaleの値を(100, 100, 100)程度に大きくしておきます。
ついでにMain Cameraの座標を(0, 0, 0)にして球の内側に入るようにしておきましょう。(Quest2にビルドする際はMain Cameraを使いませんが・・・)

ICO球用のマテリアル作成

Project内の適当な位置で新規マテリアルを作成し、ShaderをStandardからSkybox/Panoramicに変更します。(もっと良いシェーダーがある気がしますが、ひとまずこれで動いたので割愛)
シェーダーを変更したらRender QueueをFrom QueueからTransparentに変更します。こうすることで球の内側からでもテクスチャが見られるようになります。

先ほど配置したICO球のMesh Rendererに今作成したマテリアルをアタッチします。

THETA Web APIを使う

↓ 公式ドキュメント

THETA Web APIって?

THETAは内部機能で自分をHTTPサーバーとして、接続してきたクライアントと無線LANで通信することが可能です。
公式が提供しているTHETA Web APIに沿ったHTTPリクエストを作成し、POSTすることで各種コマンドを実行し、写真を撮影したり動画を撮影したりすることができます。今回はその中のgetLivePreviewコマンドを使用しています。

getLivePreviewコマンドのアレコレ

{
    "name":"camera.getLivePreview"
}
  • MotionJPEG形式でリアルタイムのカメラ映像が送信され続ける
    • MotionJPEG:0xFFD8で始まり0xFFD9で終わる画像形式
  • カメラのシャッターが押されるとライブプレビュー終了
  • ↓機種によって画質が違う

コードを書く

以下が今回作成したC#のコードです。

LivePreview.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.IO;

public class LivePreview : MonoBehaviour{
    // 取得した画像を適用するマテリアル
    public Material view_material;

    void Start(){
        StartCoroutine(GetLivePreview());
    }

    IEnumerator GetLivePreview(){
        // POST先のURL。基本的には機種によらず同じURLで実行することができる
        const string get_livepreview_URL = "http://192.168.1.1:80/osc/commands/execute";
        WebRequest livepreview_request = HttpWebRequest.Create(get_livepreview_URL);
        livepreview_request.Method = "POST";
        livepreview_request.Timeout = 300000;

        // POSTするjsonファイルの加工
        livepreview_request.ContentType = "application/json;charset=utf-8";
        byte[] post_data = System.Text.Encoding.UTF8.GetBytes("{\"name\":\"camera.getLivePreview\"}");
        livepreview_request.ContentLength = post_data.Length;

        // 通信開始
        Stream request_stream = livepreview_request.GetRequestStream();
        request_stream.Write(post_data, 0, post_data.Length);
        request_stream.Close();
        Stream response_stream = livepreview_request.GetResponse().GetResponseStream();

        BinaryReader response_reader
            = new BinaryReader(new BufferedStream(response_stream), new System.Text.ASCIIEncoding());
        List<byte> picture_data = new List<byte>();
        bool is_load_start = false;

        // MotionJPEGで送信される画像データを受け取り次第テクスチャとして適用させ続ける
        while (true){
            byte byte_data = response_reader.ReadByte();
            if (!is_load_start){
                // MotionJPEGの先頭2バイトは0xFFD8
                if (byte_data == 0xFF){
                    byte next_data = response_reader.ReadByte();
                    if (next_data == 0xD8){
                        // MotionJPEGの先頭2バイトの確認ができたので、画像データとして取得開始
                        picture_data.Add(byte_data);
                        picture_data.Add(next_data);

                        is_load_start = true;
                    }
                }
            }else{
                picture_data.Add(byte_data);
                // MotionJPEGの末尾2バイトは0xFFD9
                if (byte_data == 0xFF){
                    byte next_data = response_reader.ReadByte();
                    if (next_data == 0xD9){
                        // MotionJPEGの末尾2バイトの確認が出来たので、画像データをまとめてテクスチャとして適用
                        picture_data.Add(next_data);

                        Texture2D sphere_texture = new Texture2D(1, 1);
                        sphere_texture.LoadImage((byte[])picture_data.ToArray());
                        Destroy(view_material.mainTexture); // 書いておかないと無限にテクスチャが増えてメモリリークする
                        view_material.mainTexture = sphere_texture;
                        picture_data.Clear();
                        System.GC.Collect();
                        is_load_start = false;
                        yield return null;
                    }else{
                        picture_data.Add(next_data);
                    }
                }
            }
        }
    }
}

UnityにはUnityWebRequestという便利なクラスがありますが、今回のようにデータが垂れ流しで送りつけられてくる場合はC#の機能で通信する方が良いようです。

アタッチする

上記のスクリプトをsphereオブジェクトに、view_materialに先ほど作成したマテリアルをアタッチすることで準備完了です。
PCとTHETAを無線で繋げた状態でEditorのPlayを押すことで実行可能です。

Quest2へのビルド準備

Main CameraのままではQuest2のヘッドトラッキングが反映されません。
Asset StoreからOculus Integrationをダウンロード・インポートします。
インポートできたらAssetsフォルダ内にOculusフォルダが増えているため、Oculus → VR → Prefabs → OVRCameraRigをHierarchyにドラッグ&ドロップし、Main Cameraを削除します。

ビルド

PCとQuest2をケーブルで接続し、PlatformをAndroid、Run DeviceをQuest2にしてBuildをクリックするだけです。
Quest2内のWiFi設定でTHETAと接続することを忘れないようにしてください。

余談

リアルタイムで見ることはできますが、正直SC2では画質がかなりつらいです。Vでも試してみたところ多少マシという感じでした。
単純にPCとTHETAをUSBで繋げるだけであればUnityのWebCamTextureを使ったほうがはるかに楽できれいでした。