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


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

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

この記事の内容で以下のようなアプリが作成できます。

開発環境

開発は以下を使いました
- 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)
            {
                // 1度目のタップでテクスチャ保存
                if (!saved)
                {
                    SaveTex();
                    saved = true;
                }
                // 2度目以降のタップは平面設置
                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 のフラグを用いて以下を実現しています。

  • 1度目のタップでテクスチャ保存
  • 2度目以降のタップで平面を設置

ARCameraBackGround

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

[SerializeField]
ARCameraBackground arCamBg;

設置する平面

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

カメラ画像をテクスチャとしてコピー

上記ソースコードのSaveTex関数の以下の処理でarCamBg.materialのマテリアルをcapturedTexにコピーしています。

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

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

平面設置

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


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度回転させることでこちらを向くようにしています。


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タグが設定されていないからです。
以下のように設定しましょう。

Tips

平面のマテリアルがDefault-MaterialのままだとStanderdShaderになり暗く見えてしまうので、
Unlit系のShaderを使用したマテリアルを作っておくのがオススメです。
(ちゃんとARやろうと思ったら環境光も影響させた方がよいですが)

参考記事