Unityでステルスシェーダーを作るには

某ステルスゲームに登場するようなステルス迷彩をシェーダーで表現するには、ただの透明マテリアルでは不十分だと感じる人は多いと思います。Standardマテリアルを半透明にしても、透過部分がそのまま色で描画されるため、質感や物体感が非常に薄く感じるためです。

今回は、そんなステルス迷彩チックなシェーダーの実装について解説していこうと思います。

GrabPassで画面をキャプチャする

透明表現を扱うため、描画時点でのレンダリング結果をキャッシュする必要があります。そのためには、GrabPassというパスを使用します。具体的な使い方は以下の通りです。

SubShader
{
    Tags { "RenderType" = "Transparent" } //透明表現なのでTransparentを指定
    LOD 100

    GrabPass { "_GrabPassTexture" } //この地点のレンダリング結果をキャッシュ

    Pass
    {
        sampler2D _GrabPassTexture;
        ----------描画処理---------
    }
}

なお、このGrabPassには二通りの書き方があり、マニュアルによると以下のように説明されています。

GrabPass { }現在のレンダリング画面をテクスチャ内に取得し、後続のパスで _GrabTexture でアクセスすることができます。この Grap Pass は、このテクスチャを使用する各オブジェクトの描画ごとに実行されるため、処理が重たくなる可能性があります
GrabPass { "TextureName" }          現在のレンダリング画面をテクスチャ内に取得しますが、同じテクスチャを使用するはじめのオブジェクトにのみ実施します。テクスチャは、後続のパスで指定のテクスチャ名でアクセスすることができます。シーンで Grab Pass を使用するオブジェクトが複数ある場合はこちらのほうが処理が軽くなります。
https://docs.unity3d.com/ja/2019.4/Manual/SL-GrabPass.html

キャプチャした画像を投影する

キャプチャした画像を、今度はオブジェクトに投影していきます。投影に関して重要なポイントは、ComputeScreenPostex2Dprojです。

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex); //頂点をMVP行列変換
    o.screenPosition = ComputeScreenPos(o.vertex); //クリップ座標からスクリーン座標を計算
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    //スクリーン座標からサンプリングするのでtex2Dprojを使用
    float4 color = tex2Dproj(_GrabPassTexture, i.screenPosition);
    return color; 
}

画像だとすこぶる分かりにくいのですが、以下のように、まるでなにもないかのようなライティングになると思います。画面をそのまま投影しているため、ある意味正しい挙動です。

リムを歪ませる

キャプチャした画面をそのまま投影しただけだと、完全に同化してしまうことがわかりました。そのため、すこし色を歪ませることによって、物体感を演出してあげるいいでしょう。今回は、物が屈折しているように見せてみます。

屈折と書きましたが、正確には正しくありません。今回は単純にuvに加算しているため、屈折を正しく計算しているわけではないので注意してください。ただ、正確に計算しなくても、それっぽい表現ができていることがわかると思います。

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex); //頂点をMVP行列変換
    o.screenPosition = ComputeScreenPos(o.vertex); //クリップ座標からスクリーン座標を計算
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; //頂点をワールド座標でキャッシュ
    o.normal = UnityObjectToWorldNormal(v.normal); //法線をワールド座標系に変換
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    float3 toEye = normalize(_WorldSpaceCameraPos - i.worldPos); //視線ベクトルを計算
    float rim = dot(i.normal, toEye); //視線ベクトルと法線の内積からリム強度を計算
    rim = pow(rim, _RimLevel); //リム強度を調整

    //スクリーン座標からサンプリングするのでtex2Dprojを使用
    //リム強度が低いほどサンプリング位置をシフトさせない
    float4 color = tex2Dproj(_GrabPassTexture, i.screenPosition + (1 - rim) * _ShiftLevel);
    
    return color;
}

背景との同化を防ぐ

ここまででも、十分にステルス感が表現できているかと思いますが、ゲームで実際に使用するには少々勝手が悪い場合があります。それは、背景色が完全に単色だと、歪みによる色変化が全く見えず、視認性が著しく悪くなってしまうという点です。

そのため、ほんの少し色を混ぜたり、リムに色を乗せて輪郭を強調したほうがいいでしょう。今回は、色のほかに、リムにもすこし色を乗せて輪郭を強調してあります。

 return color * _MixColor * rim; //背景と完全同化しないように色を混ぜる

シェーダー全体

ここまでに説明してきたことをまとめると、以下のようになります。

Shader "CAGraphicsAcademy/Stealth"
{
    Properties
    {
        _MixColor("Mix Color", Color) = (1, 1, 1, 1) 
        _ShiftLevel ("Shift", Range(0.0, 1.0)) = 0
        _RimLevel ("RimLevel", Range(0.0, 10.0)) = 0
    }
    SubShader
    {
        Tags { "RenderType" = "Transparent" } //透明表現なのでTransparentを指定
        LOD 100

        GrabPass { "_GrabPassTexture" } //この地点のレンダリング結果をキャッシュ

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 screenPosition : TEXCOORD1;
                float3 normal: TEXCOORD2;
                float3 worldPos : TEXCOORD3;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _GrabPassTexture; //GrabPassで保存されたテクスチャの格納先
            float _ShiftLevel;
            float _RimLevel;
            float4 _MixColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex); //頂点をMVP行列変換
                o.screenPosition = ComputeScreenPos(o.vertex); //クリップ座標からスクリーン座標を計算
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; //頂点をワールド座標でキャッシュ
                o.normal = UnityObjectToWorldNormal(v.normal); //法線をワールド座標系に変換
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 toEye = normalize(_WorldSpaceCameraPos - i.worldPos); //視線ベクトルを計算
                float rim = dot(i.normal, toEye); //視線ベクトルと法線の内積からリム強度を計算
                rim = pow(rim, _RimLevel); //リム強度を調整

                //スクリーン座標からサンプリングするのでtex2Dprojを使用
                //リム強度が低いほどサンプリング位置をシフトさせない
                float4 color = tex2Dproj(_GrabPassTexture, i.screenPosition + (1 - rim) * _ShiftLevel);
                
                return color * _MixColor * rim; //背景と完全同化しないように色を混ぜる
            }
            ENDCG
        }
    }
}

まとめ

ステルスシェーダーを解説してきましたが、GrabPass以外はほとんど難しくないかと思います。ステルスと題しましたが、どちらかというとガラスにも近いです。このシェーダーをさらにガラスっぽく見せるためには、鏡面反射を追記してあげるといいでしょう。

鏡面反射については、以前に解説記事を書いたので、もし気になる方はこちらをぜひご覧になってみてください。

返信を残す

メールアドレスが公開されることはありません。