PhysicsRaycasterとRenderTexture&RawImageの罠

ビルドインやURPに問わず、unityのCameraにはRenderTextureをアタッチできる『TargetTexture』という項目があります。
この機能を使う事でゲームに複数視点の画面を表示することが出来ます。

しかし、TargetTextureとPhysicsRaycasterも一緒に使おうとすると、解像度に気をつけないと思わぬトラブルが発生します。

unityroomで公開している不正麻雀、および麻雀ポーカーの開発中も、このトラブルが発生して四苦八苦していました。

本記事では、CameraにRenderTextureをセットしてRawImage経由で画面に表示させた状態で、PhysicsRaycasterを使用した時に発生するバグ(仕様?)と、その対策を記載していきます。

結論

  • TargetTextureを設定してサイズをstretch設定にしたRawImage経由で画面を表示すると、解像度が変化した時にPhysicsRaycasterがバグる。
  • 原因は2つ
     TargetTextureを設定すると、カメラの挙動が変わるから。
     stretch設定のRawImageによって画面に映ったオブジェクトと、本来のオブジェクトの位置が異なるから。
  • 修正方法は3つ
     TargetTexture+RawImage構成を諦めて、カメラを重ねて表示するように変更する。
     処理負荷やメモリ節約を諦めて、RenderTextureを動的に変更する。
     マルチ解像度設定を諦めて、解像度を固定させる。ただしWebGLだと不可能。
  • ChatGPTは教えてくれません。

カメラをRenderTextureで表示する とは?

説明するの面倒なので、分かりやすく解説されているサイトを張っておきます。

雑に言えば、画面内に別の画面を表示したい時に使います。

発生条件

上記URLの様に別画面を表示するだけなら、特に何事もなく使用できます。
しかし、以下の条件を満たした場合に意図しない挙動になります。

  • CameraのTargetTexture項目にRenderTextureをセット
  • ↑のCameraへ『PhysicsRaycaster』をアタッチ
  • RenderTextureをアタッチしたRawImageのRectTransformを『stretch』に設定
    (またはCanvasScalerのUIScaleModeを『ScaleWithScreenSize』に設定)
  • 任意のオブジェクトにColliderとEventTriggerをアタッチ
  • EventTriggerにクリックイベントやマウスホバーイベントを登録
  • RenderTextureのサイズと、画面の解像度が異なる
右クリックなどで拡大すると、見やすくなるかも

状況再現したプロジェクト

状況を再現したアプリを用意しました。(Window専用)
https://drive.google.com/file/d/1iTjHq1YGytBcFrBuSQYrRvMipKaK091y/view?usp=sharing

スマホ/Macの方はYouTubeに動画を用意したので、そちらで発生状況を確認できます。
https://youtu.be/VCrDIIHNgO8

操作方法

ダウンロードしてアプリを実行すると上記の画面が表示されます。
  • ①解像度を変更します。
  • ②現在の表示モードです。画像では1280×720解像度のRenderTextureをRawImage経由で表示している事を表しています。
  • ③表示モードを変更します。Cameraで表示するモードと1280×720解像度のRenderTextureで表示するモードに変更できます。
  • ④被験体です。被験体にマウスを合わせると色が変化します?

このアプリですが、RenderTextureモードで解像度を1280×720以外に変更すると、被験体にマウスを当てても色が変化しなくなります。

そして被験体が存在しない特定の場所にマウスを当てた時に、色が変化する事が確認できます。(Youtubeの30秒以降で確認可能)

原因

この不具合が発生する原因は2つあります。

1つはCameraのTargetTextureへ値を指定しているかによって、解像度変化時のカメラ挙動が変化するため。
もう1つは、stretch指定のRawImageで引き伸ばした事により、本来存在するオブジェクトの場所とRawImageで表示した場所が一致しなくなってしまうためです。

TargetTexture指定によるCamera挙動の違い

CameraのTargetTextureがnullの場合、解像度に合わせて自動でCameraの表示領域が調整されます。
しかしTargetTextureを指定すると、解像度が変化しても起動時のCamera表示領域から変化しません。

上記だけだと分かりにくいので、RenderTextureを指定するとどう変化するのか、画像を用意しました。
RenderTextureのサイズは1280×720、表示している解像度は600×800になります。

アプリを600×800解像度で表示した画面(右図は配布したソフトで再現出来ないので悪しからず)

