あけましておめでとうございます。
でもわたしのクリスマスはまだ来てないのでAR Advent Calendar 2019に投稿します。
Portal
Augmented Reality Portal - ARKIT
これです。別の世界を現実の空間に繋げることで現実を拡張する、まさにARならではの表現なので、これを実装できれば表現の幅が大きくなって楽しいはずです。
ですが、ネットで調べる限り、みんなshader
を理解して変更することでこれを実現しています。
い…嫌じゃ…shader
など書きとうない…
環境づくり
shader
をなにがなんでも書きたくないわたしのような人や、なんらかの事情でPortalの向こうに映すGameObject
のshader
をいじれないという人もいるでしょう。
というわけで、今回はそういう人のためにshader
を一切いじらずにPortal表現を実現してみたいと思います。
今回実現するPortalは「タップした場所の壁に穴を開けて、そこから向こう側の世界が覗ける」という一般的なやつです。
完成品はこんな感じ。1
リポジトリはこちら。
Hole
環境は以下のとおりです。
Unity 2019.2.17
AR Foundation 2.0.2
Android 9
RenderTexture
とLayer
とCulling Musk
を使って実現します。
わかる人ならたぶんこれだけでもうわかるはず。
RenderTexture
わたしはなんとなくsizeを2倍にしましたが、基本的にはなにもパラメータをいじらなくても動作しました。
Layer
続いてLayerを設定します。
「Hole」というLayerを設定しました。
Scene
そしてシーンの編集に入ります。ARFoundation
の基礎的なコンポーネント作成についてはいろんな人が書いているのでここでは省きます。
タップ位置検出のためにARRaycastManager
を、平面描画のためにARPlaneManager
をAR 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 Musk
をHole
だけに設定します。
これでAR CameraにはHole
レイヤに設定されたもの以外のすべてが映るようになりました。
逆にHoleCameraにはHole
レイヤに設定されていないものは一切映りません。
*「そこにあるのに映らない」ものを専用のカメラで映し、専用のカメラの映像をRenderTexture
に投影することで「壁の穴から向こうの世界が見える」*ようになったわけです。
仮想空間
続いて穴の中として映す仮想空間。
これはUnity上でだけ動くものなのでもうなんでもいいのですが、自作するのが面倒なのでフリーのAssetのサンプルシーンをそのまま拝借してきました。git
に上げているものはgitignore
でこのアセットを除外してあるので、clone
したらこのAssetをインポートしてください。
POLYGON - Starter Pack
配置してあるオブジェクトとライトだけ引っこ抜いて、管理用の空オブジェクトの子オブジェクトにします。
負荷軽減のためにstatic
にしてlight
をbakeしておきます。
そしてLayer
をHole
に設定。これで引っこ抜いてきたものはHoleCameraにだけ映るようになりました。
以上でシーンの作成は終わりです。最初は使わないので、穴の中関連のGameObject
はすべてdisableしておきます。
script
ひとまずscriptの全文を載せます。
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デバイス・フレームワーク上でも動くはずです。ARCore
やARKit
などのスマホARはなんだかんだ練れているので変な目に合うことも少ないのですが、ARグラスはまだまだ開発中の物も多いので……。
Lighting
気にしなくていい
環境光の反映してえなぁ……してぇよなぁ! なぁ! ってなると大変な目に合いますが、そうでなければ、Portalに投影しているのは純Unity空間の映像なのでAR要素は気にせず好きにLighting
をいじれます。
分業・切り替え
Portalに映す仮想空間を別シーンとして追加ロードすることもできます。これによってPortal内は別の人に作ってもらって、自分はAR部分を作り込むことが可能です。
また、仮想空間のシーンを切り替えてロードすることでPortalの向こうを動的に切り替えることも可能です。AssetBundle
で後から追加とかも。
重いかも
なんとなく重いような気配です。RenderTexture
が重いんだと思いますが、ARは基本かつかつなので常時表示しているとまずいかもしれません。
まとめ
ARFoundation
部分はあえて解説を飛ばして、Portal部分に絞りました。
shader
書きたくないという一念で試してみたらさっくりできたのでよかったです。
ネットで調べても同じことをしているらしき人がいません。初心者だとそもそもPortal知らないし、知ってる人はshader
さくっと書けるのでそっちでやっちゃうんでしょうか。なぞ。
Portalするだけならノンコーディングで実現できるので、AR教材のチュートリアルに組み込んだりすると見た目が楽しいなのでいいかもしれません。
おしまい。
リポジトリ
参考
レンダーテクスチャを使ってカメラに映る映像をリアルタイムに描写する
Unityで認識した平面をポータル化して異世界を覗く【ARFoundation編】
-
部屋見えちゃうので左右は勘弁しておくんなさい。あとなんでかカーテンしか認識してくれなくて、肝心の壁にはPortalがうまく貼れませんでした。 ↩