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

##壁の向こうの異世界

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">8時間で終わらなかったので追加作業<br>ARFoundationとステンシルバッファ何となく理解した <a href="https://twitter.com/hashtag/hachitai?src=hash&amp;ref_src=twsrc%5Etfw">#hachitai</a> <a href="https://t.co/9mTfRCuDJQ">pic.twitter.com/9mTfRCuDJQ</a></p>&mdash; 1pp0 (@1pp0qq1) <a href="https://twitter.com/1pp0qq1/status/1142818086912815104?ref_src=twsrc%5Etfw">June 23, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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

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

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

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

##実装

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

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

2. ファイル名を*ARWorldCameraRenderTexture*、Sizeを*2048x2048*に変更
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174948/6a674053-4854-0091-44b7-9ac6afa6b230.png)

3. *World Camera*の*Target Texture*に作成した*ARWorldCameraRenderTexture*を設定 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174948/e4b0cf2f-6058-b0fc-2ed7-cbe4ef7bbdf3.png)
この状態でエディタを再生すると、*World Camera*に映るAR空間の映像が*ARWorldCameraRenderTexture*に反映されている事を確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174948/1a33606e-8dc4-902e-f56b-c486ce2b3ca1.png)

4. *ARWorldCameraRenderTexture*の映像表示用`Plane`を作成
*ARWorldCameraRenderTexture*に書き込んだAR空間の映像を*AR Camera*に表示できるよう、*AR Camera*の子として`Plane`を作成し、CameraPreviewの全体を占めるよう位置とサイズを調整します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174948/d8ab180d-f025-89cf-dcd5-b20336c4ae46.png)
この`Plane`に`RenderTexture`を反映できるよう、`Shader`及び`Material`を作成して適用します。

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

6. `Material`を作成して*ARWorldCameraRenderTexture*を適用
5.で作成したシェーダーのファイル名を*RenderTexturePlaneShader*に変更し、そのファイルの上で右クリックして `Create > Material`を選択すると*Unlit_RenderTexturePlaneShader*が作成されます。
作成された*Unlit_RenderTexturePlaneShader*の*Texture*に*ARWorldCameraRenderTexture*を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174948/9db8f66f-bae8-0a55-8e73-bc5c662a6ce2.png)

7. *Plane*に*Unlit_RenderTexturePlaneShader*を適用
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174948/16c9dfb3-3bf2-8b83-6700-3570c88b1a62.png)
これでAR空間のカメラ映像を、*AR Camera*に表示できるようになりました！

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

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

ステンシルバッファのシェーダーは下記記事を参考に改変しています。
[【Unity】ステンシルバッファの復習#シェーダーコード](https://qiita.com/r-ngtm/items/ec738be7780ccaabf606#%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%80%E3%83%BC%E3%82%B3%E3%83%BC%E3%83%89)

####平面領域にレファレンス値を書き込み
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 Off`、`Blend SrcAlpha OneMinusSrcAlpha`、`return 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空間を表示するべき平面領域が存在しないためなので正しい挙動となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174948/bc022df9-5cd7-ab03-e884-93dd0dccdcd6.png)
ビルドして実機で動作を確認すると、冒頭の動画のように平面領域として認識された領域のみAR空間が表示されます！

#おわりに
「Unityで認識した平面をポータル化して異世界を覗く」の説明は以上となります。
ARFoundationの導入方法や、平面領域の可視化方法については、前記事[Unityで認識した平面をポータル化して異世界を覗く【ARFoundation編】](https://qiita.com/ippo/items/d9c046bb365e2666ffdc)にて説明してますので、併せてご覧ください！