LoginSignup
23
21

[Unity]透明な板ポリゴンに影を落として2D背景に3Dキャラを馴染ませる

Last updated at Posted at 2023-08-24

背景を一枚絵にしたい!

やってみました↓

2D背景x3Dキャラの組み合わせは初代プレステのRPGでよく見た気がします。
背景に描画コストを高く割けないけど、広い空間を表現したい・・・という目的だったんじゃないかなーと思います。
chrono_cross.jpg
▲クロノ・クロスの美しい背景

マシンスペックが上がり、3Dモデルの製作ツールも充実してきた現代では見る機会が減りましたが、小規模開発では製作コストを削減するなどのメリットもありそうだなと思い、作ってみました。

実装

まず、新規作成したUnlitシェーダーから不要な処理を削って色を指定するだけのUnlitシェーダーを用意します。
下記シェーダーをベースに処理を書いていきます。

TransparentShadowReceive.shader
Shader "Custom/TransparentShadowReceive" {
    Properties {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader {
        Tags {
            "RenderType"="Opaque"
        }

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 vertex : SV_POSITION;
            };

            fixed4 _Color;

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = _Color;
                return col;
            }
            ENDCG
        }
    }
}

これをPlaneオブジェクトのマテリアルに反映します。
no_shadow.jpg
こんな感じのただの白い板として表示されます。まだ影も表示されません。

普通の影を受けるシェーダーにする

次に、影を受けるための最低限の処理を追加していきます。

TransparentShadowReceive.shader
Tags {
    "RenderType"="Opaque"
+   "LightMode"="ForwardBase"
}

LightModeForwardBaseを指定して、マテリアルがライトの情報を受け取れるようにします。

TransparentShadowReceive.shader
Pass {
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
+   #pragma multi_compile_fwdbase


    #include "UnityCG.cginc"
+   #include "AutoLight.cginc"

multi_compile_fwdbaseのシェーダーバリアントを指定して、フォワードレンダリングのベース処理をコンパイルします。
また、AutoLight.cgincをincludeして、ライティングを実装するために必要なマクロを使えるようにします。

TransparentShadowReceive.shader
struct v2f {
-   float4 vertex : SV_POSITION;
+   float4 pos : SV_POSITION;
+   SHADOW_COORDS(0)
};

SHADOW_COORDSマクロで、引数に応じたunityShadowCoord4型のTEXCOORDを定義します。
また、SV_POSITIONの名前をposに変えておきます。(理由は後述)

TransparentShadowReceive.shader
v2f vert (appdata v) {
    v2f o;
-   o.vertex = UnityObjectToClipPos(v.vertex);
+   o.pos = UnityObjectToClipPos(v.vertex);
+   TRANSFER_SHADOW(o);
    return o;
}

TRANSFER_SHADOWマクロでSHADOW_COORDS内で定義した変数にスクリーンスペースの座標を入れています。
この中の処理でSV_POSITIONの変数名がposが決め打ちとなっているので、v2f構造体のSV_POSITIONの名前はposである必要があります。

TransparentShadowReceive.shader
fixed4 frag (v2f i) : SV_Target {
    fixed4 col = _Color;
+   col.rgb *= LIGHT_ATTENUATION(i);
    return col;
}

LIGHT_ATTENUATIONマクロでライトの減衰率を計算します。
影が落ちているところには1が、そうでないところには0が返ってくるため、fragの返り値にそのまま乗算しています。
<備考>
LIGHT_ATTENUATIONの値は、シーン内のLightStrengthの値によって0〜1で変化します。
また、Soft Shadowが有効なときは、影の境目も0〜1で変化します。

TransparentShadowReceive.shader
            }
            ENDCG
        }
    }
+   Fallback "Diffuse"
}

ライティングに関する実装で、今回は映り込む影のこと以外は何もしないので、FallbackでDiffuseシェーダーの内容に任せています。

これでPlaneオブジェクトに影が投影されるようになりました。

shadow.jpg

影だけが表示されるシェーダーにする

影が表示されていないところを透明にして、影も透明度が指定できるようにします。

TransparentShadowReceive.shader
  Tags {
-     "RenderType"="Opaque"
+     "RenderType"="Transparent"
      "LightMode"="ForwardBase"
  }
+ Blend SrcAlpha OneMinusSrcAlpha

RenderTypeTransparentを指定して、半透明でレンダリングできるようにします。
また、Blend SrcAlpha OneMinusSrcAlphaで後ろの表示との合成タイプをしています。

以下、一般的な合成タイプです。

タイプ 記述
昔ながらの透明 SrcAlpha OneMinusSrcAlpha
プリマルチプライドの透明 One OneMinusSrcAlpha
追加 One One
ソフトな追加 OneMinusDstColor One
乗算 DstColor Zero
2x 乗算 DstColor SrcColor

詳細:ShaderLab: Blending | Unity マニュアル

TransparentShadowReceive.shader
fixed4 frag (v2f i) : SV_Target {
    fixed4 col = _Color;
-   col.rgb *= LIGHT_ATTENUATION(i);
+   col.a *= 1 - LIGHT_ATTENUATION(i);
    return col;
}

ライトの減衰率を透明度に反映しています。
ライトの減衰率は影が濃い部分ほど0に近づくため、0〜1の範囲を反転させています。

これで影の部分のみが表示されるようになりました!
white_shadow.jpg
_Colorの値で影色と透明度を設定できるため、とりあえず黒にしましょう。
black_shadow.jpg
バッチリですね。

背景スプライトの設定

画角やライティングの影響を受けずに背景スプライトを表示するのは、Render ModeScreen Space - CameraにしたキャンバスにImageコンポーネントを置いて表示されるのが簡単かなと思います。

しかし、上記シェーダーは影を受けるためにZWriteはOffにしていないので、不透明オブジェクトより描画順が後のImageなどのオブジェクトはPlaneオブジェクトより後ろにある時に描画がスキップされてしまいます。
background.jpg

これについては、背景Imageにマテリアルを用意してRender QueueGeometryを指定することで対処しました。
mat.png

シェーダー全文

----------------------------------------
TransparentShadowReceive.shader(折りたたみ)
------------------------------------------
TransparentShadowReceive.shader
Shader "Custom/TransparentShadowReceive" {
    Properties {
        _Color ("Color", Color) = (0, 0, 0, 1)
    }
    SubShader {
        Tags {
            "RenderType"="Transparent"
            "LightMode"="ForwardBase"
        }
        Blend SrcAlpha OneMinusSrcAlpha

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase

            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                SHADOW_COORDS(0)
            };

            fixed4 _Color;

            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = _Color;
                col.a *= 1 - LIGHT_ATTENUATION(i);
                return col;
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

所感

ノベルゲームのフリー素材背景は、このテクニックを使って3Dのキャラクターやオブジェクトを合成するのに適したアングルのものが多い印象があります。
夕方や夜景など、ライティング環境もさまざまなので、こだわりがいがありそう。

image.png
▲from: みんちりえ( https://min-chi.material.jp/ )様

影だけでなく、床に対してキャラの映り込みなどもやりたくなってきますね・・・!
発想としては古典的なテクニックですが、現代の表現と組み合わせればユニークな表現もできそうだし、負荷や制作コストを下げるに収まらず活かしがいがありそうなネタでした。

参考記事

23
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
21