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

# 背景を一枚絵にしたい！

やってみました↓

https://twitter.com/flankids/status/1693290766414791101

2D背景ｘ3Dキャラの組み合わせは初代プレステのRPGでよく見た気がします。
背景に描画コストを高く割けないけど、広い空間を表現したい・・・という目的だったんじゃないかなーと思います。
![chrono_cross.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/d08db791-85cb-c711-8789-fcce8ced02bf.jpeg)
▲クロノ・クロスの美しい背景

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

# 実装

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

```hlsl: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/987519e3-ec7e-d0bc-f5fd-038fd1aea430.jpeg)
こんな感じのただの白い板として表示されます。まだ影も表示されません。


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

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

```diff_hlsl:TransparentShadowReceive.shader
Tags {
    "RenderType"="Opaque"
+   "LightMode"="ForwardBase"
}
```
`LightMode`に`ForwardBase`を指定して、マテリアルがライトの情報を受け取れるようにします。


```diff_hlsl: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して、ライティングを実装するために必要なマクロを使えるようにします。

```diff_hlsl:TransparentShadowReceive.shader
struct v2f {
-   float4 vertex : SV_POSITION;
+   float4 pos : SV_POSITION;
+   SHADOW_COORDS(0)
};
```
`SHADOW_COORDS`マクロで、引数に応じた`unityShadowCoord4`型のTEXCOORDを定義します。
また、`SV_POSITION`の名前を`pos`に変えておきます。（理由は後述）

```diff_hlsl: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`である必要があります。

```diff_hlsl: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`の値は、シーン内の`Light`の`Strength`の値によって0〜1で変化します。
また、Soft Shadowが有効なときは、影の境目も0〜1で変化します。

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

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

![shadow.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/5e3420b7-379b-ac55-f7ae-a704d9c66b6b.jpeg)

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

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

```diff_hlsl:TransparentShadowReceive.shader
  Tags {
-     "RenderType"="Opaque"
+     "RenderType"="Transparent"
      "LightMode"="ForwardBase"
  }
+ Blend SrcAlpha OneMinusSrcAlpha
```
`RenderType`に`Transparent`を指定して、半透明でレンダリングできるようにします。
また、`Blend SrcAlpha OneMinusSrcAlpha`で後ろの表示との合成タイプをしています。

以下、一般的な合成タイプです。
|タイプ|記述|
|---|---|
|昔ながらの透明|SrcAlpha OneMinusSrcAlpha|
|プリマルチプライドの透明|One OneMinusSrcAlpha|
|追加|One One|
|ソフトな追加|OneMinusDstColor One|
|乗算|DstColor Zero|
|2x 乗算|DstColor SrcColor|

**詳細：[ShaderLab: Blending | Unity マニュアル](https://docs.unity3d.com/ja/2018.4/Manual/SL-Blend.html)**

```diff_hlsl: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/5c44ae5f-f20b-2b82-73df-c90cdcdc5ee6.jpeg)
`_Color`の値で影色と透明度を設定できるため、とりあえず黒にしましょう。
![black_shadow.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/182b162e-4976-4423-6206-ba51f089a458.jpeg)
バッチリですね。

## 背景スプライトの設定

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

しかし、上記シェーダーは影を受けるために`ZWrite`はOffにしていないので、不透明オブジェクトより描画順が後の`Image`などのオブジェクトはPlaneオブジェクトより後ろにある時に描画がスキップされてしまいます。
![background.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/a8c168ad-60da-df0c-c91f-b6c7df9ea581.jpeg)

これについては、背景Imageにマテリアルを用意して`Render Queue`に`Geometry`を指定することで対処しました。
![mat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/78489f5b-9572-6771-6bd5-e03c4474afbc.png)

# シェーダー全文
<details><summary><b>----------------------------------------<br> TransparentShadowReceive.shader（折りたたみ）<br> ------------------------------------------</b></summary><div>

```hlsl: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"
}
```
</div></details>

# 所感

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/10169b17-97c2-4e54-be17-696023feb101.png)
▲from: みんちりえ（ https://min-chi.material.jp/ ）様

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

# 参考記事

* [【UnityShader】オブジェクトの影を受け取る #108 | 知識0からのUnityShader勉強](https://soramamenatan.hatenablog.com/entry/2021/06/29/082351)
* [【Unity】【シェーダ】Forward Renderingで複数のライトを取り扱う | LIGHT11](https://light11.hatenadiary.com/entry/2018/07/12/002823)
* [シェーダー入門したくないけど書いてみたい その6 ライティング編 | 波打際のブログさん](https://alfa.hatenablog.jp/entry/2015/08/16/195933)