Unity AR Plane Occlusionの実装について


ARで現実世界にオブジェクトを生成する時、開発者が指示しなければ、カメラは生成したバーチャルオブジェクトと現実世界のオブジェクト(机とか)との位置関係を正しく認識することができず、結果的に不自然な描画になってしまいます。

このオブジェクトの重なりをいかに描画するかと言う問題は何もARに限った話ではなく、Computer Graphicsにおける基本的な問題の1つです。
もちろんすでに先人達が解決してくれているので、その知恵を基にこの問題に対処したいと思います。

今回、AR Plane Occlusionを実装するに当たって、レンダリングパイプラインやdepth bufferについての理解が必要不可欠だったので、そこら辺も含めて記事にしたいと思います。

今回実現したいこと

ARでより現実味のある自然な描画を実現したい!

現状

  • ARで出力された描画が不自然
  • レンダリングパイプラインのどの部分をいじれば、自然な描画に近づけるのかわからない。

準備

そもそもレンダリングパイプラインとは、パイプライン処理によって入力されたデータ(3次元モデルデータなど)を最終的に2次元の画像として出力するまでの過程全体のことです。

Unity道場 2019.2 シェーダを書けるプログラマになろう #1 シェーダを理解しよう
と言う動画で非常にわかりやすくUnityにおける描画プロセスが解説されています。

この動画の一部を転載すると

各ステップについては先ほどの動画を見てもらうとして、今回注目したいのはステップ6と8です。

ステップ6

ステップ6はZTestと言われるものです。
描画するオブジェクトが重なっている場合、depth bufferの値を基に、奥にあるのか手前にあるのかを判定します。

Depth Bufferとはざっくり言えば、カメラから対象とするオブジェクトまでの奥行き(z値)を保持しているバッファになります。なので今回のようなオブジェクト同士の前後関係(手前か奥か)を判定するとは、各オブジェクトのz値を比較すると言っても過言ではないでしょう。

ステップ8

ステップ8ではZWriteがOnならばZ値の更新を行います。基本的には不透明なオブジェクトの描画の際はOnで、部分的な透過などを実装したい際はOffにします。

後述しますが今回は不透明な場合なのでOnにします。

AR Plane Occlusionの実装

ZTestで手前にあるオブジェクトが描画されるようにすれば良いまでは分かりました。
しかし現実にある机や椅子はそのままではオブジェクトとして扱えません。そこで、机やら椅子やらに対してマッピングするように透明なplaneを生成します。

ARFoundationではAR Plane Managerなるものがあり自動的に平面を検知して、指定したplaneオブジェクトを生成してくれます。

まずAR Session OriginにAR Plane Managerコンポーネントをアタッチする。

AR Default Planeを作成し、MeshRendererには自作のMaterialをセットします。
Line Rendererも必要ないのでremoveしても良いです。

自作Materialには以下のシェーダーを指定するだけで良いです。

PlaneOcclusion
Shader "Custom/PlaneOcclusion"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue" = "Geometry-1" }
        ZWrite On
        ZTest LEqual
        ColorMask 0

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(0, 0, 0, 0);
            }
            ENDCG
        }
    }
}

大したことはしていませんが、注目ポイントはPassの前の設定の部分です。

Tags { "RenderType"="Opaque" "Queue" = "Geometry-1" }
ZWrite On
ZTest LEqual
ColorMask 0

まずQueueタグに関して

"Queue" = "Geometry-1"

Queueタグはレンダリングの順番を指定するためのタグです。Stack, Queueとかでよく出てくるQueueと同じで、値が小さいほど先に描画されます。半透明なオブジェクトを描くためには、不透明オブジェクトとの描画順を正しく指定してあげる必要があったりしますので、Queueはそういう時のために力を発揮します。Unityでは

Name Value
Background 1000
Geometry 2000
AlphaTest 2450
Transparent 3000
Overlay 4000

の順番に従って描画されます。今回はGeometry-1とすることでこのshaderをアタッチしたオブジェクトが通常のGeometryタグが指定されているオブジェクトよりも先に描画されると言うことがわかります。

ZWrite On
ZTest LEqual

ZWrite Onとはdepth bufferの更新を行うと言うことなので
また、Z値の比較判定はLess than equalの時(つまりよりカメラに近くある時)に成功とします。

こうすることで、例えば現実世界にある机が生成したバーチャルオブジェクトよりもカメラ側により近くにあるならば、机が描かれて、バーチャルオブジェクトは描かれなくなります。

ColorMask 0

これを指定することで色の出力を無効にすることができます。現実のオブジェクトに対して色付けなどしないので、より自然な描画となります。

結果

plane detectionの精度が高くなく、段ボールの面よりも大きくplaneをマッピングしているために、不自然な描画になってしまいました。
しかし、今回目標としていたplane occlusionはできていることが分かります。

Next Step

  • AR People Occlusionを実現したい!

参考文献

最後に

間違いがあれば指摘していただけると嬉しく思います!