Unityで認識した平面をポータル化して異世界を覗く【ステンシルバッファ編】


壁の向こうの異世界

はじめに

本記事はUnityのARFoundationを使用して、現実空間で認識された平面越しに異世界(AR空間)を表示する仕組みの備忘録で、Unityで認識した平面をポータル化して異世界を覗く【ARFoundation編】の続きになります。
前記事では、現実空間にAR空間を重畳して表示する所まで説明しました。本記事では、重畳されたAR空間を平面を通してのみ表示する、ステンシルバッファを用いた平面のポータル化について説明していきます。

ステンシルバッファによるポータル表現

ステンシルバッファは、一般的にピクセルマスクごとにピクセルの保存や廃棄を行うために使用されます。ステンシルバッファは、通常、1 ピクセルあたり 8 ビットの整数です。値はインクリメントまたはデクリメントで書き込みできます。後続の描画呼び出しは値をテストでき、ピクセルシェーダーを実行する前にピクセルを廃棄する必要があるか判断します。
参考:ShaderLab: ステンシル

ステンシルバッファではピクセル毎に表示非表示を決められるため、下記の手順により平面として認識されたピクセルのみAR空間のオブジェクトを表示することで、平面をポータル化する事ができます。

  1. AR空間の映像をテクスチャに書き込んで画面全体に表示
  2. 平面領域のピクセルをステンシルバッファのレファレンス値に書き込む
  3. 1のテクスチャからレファレンス値を参照して平面領域のピクセルのみ表示

実装

AR空間のカメラ映像をテクスチャに表示

Unityでカメラ映像をテクスチャに書き込むには、RenderTextureを使用します。
RenderTexturePlaneに適用する事で、AR空間を写した映像をAR Cameraで表示できるようにします。

  1. Projectを右クリックして Create > RenderTextureを作成

  2. ファイル名をARWorldCameraRenderTexture、Sizeを2048x2048に変更

  3. World CameraTarget Textureに作成したARWorldCameraRenderTextureを設定

    この状態でエディタを再生すると、World Cameraに映るAR空間の映像がARWorldCameraRenderTextureに反映されている事を確認できます。

  4. ARWorldCameraRenderTextureの映像表示用Planeを作成
    ARWorldCameraRenderTextureに書き込んだAR空間の映像をAR Cameraに表示できるよう、AR Cameraの子としてPlaneを作成し、CameraPreviewの全体を占めるよう位置とサイズを調整します。

    このPlaneRenderTextureを反映できるよう、Shader及びMaterialを作成して適用します。

  5. Projectを右クリックして Create > Shader > Unlit Shaderを作成

  6. Materialを作成してARWorldCameraRenderTextureを適用
    5.で作成したシェーダーのファイル名をRenderTexturePlaneShaderに変更し、そのファイルの上で右クリックして Create > Materialを選択するとUnlit_RenderTexturePlaneShaderが作成されます。
    作成されたUnlit_RenderTexturePlaneShaderTextureARWorldCameraRenderTextureを設定します。

  7. PlaneUnlit_RenderTexturePlaneShaderを適用

    これでAR空間のカメラ映像を、AR Cameraに表示できるようになりました!

この段階では、AR Cameraに本来表示される現実空間のカメラ映像の上全体に、AR空間の映像が重畳されている状態です。ここからステンシルバッファにより、平面として検出された領域のAR空間の映像のみを表示することで、冒頭の動画のような平面領域のポータル化が実現できます。

ステンシルバッファによる平面領域のAR空間描画

最初に平面領域として検出されたピクセルにステンシルバッファのレファレンス値を書き込みます。次に、AR空間の映像を表示しているPlaneからそのレファレンス値を確認し、平面領域として書き込まれているピクセルのみを描画することで、平面領域として検出された領域のみAR空間が表示されるようになります。

ステンシルバッファのシェーダーは下記記事を参考に改変しています。
【Unity】ステンシルバッファの復習#シェーダーコード

平面領域にレファレンス値を書き込み

Projectを右クリックして Create > Shader > Unlit Shaderを作成し、ファイル名をPlaneAreaTransparentShaderに変更して下記コードで上書きします。

PlaneAreaTransparentShader.shader
Shader "Custom/PlaneAreaTransparentShader" {
    SubShader {
        Tags { "RenderType"="Transparent" "Queue"="Geometry" }
        ZWrite Off 
        Blend SrcAlpha OneMinusSrcAlpha 
        Stencil {
            Ref 1
            Comp Always
            Pass Replace
        }
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            struct appdata {
                float4 vertex : POSITION;
            };
            struct v2f {
                float4 pos : SV_POSITION;
            };
            v2f vert(appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }
            half4 frag(v2f i) : SV_Target {
                return half4(0,0,0,0);
            }
            ENDCG
        }
    } 
}

参考元からの変更点としては、平面領域はレファレンス値を書き込むだけで何も表示しないようにするため、"RenderType"="Transparent"ZWrite OffBlend SrcAlpha OneMinusSrcAlphareturn half4(0,0,0,0); として透明にしています。

このPlaneAreaTransparentShaderからCustom_PlaneAreaTransparentShaderを作成し、AR Default Planeに設定されている、Mesh Renderer > Materials > Element0 > DebugPlane及び、Line Renderer > Materials > Element0 > Default-Lineと置き換えます。

以上の処理により、平面領域として検出された領域は透明で表示され、そのピクセルのステンシルバッファには、レファレンス値1が書き込まれるようになります。

AR空間の映像を平面領域箇所のみ表示

AR空間の映像を表示しているPlaneからレファレンス値を確認し、平面領域として書き込まれているピクセルのみ表示するようにRenderTexturePlaneShaderを修正します。

RenderTexturePlaneShader.shader
Shader "Unlit/RenderTexturePlaneShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}
        Stencil {
            Ref 1
            Comp Equal
        }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

全コードを掲載していますが、Unlit Shaderからの変更点は、"Queue"="Geometry+1"及び、Stencil {Ref 1 Comp Equal}の追加のみです。
AR空間の映像を表示しているPlaneは、レファレンス値として1が書き込まれている平面領域のみが表示されるようになり、平面領域のポータル化が実現します!
この修正の適用によりエディタのシーン上ではPlaneが表示されなくなりますが、これはPlaneの後ろにAR空間を表示するべき平面領域が存在しないためなので正しい挙動となります。

ビルドして実機で動作を確認すると、冒頭の動画のように平面領域として認識された領域のみAR空間が表示されます!

おわりに

「Unityで認識した平面をポータル化して異世界を覗く」の説明は以上となります。
ARFoundationの導入方法や、平面領域の可視化方法については、前記事Unityで認識した平面をポータル化して異世界を覗く【ARFoundation編】にて説明してますので、併せてご覧ください!