2022年5月25日水曜日

光学透過型のARディスプレイっぽく表示するUnityシェーダ

光学透過型ディスプレイで提示したCGの仮想物体をUnity内で再現するには,単にオブジェクトを半透明するだけではダメで, 背景像に仮想物体の輝度が加算されないといけない. それを簡単に再現するUnity用シェーダを作ったので忘れないように重要な部分だけメモしておく.

必要な特徴

Unity上では全部CGだけど,不透明なオブジェクトを実物体,光学透過型ディスプレイで提示されたつもりの半透明なオブジェクトを仮想物体と呼んでおく.

  1. 実物体の前にある仮想物体の輝度は単純加算される(仮想物体の黒い箇所は透明になる)
  2. 実物体の後ろにある仮想物体は実物体により遮蔽される
  3. 仮想物体の後ろにある別の仮想物体は,前の仮想物体により遮蔽される
  4. 仮想物体の背面は,前面に遮蔽される

今回紹介する方法では,上の特徴全部を再現できる.

ソースコード

このシェーダを光学透過型ディスプレイで表示されたようにしたい物体にアタッチするだけ. このシェーダはUnlitシェーダを数行改変しただけ.

Shader "Unlit/OstDisplay"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "black" {}
        _Luminance ("Luminance", Range(0.0, 1.0)) = 1.0
    }
    SubShader
    {
        Cull Back ZWrite On ZTest Less
        Blend One One
        Tags {
            "Queue"="AlphaTest"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
        }       
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            sampler2D _MainTex;
            float _Luminance;
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col*_Luminance;
            }
            ENDCG
        }
    }
}

左が実際に使用したテクスチャで右がレンダリング結果. 結果をみると分かるようにチェスボード模様の黒い部分は完全に透明になっていて背景をそのまま透過させている(特徴1). 黒じゃない部分は背景色に白や緑が加算されている(特徴1). 青い球と後ろにある仮想物体を見てみるとちゃんと仮想物体が実物体に遮蔽されている(特徴2). 一方,仮想物体どうしの重なりを見ると後ろの仮想物体の「あ」の一部は描画されていないので仮想物体どうしでも遮蔽が実現できている(特徴3). 一つの仮想物体だけに注目すると立方体の3面しか見えていないので単一の仮想物体の背面が前面に遮蔽されているのも分かる(特徴4). ということで必要な全ての特徴を満たすことができた. だからこのシェーダは提示する仮想物体の幾何学的整合性が全部満たされたときに,光学透過型ディスプレイでどのように見えるかを再現していると言える.

解説

初期値

まずこの部分.この部分はディスプレイに表示するテクスチャ(_MainTex)とディスプレイの輝度(_Luminance)の初期化に関する部分. Properties内に変数と初期値を定義するとインスペクタ上で選択できる.初期値はデフォルトの値となる.

    Properties
    {
        _MainTex ("Texture", 2D) = "black" {}
        _Luminance ("Luminance", Range(0.0, 1.0)) = 1.0
    }

インスペクタ上で設定した値はこの変数に代入される. これを定義していないとコンパイルエラーになる.

            sampler2D _MainTex;
            float _Luminance;

遮蔽

次にこの部分.透明な物体を描く上で遮蔽関係は重要.ディスプレイにどのように3D CGを表示したいかによるけど, ここでは3Dオブジェクトでも一旦は普通に描画してその結果が光学透過型ディスプレイに表示されている,という感じにしたい. だから,背面は普通にカリングされて表示されないようにした上で,後ろの物体とブレンドされないといけえない.

        Cull Back ZWrite On ZTest Less

Cull Backはカリング(Cull)される対象が物体の背面(Back)という意味.左がCull Back,中央がCull Off,右がCull Frontの結果. 各オブジェクト内のポリゴンで背面を描かないという設定で特徴4に対応.

左:Cull Back,中央:Cull Off.右:Cull Front.

ZWrite Onはこの仮想物体のデプスをzバッファに書き込むかどうかを指定している. 左がZWrite On,右がOff.透明な仮想物体(QueueがAlphaTestやTransparent)は,不透明な物体の描画が終わってから描かれる. だから仮想物体自体のデプスが影響するのは別の仮想物体.右図では,後ろの仮想物体のZバッファないため,どちらの仮想物体を描くときも実物体の奥行きと仮想物体の奥行きが比較されて,実物体より手前にある部分が描画されている.そのため仮想物体どうし重なる部分の画素が2重に描かれてしまっている.

左:ZWrite On,右:ZWrite Off.

ZTest Lessは,既に描かれたzバッファに対してこの仮想物体のポリゴンの奥行きが手前(Less)にあるときのみ描画することを意味している.下図の通りZTest Less(左)のときは正常に描画されているけど,ZTest Greater(右)のときは描画された実物体のzバッファの値よりも奥(Greater)にある透明物体のポリゴンだけが描かれる.

左:ZTest Less,右:ZTest Greater.

ブレンディング

透明な物体を描くということは前景と背景が混ぜるということなので,ここが一番重要. 光学透過型ディスプレイでは,実シーンからの光に,ディスプレイの光が加算される.

        Blend One One

左のOneが手前の物体のアルファ値で,後ろのOneが背景のアルファ値と考えればいい. Unityのマニュアルでは左がsource,右がdestinationとなっているけどそのネーミングが分かりにくい. 数式で表せば分かりやすい.透明な物体は,不透明な物体が全部描き終えてから,一番奥の透明物体から手前の透明物体へ順に描画される. $i$番目の透明物体(もしくは最後の不透明物体)が描かれた時点での画素値を$C_{i}$,今から描画しようとする透明物体の画素値を$C_{\rm obj}とすると,その物体の描画結果$C_{i+i}$は次式で表すことができる.

\[ C_{i+1} = w_{\rm obj} C_{\rm obj} + w_{i} C_{i} \]

$w_{\rm obj}$と$w_{i}$は重みでこれが左のOne($w_{\rm obj}=1$)と右のOne($w_{i}=1$)に対応している. この結果$C_{i+1}$は次の物体を描画する際には右辺に入ることになる. この重みを変えてみた結果が下の図.

左:Blend One One.右:Blend Zero Zero.

左:Blend Zero One.右:Blend One Zero.

Tag

Tagはシェーダの動作を切り替える役割を持っている. デフォルトのUnlitシェーダからの変更点は,1行目と3行目.

        Tags {
            "Queue"="AlphaTest"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
        }

Queueは,描画順序を決めるタグ. 透明物体に関するタグはAlphaTestとTransparentがあり,不透明物体はGeometry.他にもあるけどこの3つなら Geometry→Transparent→AlphaTestの順に描画される.

次の図はこの3つのタグを試してみた結果. AlphaTestとGeometryが全く同じ結果に見えるし,TransparentはZWriteをOffにしたときの結果と結果と全く同じに見える. 描画順序だけの問題ならこうはならないように思えるのでこのタグによって何が変化するのか理解できていないかも. マニュアルを読んだだけでは理解できなかった.

左:AlphaTest.中央:Transparent.右:Geometry.

RenderTypeは,特定のオブジェクトだけシェーダを切り替えるためのタグ. これはOpaqueに変えようがTransparentに変えようが結果に変化なかった.今回は関係なさそう.

仮想物体の輝度

最後は仮想物体の輝度を反映させる部分. インスペクターで設定した輝度(Luminance)を描画結果の輝度に掛け算する.

                fixed4 col = tex2D(_MainTex, i.uv);
                return col*_Luminance;

参考