はじめに
前回の記事でいい感じにインクを塗ることができました。今回は「遮蔽されたオブジェクトの後ろはインクが塗られない」という機能を追加で実装してみました。
やること
インクを光と考えると塗られない部分は影になります。プロジェクターにデプスシャドウを実装することで実現してみます。
デプスシャドウに関しては以下の記事がとても参考になりました。 shibuya24.info
プロジェクターからみたデプスを取得する
前回プロジェクターを自作した際に、視錐台計算用に描画しないカメラを使っていました。そのカメラを使って、プロジェクターから見たデプスをレンダーテクスチャに描画するようにします。
private void Start() { ... m_Camera.depthTextureMode = DepthTextureMode.Depth; m_Camera.targetTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.Depth); m_Material.SetTexture("_DepthTex", m_Camera.targetTexture); }
note
m_Camera.targetTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.Depth);
デプスを書き込むテクスチャのフォーマットです。
m_Material.SetTexture("_DepthTex", m_Camera.targetTexture);
プロジェクター用のマテリアルに渡して、描画時にデプスを比較するようにします。
深度を比較して塗らないようにする
プロジェクター用のシェーダーを少し変更します。
Shader "Unlit/Projector" { Properties { _InkTex ("InkTex", 2D) = "white" {} _DepthTex ("DepthTex", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } ZWrite Off ColorMask RGB // アルファブレンド Blend SrcAlpha OneMinusSrcAlpha Offset -1, -1 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float2 uv2 : TEXCOORD1; }; struct v2f { float4 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _InkTex; sampler2D _DepthTex; float4x4 _ProjectorVP; float4 projector_LightmapST; v2f vert (appdata v) { v2f o; // ライトマップのuvに変換 v.uv2 = v.uv2.xy * projector_LightmapST.xy + projector_LightmapST.zw; #if UNITY_UV_STARTS_AT_TOP v.uv2.y = 1.0 - v.uv2.y; #endif // 頂点をuv座標にする o.vertex = float4(v.uv2 * 2.0 - 1.0, 0.0, 1.0); o.uv = ComputeScreenPos(mul(mul(_ProjectorVP, unity_ObjectToWorld), v.vertex)); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2Dproj(_InkTex, UNITY_PROJ_COORD(i.uv)); float4 uv = i.uv / i.uv.w; #if UNITY_UV_STARTS_AT_TOP uv.y = 1.0 - uv.y; #endif fixed depth = tex2D(_DepthTex, uv).r; #if defined(UNITY_REVERSED_Z) clip(uv.z - depth + .0001); #else clip(depth - uv.z + .0001); #endif fixed3 isOut = step((uv - 0.5) * sign(uv), 0.5); float alpha = isOut.x * isOut.y * isOut.z; return col * alpha; } ENDCG } } }
note
Properties { _InkTex ("InkTex", 2D) = "white" {} _DepthTex ("DepthTex", 2D) = "white" {} } ... sampler2D _DepthTex;
デプス画像を追加します。
float4 uv = i.uv / i.uv.w; #if UNITY_UV_STARTS_AT_TOP uv.y = 1.0 - uv.y; #endif fixed depth = tex2D(_DepthTex, uv).r;
tex2Dで使えるuv(0〜1)にするために、ComputeScreenPosで作ったuv
をwで割っています。この時uv.z
がデプスの値になっています。
fixed depth = tex2D(_DepthTex, uv).r; #if defined(UNITY_REVERSED_Z) clip(uv.z - depth + .0001); #else clip(depth - uv.z + .0001); #endif
プロジェクターから見たデプスと現在描画しようとしているピクセルのデプスを比べて、奥にある場合はclipして描画しないようにしています。また、Zファイティングを抑制するために+ .0001
しています。(同じオブジェクトを描画するので確実にZファイティングするため。)
これだけで完成です。
memo
UNITY_UV_STARTS_AT_TOP
やUNITY_REVERSED_Z
を使ってプラットフォーム間の違いを吸収していますが、完全に正しいかは自信がありません。カメラを介して描画する時はunityがいい感じに吸収してくれますが、今回のように自分で描画する場合いろいろ自分で対応する必要がありました。
o.uv = ComputeScreenPos(mul(mul(_ProjectorVP, unity_ObjectToWorld), v.vertex));
ComputeScreenPos
を使っていますが、ComputeScreenPos
は内部で_ProjectionParams
を使っているので、これはうまく動かないかもしれません。
おわり
ステンシルっぽい表現も。