のりまき日記

Unityなどの活用リファレンスブログ。「こうしたい時どうする」をまとめたい

unity)Splatoonみたいにインクを塗るのを実装してみたい ~デプスシャドウ編~

塗られない様子

はじめに

前回の記事でいい感じにインクを塗ることができました。今回は「遮蔽されたオブジェクトの後ろはインクが塗られない」という機能を追加で実装してみました。

tsururin.hatenablog.com

やること

インクを光と考えると塗られない部分は影になります。プロジェクターにデプスシャドウを実装することで実現してみます。

デプスシャドウに関しては以下の記事がとても参考になりました。 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_TOPUNITY_REVERSED_Zを使ってプラットフォーム間の違いを吸収していますが、完全に正しいかは自信がありません。カメラを介して描画する時はunityがいい感じに吸収してくれますが、今回のように自分で描画する場合いろいろ自分で対応する必要がありました。

o.uv = ComputeScreenPos(mul(mul(_ProjectorVP, unity_ObjectToWorld), v.vertex));

ComputeScreenPosを使っていますが、ComputeScreenPosは内部で_ProjectionParamsを使っているので、これはうまく動かないかもしれません。

おわり

ステンシルっぽい表現も。

スプレーアートみたい

他に参考にした記事・サイト