[AR Foundation] カメラに写った空間を切り取ってARで配置してみる

先日ハッカソンでこんなプロトタイプを作ってみました

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">空間を切り取ってARで部屋に飾るプロトタイプ作ってみた！<br>おしゃれな装飾で自分の部屋を簡単に飾り付けできる<br> <a href="https://twitter.com/hashtag/withAR%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#withARハッカソン</a> <a href="https://twitter.com/hashtag/%E7%A9%BA%E9%96%93?src=hash&amp;ref_src=twsrc%5Etfw">#空間</a> <a href="https://t.co/LIbYOjIips">pic.twitter.com/LIbYOjIips</a></p>&mdash; ふしっきー (@fusikky) <a href="https://twitter.com/fusikky/status/1188418492925501441?ref_src=twsrc%5Etfw">October 27, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

カメラに写っている空間を平面として切り取って、AR配置するというものです。
ハッカソン当日にいろいろ調べながらやったのでそれの備忘録ですが、全部書くと結構な量になってしまうので、
「カメラに写った画像をテクスチャとして切り出してARで平面として配置する」ところまで記述します。

この記事の内容で以下のようなアプリが作成できます。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ARで空間を切り取るアプリのプロトタイプでラッセン絵のポストカードをARで置きまくってみたら結構楽しかったw <a href="https://t.co/Y91QjHl50L">pic.twitter.com/Y91QjHl50L</a></p>&mdash; ふしっきー (@fusikky) <a href="https://twitter.com/fusikky/status/1191665225323302912?ref_src=twsrc%5Etfw">November 5, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## 開発環境
開発は以下を使いました
- Unity (2019.2.0f1)
- ARfoundation (3.0.0 preview.4)
- ARKit XR Plugin (3.0.0 preview.4)

AR Foundationの導入方法はこちら
https://qiita.com/fushikky/items/e43a1974d0f833121804

## やっていること

- AR CameraオブジェクトからARCameraBackgroundを取得しRenderTextureに一時保存
- 平面オブジェクトのマテリアルに保存したテクスチャを設定しAR設置

上記で終わりなのですが、寂しいのでコードも書いておきます。

## ソースコード
```ARSaveTexture.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;

[RequireComponent(typeof(ARRaycastManager))]
public class ARSaveTexture : MonoBehaviour
{
    [SerializeField]
    ARCameraBackground arCamBg;
    [SerializeField]
    GameObject planePrefab;

    ARRaycastManager raycastManager;
    List<ARRaycastHit> hitResults = new List<ARRaycastHit>();
    RenderTexture capturedTex;
    bool saved = false;

    void Start()
    {
        raycastManager = GetComponent<ARRaycastManager>();
        // スクリーンサイズで初期化
        capturedTex = new RenderTexture(Screen.width, Screen.height, 0);
    }

    public void SaveTex()
    {
        if (arCamBg.material != null)
        {
            // RenderTextureに deep copy
            Graphics.Blit(null, capturedTex, arCamBg.material);
        }
    }

    void Update()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.GetTouch(0);
            if (touch.phase == TouchPhase.Began)
            {
                // １度目のタップでテクスチャ保存
                if (!saved)
                {
                    SaveTex();
                    saved = true;
                }
                // ２度目以降のタップは平面設置
                else
                {
                    if (raycastManager.Raycast(touch.position, hitResults))
                    {
                        var plane = Instantiate(planePrefab, hitResults[0].pose.position, hitResults[0].pose.rotation);
                        var mat = plane.GetComponent<Renderer>().material;
                        mat.mainTexture = capturedTex;
                        // Billboard
                        if (Camera.main != null)
                        {
                            Vector3 target = new Vector3(
                                Camera.main.transform.position.x,
                                plane.transform.position.y,
                                Camera.main.transform.position.z
                            );
                            plane.transform.LookAt(target);
                            // 裏表逆向きになってしまうので反転
                            var angles = plane.transform.localEulerAngles;
                            angles.y += 180;
                            plane.transform.localEulerAngles = angles;
                        }
                    }
                }
            }
        }
    }
}

```

