Shaderを一切いじらずにPortal表現を行う


あけましておめでとうございます。
でもわたしのクリスマスはまだ来てないのでAR Advent Calendar 2019に投稿します。

Portal

Augmented Reality Portal - ARKIT

これです。別の世界を現実の空間に繋げることで現実を拡張する、まさにARならではの表現なので、これを実装できれば表現の幅が大きくなって楽しいはずです。
ですが、ネットで調べる限り、みんなshaderを理解して変更することでこれを実現しています。

い…嫌じゃ…shaderなど書きとうない…

環境づくり

shaderをなにがなんでも書きたくないわたしのような人や、なんらかの事情でPortalの向こうに映すGameObjectshaderをいじれないという人もいるでしょう。
というわけで、今回はそういう人のためにshaderを一切いじらずにPortal表現を実現してみたいと思います。
今回実現するPortalは「タップした場所の壁に穴を開けて、そこから向こう側の世界が覗ける」という一般的なやつです。

完成品はこんな感じ。1

リポジトリはこちら。
Hole

環境は以下のとおりです。

Unity 2019.2.17
AR Foundation 2.0.2
Android 9

RenderTextureLayerCulling Muskを使って実現します。
わかる人ならたぶんこれだけでもうわかるはず。

RenderTexture

まずRenderTextureを新規作成します。

わたしはなんとなくsizeを2倍にしましたが、基本的にはなにもパラメータをいじらなくても動作しました。

Layer

続いてLayerを設定します。

「Hole」というLayerを設定しました。

Scene

そしてシーンの編集に入ります。ARFoundationの基礎的なコンポーネント作成についてはいろんな人が書いているのでここでは省きます。

タップ位置検出のためにARRaycastManagerを、平面描画のためにARPlaneManagerAR Session Originに追加します。

AR描画部分は終わったので次はPortalの中の描画に必要なコンポーネントをSceneに作っていきます。

camera

まず穴の中を映すためのHoleCameraを作ります。このCameraはUnityのメニューから新規作成するのではなく、AR Session Originの子オブジェクトになっているAR CameraをCopy Componentして、 空オブジェクトにPaste As Newすることで作成しましょう。FoVなどの数値はARFoundationのCameraと合わせておいたほうが見え方の不自然さが減ります。

そして、今回の要諦である、カメラのCulling Muskを設定します。
まずAR CameraのCulling MuskからHoleを除外します。

続いてHoleCameraのCulling MuskHoleだけに設定します。

これでAR CameraにはHoleレイヤに設定されたもの以外のすべてが映るようになりました。
逆にHoleCameraにはHoleレイヤに設定されていないものは一切映りません。
「そこにあるのに映らない」ものを専用のカメラで映し、専用のカメラの映像をRenderTextureに投影することで「壁の穴から向こうの世界が見える」ようになったわけです。

仮想空間

続いて穴の中として映す仮想空間。
これはUnity上でだけ動くものなのでもうなんでもいいのですが、自作するのが面倒なのでフリーのAssetのサンプルシーンをそのまま拝借してきました。gitに上げているものはgitignoreでこのアセットを除外してあるので、cloneしたらこのAssetをインポートしてください。
POLYGON - Starter Pack

配置してあるオブジェクトとライトだけ引っこ抜いて、管理用の空オブジェクトの子オブジェクトにします。
負荷軽減のためにstaticにしてlightをbakeしておきます。
そしてLayerHoleに設定。これで引っこ抜いてきたものはHoleCameraにだけ映るようになりました。

以上でシーンの作成は終わりです。最初は使わないので、穴の中関連のGameObjectはすべてdisableしておきます。

Hierarchyはこんな感じになりました。

script

ひとまずscriptの全文を載せます。

HolePlaceManager
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

namespace NekomimiDaimao
{
    public class HolePlaceManager : MonoBehaviour
    {
        [SerializeField]
        private Transform _mainCamera;

        [SerializeField]
        private ARRaycastManager _raycastManager;

        private bool _holePlaced = false;

        public bool HolePlaced
        {
            get => _holePlaced;
            set
            {
                if (_holePlaced == value)
                {
                    return;
                }

                _holePlaced = value;
                _otherWorld.SetActive(value);
                _holeCanvas.gameObject.SetActive(value);
                _holeCamera.gameObject.SetActive(value);
            }
        }

        [SerializeField]
        private GameObject _otherWorld;

        [SerializeField]
        private Transform _holeCamera;

        private readonly Vector3 AppendHeight = Vector3.up * 1.5f;

        [SerializeField]
        private Transform _holeCanvas;

        private readonly List<ARRaycastHit> _hitResults = new List<ARRaycastHit>();

        private void Start()
        {
            HolePlaced = false;
        }

