はじめに
前回のブログで少しだけ穴あき表現について触れました。
本エントリでは動的に穴を開ける方法について解説したいと思います。
デモ
環境
- Windows 10
- Unity 5.5.1f1
ダウンロード
おさらい
ドキュメント:Case study - Looking through holes in your reality
HoloLens では黒色が透明に見えるため、穴の空いた黒いプレーンを通して向こう側を見るような表現を作れば、壁に穴をあけることも出来ます。しかしこの方法の欠点は、キャプチャをする際は黒は黒として出力されてしまう点です(※ Holographic Remoting Player でのキャプチャは透明になります)。これは以下のような数式で説明できます(厳密に正しい数式ではないので注意)。まず実際の目に見えている画は以下のように表現できます
(現実 * バイザの黒半透明) + ホログラム
ホログラムが黒(= 0)の時は現実がちょっと暗くなっただけとなります。しかしながら、スクリーンショットやビデオを撮った際は以下のようになります。
// a = ホログラムのアルファ (現実 * (1 - a)) + ホログラムの色 * a
このため、黒が 0 より大きいアルファ値を持っている場合は、透明でなく黒として映ってしまいます。これを回避するには、黒ではなく ColorMask 0
をしてデプスバッファにだけ書き込むシェーダを使った遮蔽用のマテリアルを適用します。シェーダのコードは HoloToolkit に含まれています。
https://github.com/Microsoft/HoloToolkit-Unity/blob/master/Assets/HoloToolkit/Utilities/Shaders/WindowOcclusion.shadergithub.com
方針
動的に穴を開ける方法は色々あると思いますが、メッシュに直接穴を開けるのは大変ですしコスト高です。そこで簡単にできる方法としてステンシルを使った方法を紹介します。
ステンシルはマスクとして使うことが出来、特定の領域をマーキングしてそこだけ色を付けたりつけなかったりといった事ができます。本ブログでも何度か取り扱いました:
このステンシルを使って HoloLens で壁に穴を開ける方法としては、壁をステンシルで抜く方法と、ステンシルで別の世界を切り出す方法の 2 つが考えられます*1。では 2 つの方式とメリット・デメリットを見ていきましょう。
方法 1
まず、最初の方法は壁をステンシルで抜く方法です。次の 2 つの手順になります。
- 窓となるオブジェクトをステンシルでマークして描画
- 次に壁オブジェクトを窓オブジェクトのステンシルでマークされた場所以外に描画
窓オブジェクト
適当な窓オブジェクトを作成します。窓枠と窓には別々のシェーダを適用し、窓枠は通常のスタンダードシェーダ、窓には次に説明するシェーダを適用します。
窓シェーダ
まず、窓となるシェーダから見ていきます。
Shader "HoloLens/Window" { Properties { _Mask ("Mask", Int) = 1 } SubShader { Tags { "RenderType" = "Opaque" "Queue" = "Geometry-2" } CGINCLUDE #include "UnityCG.cginc" struct v2f { float4 vertex : SV_POSITION; UNITY_VERTEX_OUTPUT_STEREO }; v2f vert(appdata_base v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return 0; } ENDCG Pass { ColorMask 0 ZWrite Off Stencil { Ref [_Mask] Comp Always Pass Replace } CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } }
Geometry-2
キューで壁オブジェクトに先駆けて描画をしています。その上で、Stencil
ブロックで Comp Always
および Pass Replace
を使ってプロパティで与えたマスク値をステンシルバッファへ書き込んでいます。この際、ColorMask 0
かつ ZWrite Off
にすることで、ステンシルバッファのみに書き込むようにしています(RGB およびデプスバッファには書き込まれない)。これで窓領域にマスクがセットできました。
ちなみに、Unity 5.5 より Single Pass Instanced が有効になっているため、その対応用に UNITY_VERTEX_OUTPUT_STEREO
および UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO()
のコードを追加しています(しない場合は Single Pass 設定時に片目にしか画が表示されなくなります)。Single Pass の設定は Player Settings から可能です:
Single Pass にすると描画コストがかなり減ります。ただし全てのシェーダが対応している必要があります(Unity のビルトインシェーダは対応しています)。
壁シェーダ
次に壁シェーダを見ていきます。
Shader "HoloLens/Hole/Wall" { Properties { _Mask("Mask", Int) = 1 } SubShader { Tags { "RenderType" = "Opaque" "Queue" = "Geometry-1" } CGINCLUDE #include "UnityCG.cginc" struct v2f { float4 vertex : SV_POSITION; UNITY_VERTEX_OUTPUT_STEREO }; v2f vert(appdata_base v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag(v2f i) : COLOR { return 0; } ENDCG Pass { ColorMask 0 ZWrite On ZTest LEqual Stencil { Ref [_Mask] Comp NotEqual } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 5.0 #pragma only_renderers d3d11 ENDCG } } }
さきほどよりも 1 つキューを上げて Geometry-1
のタイミングで描画します。こちらのステンシルは先ほどと同じマスク値を Ref
で参照するよう Mask
プロパティを調整し、その上で Comp NotEqual
で、マスクをセットした場所「以外」の場所に壁を描くようにしています。
結果
メリット / デメリット
メリットはとても簡単なことで、背後のオブジェクトのシェーダには手を入れずに、壁と窓だけで完結できる点です。また、壁に穴を開けるのと等価なので直感的です。
デメリットは、壁に近寄って壁がニアクリップで削れてしまうと向こう側が見えてしまいます(それはそれで良いかもしれませんが)。また、空間マッピングがされるまでは背景は全部見える状態になってしまいます。空間マッピングが終わった後に、壁の外のオブジェクトの Visibility を ON にする、といった制御が必要になります。
方法 2
方法 1 は、窓を壁を透かすものとして扱いましたが、もうひとつの方法は、窓の領域のみに背景を描く、というものです。背景のオブジェクト側のシェーダをいじる必要がありますが、前回のデメリットのいくつかは解消されます。
窓シェーダ
窓は方法 1 と同じシェーダを使います。これにより、窓の領域に指定したマスク値がステンシルバッファへと書き込まれます。
背景シェーダ
背景のシェーダの Stencil
ブロックで、Comp Equal
を使うようにします。先程は NotEqual
で、窓「以外」の場所に壁を描く、という形でしたが、今回は Equal
で、窓と同じ位置に描く、ということになります。キューは窓の後になるようにして、窓で書き込んだステンシルのマスクを参照できるようにします。窓のあとならいつでも良いので、ここでは通常の Gemetory
キューにしています。
Surface シェーダ
Shader "HoloLens/MaskedObject" { Properties { _Mask("Mask", Int) = 1 _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType" = "Opaque" } Stencil { Ref [_Mask] Comp Equal } CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf(Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } }
Skybox
通常のスカイボックスとして適用すると Background
で描かれてしまうので、別途キューブマップに対応した天球モデルを作成してそれを配置するようにします。
Shader "HoloLens/MaskedSky" { Properties { _Mask("Mask", Int) = 1 _Tint ("Tint Color", Color) = (.5, .5, .5, .5) [Gamma] _Exposure ("Exposure", Range(0, 8)) = 1.0 [NoScaleOffset] _Cube ("Cubemap (HDR)", Cube) = "grey" {} } SubShader { Tags { "RenderType" = "Opaque" "Queue" = "Geometry-1" "PreviewType" = "Skybox" } CGINCLUDE #include "UnityCG.cginc" samplerCUBE _Cube; half4 _Cube_HDR; half4 _Tint; half _Exposure; struct v2f { float4 vertex : SV_POSITION; float3 texcoord : TEXCOORD0; UNITY_VERTEX_OUTPUT_STEREO }; v2f vert(appdata_base v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); o.texcoord = v.vertex.xyz; return o; } fixed4 frag(v2f i) : SV_Target { half4 tex = texCUBE(_Cube, i.texcoord); half3 c = DecodeHDR(tex, _Cube_HDR); c = c * _Tint.rgb * unity_ColorSpaceDouble.rgb; c *= _Exposure; return half4(c, 1); } ENDCG Pass { Cull Front ZWrite On Stencil { Ref [_Mask] Comp Equal } CGPROGRAM #pragma fragmentoption ARB_precision_hint_fastest #pragma vertex vert #pragma fragment frag #pragma target 5.0 #pragma only_renderers d3d11 ENDCG } } }
結果
スカイボックス
オブジェクト
メリット / デメリット
メリットは、窓の領域のみに描かれるので壁がニアクリップで表示されなくなっても向こう側が見えたりしません。空間マッピングを待つ必要もありません。また、マスク値を分ければ複数の異なる世界を窓から見せることが出来ます。
デメリットとしては、背景のオブジェクト全てにステンシルを適用したシェーダを使わなければならない点と、窓オブジェクトそのものがニアクリップで見えなくなってしまうと、背景も同時に見えなくなってしまう点があげられます。窓に近づきたい場合は、窓の外に Cull Front
した箱を置いておくなどの対策が必要になります。
先行事例
HoleLenz
VoxelKei さんの HoleLenz が本日(2017/02/18)より公式の Apps で公開されていてすぐ遊べます!
おそらく方式 2 と同等の方法かなと推測しています。
Portal
ファンメイドの Portal デモです。
おわりに
他にも方法はあると思いますが、取り敢えずこの 2 つがシンプルかなと思いました。ぜひお試しください。