【Unity】Vuforiaを使うとCanvasの 'Screen Space - Camera' が使えなくなるので対処法を考えた

今作っているアプリで、画面の左下に、Cameraにレンダリングされる UI(アプリロゴ)を表示する必要があった(Render textureとして取得したかった)。

当然、普通ならCanvasのRender ModeをScreen Space - Cameraに設定しておけば良い。 しかし、何度やってもうまく描画されず、なんでやねんと調べてみると、どうやら Vuforia Behaviour を適用している状態のカメラをRender Cameraに設定すると、WidthとかRightとか、そういった値が壊れてしまうことが分かった。

当初はARCameraの階下に別のCameraを配置して、レンダリングはそっちにやらせれば…などと思っていたのだが、処理が重くなりそうなのと、肝心なAR周りの描画がちょっと不安定になったりしたのでやめた。

完成図

手法

結果としては、今回は、カメラの near clipping plane にあたる位置を取得し、それをもとにQuadを配置する手法をとった。

当初はポストエフェクト的にロゴを表示する方法なども考えたが、ポストエフェクトではCameraがレンダリングした画像としては取得できないことに気づいてこちらもやめた。

実装

まず作ったのは、near clip planeの取得処理。

これはそのものを取得することは出来ないので、 Camera.ScreenToWorldPoint を使って取得する。 具体的には以下

AsagiHandyScriptSingle/GetScreenWorldPoint.cs at master · M-T-Asagi/AsagiHandyScriptSingle · GitHub

using UnityEngine;

namespace AsagiHandyScripts
{
    public static class GetScreenWorldPoint
    {
        public struct ScreenWorldPoints
        {
            public Vector3 topLeft;
            public Vector3 topRight;
            public Vector3 bottomLeft;
            public Vector3 bottomRight;

            public ScreenWorldPoints(Vector3 tl, Vector3 tr, Vector3 bl, Vector3 br)
            {
                topLeft = tl;
                topRight = tr;
                bottomLeft = bl;
                bottomRight = br;
            }
        }

        public static ScreenWorldPoints GetPoints(Camera eye)
        {
            Vector3 UPSIDE_DOWN;
            UPSIDE_DOWN.x = 1;
            UPSIDE_DOWN.y = -1;
            UPSIDE_DOWN.z = 1;

            ScreenWorldPoints points;
            points.bottomLeft = eye.ScreenToWorldPoint(new Vector3(0, 0, eye.nearClipPlane));
            points.topLeft = eye.ScreenToWorldPoint(new Vector3(0, Screen.height, eye.nearClipPlane));
            points.topRight = eye.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, eye.nearClipPlane));
            points.bottomRight = eye.ScreenToWorldPoint(new Vector3(Screen.width, 0, eye.nearClipPlane));
            return points;
        }
    }
}

のように書いた。 画面のそれぞれ端点をNearClipの距離に合わせて取得する。

次に書いたのはQuadの配置とスケーリング処理で、

以下のように書いた

private void Update()
{
    GetScreenWorldPoint.ScreenWorldPoints points = GetScreenWorldPoint.GetPoints(camera);

    Vector3 newScale;
    newScale.x = Vector3.Distance(points.topLeft, points.topRight);
    newScale.y = newScale.x;
    newScale.z = 1;
    quad.localScale = newScale;

    Vector3 newPos = camera.transform.position;
    newPos += camera.transform.forward * (camera.nearClipPlane - 1f);
    newPos += -(points.topLeft - points.bottomLeft) * 0.5f + camera.transform.up * newScale.y * 0.5f;
    quad.position = newPos;
}

Scale部分の処理

Vector3 newScale;
newScale.x = Vector3.Distance(points.topLeft, points.topRight);
newScale.y = newScale.x;
newScale.z = 1;
quad.localScale = newScale;

今回、の目的は「ロゴを左下に表示したい」と「画面の大きさに合わせてスケールさせたい」の二点だったので、上記のような処理になった。

表示する画像はただのロゴ画像ではなく、背景透過された正方形の画像の左下にロゴが配置されている、といったもので、たとえば画面が正方形であればこの処理だけで画面の左下にロゴが配置できるものになった。

もし、そういった画像が用意できない、あるいはスクリプトだけで処理を完結させたい場合は、これを

newScale.x = Vector3.Distance(points.topLeft, points.topRight) * 0.25f;
newScale.y = (newScale.x / originX) * originY;

のように書く必要がある。

Position部分の処理

こちらは、

Vector3 newPos = camera.transform.position; でQuadの位置をカメラにそろえ

newPos += camera.transform.forward * (baseSettings.Eye.nearClipPlane - 1f); でnearClipPlaneの位置にQuadをそろえ

newPos += -(points.topLeft - points.bottomLeft) * 0.5f + camera.transform.up * newScale.y * 0.5f; でQuadを画面下部に固定している。

気を付けたいのはnear clip planeの位置に合わせている部分で、最後に -1f などしているように、少しばかりカメラ側に寄せないとVuforiaのAR(カメラ映像)とぶつかる。

おわり