UnityとFirebaseでストリートビューっぽいの作る

## 成果物
PCブラウザで実行してる動画
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Unityでストリートビューっぽいもの作ってみた<a href="https://twitter.com/hashtag/unity?src=hash&amp;ref_src=twsrc%5Etfw">#unity</a> <a href="https://twitter.com/hashtag/firebase?src=hash&amp;ref_src=twsrc%5Etfw">#firebase</a> <a href="https://twitter.com/hashtag/webgl?src=hash&amp;ref_src=twsrc%5Etfw">#webgl</a> <a href="https://t.co/LV3bBpHEqV">pic.twitter.com/LV3bBpHEqV</a></p>&mdash; ユキネコのヤマト (@yamatohkd) <a href="https://twitter.com/yamatohkd/status/1315236203864576001?ref_src=twsrc%5Etfw">October 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

iPhoneのSafariから実行
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/68d36161-1583-e68b-ec67-a3fd717c7738.png">

## この記事の趣旨
1. Unityで**簡単にWebGLアプリをビルド**できる
2. Firebase Hostingで**簡単にWebGLアプリを公開**できる
3. しかも**無料**([※制限あり](https://firebase.google.com/pricing?hl=ja))


## WebGLって何？？
ブラウザ上で2Dとか3Dをグリグリいじれるスゴイやつ

JavaScriptライブラリだと[**three.js**](https://threejs.org/)などが有名
↓サンプル見るだけで楽しい
https://threejs.org/

Unityの3Dプロジェクトなんかも**WebGL(HTML5 + JavaScript)でビルド**できる

※ただし**モバイル端末は未サポート**なので注意(サポートしてないだけで割と動く)
>Unity WebGL コンテンツは現在モバイル端末ではサポートされていないことに注意してください一部の機器、特にハイエンドなものでは動く場合もありますが、現在の大抵の端末は性能不足で Unity WebGL を十分に動かすだけのメモリがありません

参考：[WebGL のブラウザー間での互換性](https://docs.unity3d.com/ja/2019.4/Manual/webgl-browsercompatibility.html)

## UnityでWebGLを利用できるようにする
Unityインストール時に以下の項目をチェックする
・WebGL Build Support

既にUnityをインストールしている場合は、以下の手順でモジュールの確認・追加が可能
参考：[Unityにモジュールを後から追加する方法](https://styly.cc/ja/manual/add-modules-unity/)

## Unityでプロジェクトを作成
**3Dプロジェクト**で作業を進めます

##### 1. 3Dビューオブジェクトの準備
まずは有志(warapuri氏)が作成した以下ファイルをダウンロードしてください
* [Sphere100.fbx](https://t.umblr.com/redirect?z=https%3A%2F%2Fwww.dropbox.com%2Fs%2Fu5b2uejauh38lvq%2FSphere100.fbx&t=NWNhYzA2OGFlMTdiOWNiN2UxYzI0YWY4YzA2N2NmNDcyOWMxMTIxOCxadTZ5YTdNaQ%3D%3D&b=t%3ApNJlTKNdg-dtr5P_yVCMwQ&p=https%3A%2F%2Fwarapuri.com%2Fpost%2F131599525953%2Funity%25E3%2581%25A8oculus%25E3%2581%25A7360%25E5%25BA%25A6%25E3%2583%2591%25E3%2583%258E%25E3%2583%25A9%25E3%2583%259E%25E5%2585%25A8%25E5%25A4%25A9%25E5%2591%25A8%25E5%258B%2595%25E7%2594%25BB%25E3%2582%2592%25E8%25A6%258B%25E3%2582%258B%25E6%2596%25B9%25E6%25B3%2595%25E7%2584%25A1%25E6%2596%2599%25E7%25B7%25A8&m=1&ts=1596098168)

参考：[UnityとOculusで360度パノラマ全天周動画を見る方法【無料編】](https://warapuri.com/post/131599525953/unity%E3%81%A8oculus%E3%81%A7360%E5%BA%A6%E3%83%91%E3%83%8E%E3%83%A9%E3%83%9E%E5%85%A8%E5%A4%A9%E5%91%A8%E5%8B%95%E7%94%BB%E3%82%92%E8%A6%8B%E3%82%8B%E6%96%B9%E6%B3%95%E7%84%A1%E6%96%99%E7%B7%A8)

①Unityの**Projectウィンドウ**へドラッグ＆ドロップでインポート
②それを**Hierarchyウィンドウ**へドラッグ&ドロップ
![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/351df5f0-d3ea-c954-b90f-f2cb5550cd06.png)

##### 2. 360度写真の準備
以下のサイトなどで360度写真をダウンロードできます
・[360cities.net](https://www.360cities.net/stereoscopic/gallery?lang=ja&page=1)

スマホカメラでも『Googleストリートビュー』アプリなどで撮影することができるようです

今回は購入後一度だけ使用して物置きに放置された[GoPro MAX]((https://gopro.com/ja/jp/shop/cameras/max/CHDHZ-201-master.html))で撮影しました

##### 3. 画像をオブジェクトにセット
①画像を**「Resources/Images」フォルダ**へインポート
②オブジェクトに画像をセット
![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/841de398-3827-422d-751b-4f0ac7ffd8ee.png)
③画像の**Max Sizeを変更**（小さいと画像が荒くなる）
![4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/b7d3912c-ce01-a0a1-ed1a-1e99cc2e18ee.png)
HierarchyウィンドウからSphere100を選択し、
④Inspectorウィンドウから**Scaleを全て100**に、
⑤Shaderを**Unlit/Texture**に、
⑥写真が**左右反転している場合はTillngのXを-1**に変更
![3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/904169cc-1bea-5bab-8262-893d4904738a.png)
⑦Shere100オブジェクトに**Canvasをアタッチ**して、
⑧Projectウィンドウへ**ドラッグ＆ドロップでPrefab化**
![5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/a6c8fbdb-d351-fb17-07bf-e1d694bc2110.png)
ボタンもいい感じに作ってPrefab化して、
**「JumpButton」**という名前でResources/Prefabsフォルダへ配置

Prefab化したオブジェクトは**Hierarchyから削除しておく**

##### 4. カメラ制御用のスクリプト
以下のサイトから拝借
[Unity:マウスドラッグでカメラを回転して視点を操作するスクリプト
](http://corevale.com/unity/2346)

マウスホイールでズームイン/ズームアウトできるようUpdate関数に追記

```c#:CameraRotater.cs
    private float scroll;
    private float view;

    void Update()
    {
        scroll = Input.GetAxis("Mouse ScrollWheel");
        view = mainCamera.fieldOfView - scroll * 50;

        mainCamera.fieldOfView = Mathf.Clamp(value: view, min: 30f, max: 90f);

        /** 省略 **/
```

##### 5. 移動時のカメラ演出用スクリプト
移動するときのビヨ～ン感を出したい
以下のサイトを参考にカメラにブラーを追加する
[【Unity】【シェーダ】Radial Blur（放射状ブラー）のポストエフェクトを実装する](https://light11.hatenadiary.com/entry/2019/01/04/232733)

初期値は以下にしておく
・Sample Count : 16
・Strength : 0

外から変数を変更できるようスクリプトに以下を追記

```c#:RadialBlur.cs
    public void SetStrength(float _strength)
    {
        this._strength = _strength;
    }
}
```

##### 6. 写真の位置情報を作成
写真のそれぞれのid, xy座標, 移動先のidを設定
これを元に移動ボタンを作成します
![9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/3bdfdd2d-4636-353d-e5a8-4283fb7266f4.png)
これを**CSVファイル**にして**Resourcesディレクトリに配置**

CSVに対応したクラスを作成

```c#
public class PhotoPosition
{
    public int id;
    public float x;
    public float y;
    public int[] jumpTo; 
}
```

CSVをクラスに変換してくれる無料アセットをインポート
・[CSV Serialize](https://assetstore.unity.com/packages/tools/integration/csv-serialize-135763?locale=ja-JP)

##### 7. メインスクリプトの作成
以下のスクリプトを作成し、適当なオブジェクトにアタッチ

```c#:CreateSphereController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;

public class CreateSphereController : MonoBehaviour
{
    // 写真の位置情報リスト
    private List<PhotoPosition> photoPositions;
    // 写真のオブジェクト一覧
    private Object[] images;

    // 3Dビューインスタンス一覧
    private List<GameObject> spheres;
    // 3Dビューのプレハブ
    private GameObject spherePrefab;
    // 移動ボタンプレハブ
    private GameObject buttonPrefab;
    // 移動時のカメラのブラースクリプト
    private RadialBlur radialBlurScript;

    // カメラのブラーの強さ
    float blurStrength;
    // カメラのブラー処理用フラグ
    bool isUpStrength;
    bool isDownStrength;

    void Start()
    {
        spheres = new List<GameObject>();
        // Resourcesフォルダのposition.csvを取得
        TextAsset csvFile = Resources.Load("position") as TextAsset;
        photoPositions = new List<PhotoPosition>();

        // CSVから写真の位置情報をオブジェクトリストに変換
        photoPositions.AddRange(CSVSerializer.Deserialize<PhotoPosition>(csvFile.text));
        
        // id=1の写真を初期値に設定
        PhotoPosition photoPosition = photoPositions.Find(n => n.id == 1);

        // プレハブ取得
        spherePrefab = (GameObject)Resources.Load("Prefabs/Sphere100");
        buttonPrefab = (GameObject)Resources.Load("Prefabs/JumpButton");
        // Imagesフォルダ配下をすべて取得
        images = Resources.LoadAll("Images", typeof(Texture));

        // メインカメラのブラースクリプトを取得
        GameObject mainCamera = Camera.main.gameObject;
        radialBlurScript = mainCamera.GetComponent<RadialBlur>();

        // ID=1の写真で3Dビューオブジェクト生成
        spheres.Add(CreateNextSphere(1));
    }

    // 3Dビューオブジェクトを生成
    GameObject CreateNextSphere(int nextPositionId)
    {
        // IDから写真の位置情報を取得
        PhotoPosition nextPosition = photoPositions.Find(n => n.id == nextPositionId);
        // 3Dビューのインスタンス生成
        GameObject nextSphere = Instantiate(spherePrefab, new Vector3(0.0f, 0.0f, 0.0f), Quaternion.identity);

        // 3Dビューオブジェクトのマテリアルを取得
        Material material = nextSphere.GetComponent<Renderer>().material;
        // 写真をテクスチャに設定
        material.SetTexture("_MainTex", (Texture)images[nextPosition.id - 1]);

        // 移動用ボタンを生成
        foreach (int jumpToId in nextPosition.jumpTo)
        {
            // 移動先の写真情報を取得
            PhotoPosition jumpToPosition = photoPositions.Find(n => n.id == jumpToId);
            // 移動用ボタンのインスタンスをCanvas配下に生成
            GameObject jumpButton = Instantiate(buttonPrefab, new Vector3(0.0f, -3.0f, 0.0f), new Quaternion(0.0f, 0.0f, 0.0f, 0.0f), nextSphere.transform.Find("Canvas"));
            // ボタンクリック時のイベントを追加
            jumpButton.GetComponent<Button>().onClick.AddListener(() => OnClickButton(jumpToId));

            // 次の写真のxyベクトル
            Vector2 nextVector = new Vector2(nextPosition.x, nextPosition.y);
            // 移動先の写真のxyベクトル
            Vector2 jumpToVector = new Vector2(jumpToPosition.x, jumpToPosition.y);

            // ベクトルから角度を計算
            Vector2 dt = jumpToVector - nextVector;
            float radian = Mathf.Atan2(dt.y, dt.x);
            float degree = radian * Mathf.Rad2Deg;

            // ボタンの向き、位置を設定
            jumpButton.transform.Rotate(0.0f, 0.0f, degree);
            jumpButton.transform.localPosition = new Vector2(-5 * Mathf.Cos(radian), -5 * Mathf.Sin(radian));
        }

        // 3Dビューのインスタンスを返す
        return nextSphere;
    }

    // ボタンクリック
    void OnClickButton(int id)
    {
        // 3Dビューインスタンスを追加
        spheres.Add(CreateNextSphere(id));
        // カメラのブラー処理開始
        isUpStrength = true;
    }

    // Update is called once per frame
    void Update()
    {
        // カメラのブラー処理を強める
        if (isUpStrength)
        {
            ChangeBlurStrength(true);
        }
        // カメラのブラー処理を弱める
        else if (isDownStrength)
        {
            ChangeBlurStrength(false);
        }
    }

    // カメラのブラー処理
    void ChangeBlurStrength(bool isUpFlg)
    {
        // 強める
        if (isUpFlg)
        {
            blurStrength += 0.05f;
            // 一番強まったら
            if (blurStrength >= 1)
            {
                // 強めるの終了
                isUpStrength = false;
                // 弱めるの開始
                isDownStrength = true;

                // 古い方の3Dビュー削除
                Destroy(spheres[0]);
                spheres.RemoveAt(0);
            }
        // 弱める
        } else
        {
            blurStrength -= 0.05f;
            // 弱まりきったら
            if(blurStrength <= 0)
            {
                // 弱めるの終了
                isDownStrength = false;
                // 誤差が生じるから明示的に0を指定
                blurStrength = 0;
            }
        }
        // ブラーの強さ設定
        radialBlurScript.SetStrength(blurStrength);
    }
}

```

</div></details>

</div></details>
## WebGLアプリをビルド
Build SettingsからWebGLを選択してBuildするだけ！
![7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/97710c7d-cd6b-4303-9cd0-dd25f1352a28.png)
## Firebaseの設定

##### 1. Firebaseでプロジェクト作成
[Firebase](https://console.firebase.google.com/?hl=ja)にGoogleアカウントでログイン

「プロジェクトを追加」ボタンを選択し、
プロジェクト名などを入力して作成

##### 2. PCにFirebaseをインストール
Firebase CLIをインストール

```shell
npm install -g firebase-tools
```

##### 3. Firebaseプロジェクトの初期化

まずはFirebaseへログイン

```shell
$ firebase login
```

Firebaseプロジェクトの初期化

```shell
$ firebase init
```

以下の選択肢は**Hosting**を指定

```shell
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confi
rm your choices.
 ( ) Database: Deploy Firebase Realtime Database Rules
 ( ) Firestore: Deploy rules and create indexes for Firestore
 ( ) Functions: Configure and deploy Cloud Functions
>(*) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
 ( ) Emulators: Set up local emulators for Firebase features
 ( ) Remote Config: Get, deploy, and rollback configurations for Remote Config      
```

selectオプションは「Use an existing project」を選択

```shell
? Please select an option:
> Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project    
```

自分のプロジェクト一覧が表示されるので、先程作成したプロジェクトを選択

あとはエンター連打でOK！

##### 4. WebGLアプリをデプロイ

作成された**publicフォルダ**に、ビルドしたWebGLアプリ（以下ファイル）を配置
・index.html
・Build
・TemplateData


Firebaseへデプロイ

```shell
$ firebase deploy
```

##### 5. ブラウザで表示
完了メッセージに表示されたURLへアクセスするだけ
簡単！


## まとめ
WebGLアプリのビルドから公開までが簡単にできました。

せっかくUnityでアプリを作れても、AppStoreやGoogle Playに登録するのって結構大変なんですよね。

WebGLビルドを利用すれば、本記事のFirebase Hostingや、[unityroom](https://unityroom.com/)などのサイトで簡単に公開できるのでオススメです！

ちなみに撮影した写真は、北海道の[美幌峠](https://www.google.co.jp/search?q=%E7%BE%8E%E5%B9%8C%E5%B3%A0&sxsrf=ALeKk02yrB69lLZPOv2Bqfc5LLtr-RNB4Q:1602429856543&npsic=0&rflfq=1&rlha=0&rllag=43648534,144248149,23&tbm=lcl&rldimm=11005608225890299963&lqi=Cgnnvo7luYzls6BaGAoK576O5bmMIOWzoCIK576O5bmMIOWzoA&phdesc=F18e2eptKEE&ved=2ahUKEwjur4Gz7KzsAhWVPXAKHUXXDqMQvS4wAHoECAsQKg&rldoc=1&tbs=lrf:!1m4!1u3!2m2!3m1!1e1!1m4!1u2!2m2!2m1!1e1!2m1!1e2!2m1!1e3!3sIAE,lf:1,lf_ui:1&rlst=f#rlfi=hd:;si:11005608225890299963,l,Cgnnvo7luYzls6BaGAoK576O5bmMIOWzoCIK576O5bmMIOWzoA,y,F18e2eptKEE;mv:[[44.45128456030043,145.6768740870683],[42.877829386934295,141.5570010401933],null,[43.66971277997853,143.6169375636308],9])から望む屈斜路湖でした。