画像にある各図の説明

  • 左図:TargetTextureはnullで、RawImageも使用していません。
    600×800解像度に合わせ、カメラの表示領域が自動で調整されます。
  • 中央図:TargetTextureを指定して、RawImageのRectTransform.Anchorsを『Stretch』にしています。
    600×800解像度に合わせるために、アスペクト比を変更してまで1280×720の解像度を詰め込んでいます。
  • 右図:TargetTextureを指定、RawImageのRectTransform.Sizeを『1280×720』に指定、さらにCanvasScalerのUIScaleModeを『Constant Pixel Size』に指定しています。
    600×800解像度なのに、1280×720と同じ領域をそのまま表示しています(超重要!)
    横は600pxしかないため、601pxから1280pxまでの部分は画面に写っていません。
    縦は800pxありますが、720pxまでしか描写しておらず、721から800pxまでの部分は使用されません。

このように、TargetTextureの指定有り無しによって、解像度が変わった時のカメラ挙動が変わる仕様になっています。

まぁ、こんな極端にアスペクト比を変える機会は少ないと思いますが…
アスペクト比が同じで解像度が異なる場合(アプリだと1920×1080に変更した場合)、左図と中央図は『全く同じ見た目』になります。

ここにもう1つの原因が組み合わさると、私を2度も悩ませた凶悪な罠へと変貌します。

存在するオブジェクトと表示したオブジェクトの場所が違う

RawImageのRectTransform.Anchorsが『stretch』になっている場合、上記画像の中央図のようにぴったり合うように画像サイズを調整します。

気をつけないといけないのは、調整したのは画像サイズであって、Cameraではないという事です。
つまり、Cameraに写っているオブジェクトの場所と、画面に表示しているオブジェクトの位置が調整した量だけずれます。

上記アプリをRenderTextureモードで1920×1080の画面を表示すると、一見すると綺麗に全画面表示されます。
ですが、この画面はRawImageによって引き伸ばされた画面です。
オブジェクトが存在する場所は、実際には画像で示す以下の場所になります。

黄色はRawImageで表示されるキューブの位置で、赤色はキューブが実在する位置です。

さて、ここでPhysicsRaycasterの説明です。
PhysicsRaycasterとは、マウスの座標へ『Camera』からRayを発射してColliderを検知、イベントを通知する機能です。

『Canvas』からRayが発射される訳じゃありません!


『Displayed cube position』へマウスを当てても反応しなかった理由がこれです。
つまり、PhysicsRaycasterからRayを発射してオブジェクトに命中させるには『Correct cube position』へマウスを当てなきゃダメなんです。

はい、完全に罠です。思い出すだけでも腹立たしい。

修正方法

著作の不正麻雀および麻雀ポーカーでは、それぞれ別の対策を取っています。
プロジェクトによってより良い修正方法が変わる可能性があるため、それぞれの修正方法を書いておきます。

TargetTextureを使用しない

まず最初の修正方法は、TargetTextureとRawImageを使用せず、カメラをそのまま表示することです。
ビルドインであればDepthを変更して画面を上書き表示、URPであればカメラスタッキング機能を使って上書きできます。

ただし、このままだとアスペクト比が変更されたときに、表示しておきたいオブジェクトが見切れてしまいます。

そのため、以下のスクリプトをCameraにアタッチします。

using System;
using UnityEngine;

namespace WhimGames.CameraClass {

    [RequireComponent(typeof(Camera))]
    public class CameraAspectFixer : MonoBehaviour {

        Camera targetCamera = null;
        [SerializeField] Vector2 aspectRatio = new Vector2(9, 16);

        /// <summary> Orthographic size at awake </summary>
        float startOrthographicSize = 0;
        /// <summary> Perspective fov at awake </summary>
        float startPerspectiveFov = 0;

        private double aspectRatioRate = 0;

        void Awake() {
            targetCamera = GetComponent<Camera>();
            startOrthographicSize = targetCamera.orthographicSize;
            startPerspectiveFov = targetCamera.fieldOfView;
        }
        private void OnEnable() {
            AspectChange();
        }

        private void Update() {

            //Corrects the camera if the aspect ratio changes.
            if (aspectRatioRate != GetCurrentAspectRate()) {
                AspectChange();
            }
        }

        private void AspectChange() {

            // Adjusted to maintain starting width
            var scaleWidth = (Screen.height / aspectRatio.y) * (aspectRatio.x / Screen.width);
            var scaleRatio = Mathf.Max(scaleWidth, 1.0f);

            if (targetCamera.orthographic) { 
                targetCamera.orthographicSize = startOrthographicSize * scaleRatio;
            }
            else { 
                targetCamera.fieldOfView = FOV(startPerspectiveFov, scaleRatio);
            }

            //Update the current aspect ratio.
            aspectRatioRate = GetCurrentAspectRate();
        }