        private void Update()
        {
            if (HolePlaced)
            {
                _holeCamera.SetPositionAndRotation(
                    _mainCamera.position + AppendHeight,
                    _mainCamera.rotation
                );
            }

            PlaceHole();
        }

        private void PlaceHole()
        {
            if (Input.touchCount < 1)
            {
                return;
            }

            var touch = Input.GetTouch(0);
            if (touch.phase != TouchPhase.Began)
            {
                return;
            }

            _hitResults.Clear();
            if (!_raycastManager.Raycast(touch.position, _hitResults, TrackableType.Planes))
            {
                return;
            }

            var p = _hitResults[0].pose;
            _holeCanvas.SetPositionAndRotation(p.position, p.rotation);
            // 壁の場合, transform.upがnormalになる
            var normal = _holeCanvas.up;
            _holeCanvas.forward = normal;
            // eularAngleのxz成分を0にすることで穴の向きを合わせる
            var euler = _holeCanvas.eulerAngles;
            _holeCanvas.eulerAngles = new Vector3(0f, euler.y, 0f);
            HolePlaced = true;
        }
    }
}

穴の中向けカメラの位置合わせ

private readonly Vector3 AppendHeight = Vector3.up * 1.5f;

private void Update()
{
    if (HolePlaced)
    {
        _holeCamera.SetPositionAndRotation(
            _mainCamera.position + AppendHeight,
            _mainCamera.rotation
        );
    }

    PlaceHole();
}

Updateで毎フレームHoleCameraの位置と向きをAR Cameraの位置と向きに合わせています。
基本的にUnityのシーンは地面の中心をワールド座標の原点として作成されている……はずです。
一方、AR Foundationのワールド座標系はアプリ起動時の位置を原点とします。そのため、HoleCameraをそっくりそのままAR Cameraと一致させるとHoleCameraが床の中を掘り進んだりしてつらいことになります。

ゆえに、なんらかの補正をかけて現実空間の床の高さと仮想空間の床の高さを一致させる必要があります。

今回はサンプルなのとめんどくさいなので固定値で高さを補正していますが、本来ならばあらかじめAR Cameraの床面からの高さを取得しておき、HoleCameraを有効にした時点での値を常に反映する、などするのがよいと思います。
HoleCameraではなく仮想空間の座標を下げる、という手段もありますが、今回は仮想空間をまるごとぜんぶstaticにしてしまったのでHoleCameraの方に補正をかけました。あと個人的にカメラ動かすほうがなんか感覚に合ってて好きです。
あと、当然ですが、HoleCameraをAR Cameraの子オブジェクトにしてLocalPositionでどうにかしようとすると、Rotationの影響を受けて座標がぶっ飛びます。

もろもろのメリットとか注意点とか

platoformへの依存が少ない

Unity層で話が完結しているため、どのようなARデバイス・フレームワーク上でも動くはずです。ARCoreARKitなどのスマホARはなんだかんだ練れているので変な目に合うことも少ないのですが、ARグラスはまだまだ開発中の物も多いので……。

Lighting気にしなくていい

環境光の反映してえなぁ……してぇよなぁ! なぁ! ってなると大変な目に合いますが、そうでなければ、Portalに投影しているのは純Unity空間の映像なのでAR要素は気にせず好きにLightingをいじれます。

分業・切り替え

Portalに映す仮想空間を別シーンとして追加ロードすることもできます。これによってPortal内は別の人に作ってもらって、自分はAR部分を作り込むことが可能です。
また、仮想空間のシーンを切り替えてロードすることでPortalの向こうを動的に切り替えることも可能です。AssetBundleで後から追加とかも。

重いかも

なんとなく重いような気配です。RenderTextureが重いんだと思いますが、ARは基本かつかつなので常時表示しているとまずいかもしれません。

まとめ

ARFoundation部分はあえて解説を飛ばして、Portal部分に絞りました。
shader書きたくないという一念で試してみたらさっくりできたのでよかったです。
ネットで調べても同じことをしているらしき人がいません。初心者だとそもそもPortal知らないし、知ってる人はshaderさくっと書けるのでそっちでやっちゃうんでしょうか。なぞ。

Portalするだけならノンコーディングで実現できるので、AR教材のチュートリアルに組み込んだりすると見た目が楽しいなのでいいかもしれません。

おしまい。

リポジトリ

Hole

参考

レンダーテクスチャを使ってカメラに映る映像をリアルタイムに描写する

Unityで認識した平面をポータル化して異世界を覗く【ARFoundation編】


  1. 部屋見えちゃうので左右は勘弁しておくんなさい。あとなんでかカーテンしか認識してくれなくて、肝心の壁にはPortalがうまく貼れませんでした。