少し解説します。
### savedフラグ
簡易的な処理ではありますが、`bool saved` のフラグを用いて以下を実現しています。

- １度目のタップでテクスチャ保存
- ２度目以降のタップで平面を設置

### ARCameraBackGround
冒頭のarCamBg変数にはARCameraオブジェクトのARCameraBackGroundコンポーネントがアタッチされています。
これはカメラで撮影している内容をCameraの背景(Background)に描画するものです。こちらのコンポーネントからマテリアルとしてカメラ画像を取得できます。

```csharp
[SerializeField]
ARCameraBackground arCamBg;
```

<img width="580" alt="qiita1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71629/8ce41eed-15bf-bc90-7fc7-a8a96544ca63.png">

### 設置する平面
planePrefab変数には Create>3D Object>QuadからQuad Prefabを生成し、Scaleに以下のパラメータを設定しています。
これは使用する端末の解像度の縦横比に合わせています。そうしないとテクスチャを貼り付けた際に歪んだ見た目になってしまいます。
(私はiPhone11 proユーザーなので2,436x1,125の比率にしています。)

<img width="572" alt="qiita3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71629/9645619f-c056-1a22-6a81-dd1deacb3e3a.png">

### カメラ画像をテクスチャとしてコピー
上記ソースコードの`SaveTex`関数の以下の処理でarCamBg.materialのマテリアルをcapturedTexにコピーしています。

```csharp
Graphics.Blit(null, capturedTex, arCamBg.material);
```

Graphics.Blit関数を使えば、第３引数に与えたマテリアルを第２引数のレンダーテクスチャへコピーすることができます。
https://docs.unity3d.com/ja/2017.4/ScriptReference/Graphics.Blit.html

### 平面設置
２度目以降のタップで平面を設置しています。
まず、RaycastManagerでタップ位置とAR空間の衝突判定を行い、衝突位置に平面をInstantiateしています。
設置した平面からマテリアルを取得し、mainTextureに先ほど保存したテクスチャを設定しています。

```csharp

if (raycastManager.Raycast(touch.position, hitResults))
{
     var plane = Instantiate(planePrefab, hitResults[0].pose.position, hitResults[0].pose.rotation);
     var mat = plane.GetComponent<Renderer>().material;
     mat.mainTexture = capturedTex;
     ...
}
```

このままだと平面がこちらを向いてくれないので、以下のコードでBillboard的な処理をして平面をこちらに向かせています。
内容としては、カメラ位置の高さ(y座標)を平面と同じにした点を向くようにLookAt関数を使用しています。
また、そのままだと裏表逆向きになってしまったので、localEulerAnglesのy軸を180度回転させることでこちらを向くようにしています。

```csharp

Vector3 target = new Vector3(
    Camera.main.transform.position.x,
    plane.transform.position.y,
    Camera.main.transform.position.z
);
plane.transform.LookAt(target);
// 裏表逆向きになってしまうので反転
var angles = plane.transform.localEulerAngles;
angles.y += 180;
plane.transform.localEulerAngles = angles;
```

ここで、Camera.mainがnullになってしまう場合は、Cameraに`MainCamera`タグが設定されていないからです。
以下のように設定しましょう。
<img width="302" alt="qiita2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71629/e39a9083-4c09-0ee8-fa1a-28348409119b.png">

### Tips
平面のマテリアルがDefault-MaterialのままだとStanderdShaderになり暗く見えてしまうので、
Unlit系のShaderを使用したマテリアルを作っておくのがオススメです。
(ちゃんとARやろうと思ったら環境光も影響させた方がよいですが)
<img width="288" alt="Screen Shot 2019-11-03 at 20.01.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71629/7ceb67a9-fbac-b6f6-a7ed-e8f10d7de23a.png">


## 参考記事
http://edom18.hateblo.jp/entry/2019/08/11/223803