        private float FOV(float fov, float scaleRatio) {
            return Mathf.Atan(Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad) * scaleRatio) * 2.0f * Mathf.Rad2Deg;
        }
        public double GetCurrentAspectRate() {

            float wigth = Screen.width;
            float height = Screen.height;
            if (height == 0) return 0;

            return Math.Round(wigth / height, 2);
        }
    }
}

このスクリプトは、アスペクト比が変化した際に、今まで映っていた横幅の映像を維持する処理を行っています。
例によって説明が難しいので画像を用意しました。

このように画像が見切れないよう調整すれば、TargetTextureを代替として活用できます。

難点は、表示したい領域をViewPortRect等で調整するのが苦労する事と、RawImageで何かしらの処理や加工を行っている場合はこの方法が使用できない事という事です。

この修正方法は、プロジェクトを始めて間もない時期だったり影響が殆どないと判断できる場合、つまり仕様変更に柔軟な状態であればお勧めの方法です。

そして不正麻雀ではこの方法で対応しています。
ちなみに、プロジェクト後半で修正したのでメチャクチャ悪影響を及ぼしてます。
(当時は不具合の原因を把握できてなかったため、やむを得ずこの対応を行いました)

RenderTextureのSizeを動的に変更する

どうしてもRawImageが必要だったり、変更後の影響を少なくしたい場合の対応法になります。

修正方法は単純で、解像度が変化した時にRenderTextureをリサイズします。
リサイズするためのサンプルスクリプトは以下の通りです。
なお、下記スクリプトはRawImageの横幅Anchorだけをstretchにする前提で作成しています。

using UnityEngine;

namespace WhimGames.CameraClass {

    public class ResizeRenderTexture : MonoBehaviour {

        [SerializeField] RenderTexture render = null;

        int width;
        float renderAspect;
        Vector2Int originalSize;

        private void OnEnable() {
            //Resize to current size
            originalSize = new Vector2Int(render.width, render.height);
            width = Screen.width;
            renderAspect = (float)render.width / render.height;
            ResizeTexture();
        }

        private void OnDisable() {
            //Restore to original size
            render.Release();
            render.width = originalSize.x;
            render.height = originalSize.y;
            render.Create();
        }

        private void Update() {
            if (width != Screen.width) {
                width = Screen.width;
                ResizeTexture();
            }
        }

        private void ResizeTexture() {
            if (renderAspect == 0) return;
            render.Release();
            //Resize based on width
            render.width = width;
            render.height = (int)(width / renderAspect);
            render.Create();
        }
    }
}

このクラスを任意の場所にアタッチしてリサイズしたいRenderTextureを指定すれば、アスペクト比を維持したまま自動でサイズ調整してくれます。

この修正方法の利点は、既存のゲームシステムをほぼ壊さない事です。
しいて言えばRawTextureのRect設定は弄るかもしれませんが、それ以外の変更点はないハズ。

しかし高解像度にすると、RenderTextureもその解像度に合わせて大きくなるので、その分レンダリングの負荷が増えます。
それと、もしかしたらリサイズする度にメモリを消費してGCが発生しそうな気がします。
(この辺詳しくないので間違ってたらごめんなさい)

麻雀ポーカーではこの修正方法を採用しています。

解像度を固定する

最後の修正方法、といえるかは分かんないですが一応。
不正麻雀、麻雀ポーカーの両方とも、最初はこの対応で解決しようと奮闘していたので書いておきます。

そもそもの話、解像度を固定してRenderTextureをその解像度に合わせておけば、この不具合は発生しません。
ぶっちゃけこの対応が一番楽だし、さらに他システムへの影響も一切与えません。

難点としては、当然ながら解像度変更には対応できない事です。
そして、この対応はWebGLだと不可能です。

もう一度言います。

WebGLだと不可能です

WebGLはPlayerSettingsで指定した解像度を勝手に2倍し、ブラウザでズームした時にしれっと解像度を変えて、スクリプトによる解像度指定をガン無視する。
解像度が変化しないよう設定できたと思っていたのに、WebGLが無断で解像度を書き換える暴挙を行っているという事実を知った時の私の心境が如何ほどのものであったか、言うまでもないですよね?(# ゚Д゚)

最後に

検索しても出てこないし、ChatGPTに聞いてもあんまり有力な回答を得られなかったので、記事にしました。
いやまぁ、正確にはChatGPTのおかげで解決への糸口は掴めたんですが、提示された修正案が的外れでした。

という訳で、この記事が同じ問題を抱えている方への役に立てば幸いです。

それと、ChatGPTが答えてくれない不具合というのは意外とあるので、これからもそういう不具合を見つけたら記事にしていきたいですね。

コメントする

CAPTCHA