[UE5]ライトのキャストシャドウについて

# 概要
ライトのキャストシャドウについて調査を行いました。

- シャドウに関する制約
    - 何個目のライトまで可能か
    - 有効になるライトの選択ロジック
- 思った通りのシャドウを出すためのベストプラクティス

などをまとめます。
UE5.1で調査を行いましたが、あまり変わっていない部分だと思うのでUE4でも同じような結果ではないかと思います。

# 用語解説

最初にこのページに出てくるなじみの薄い用語の解説をまとめます

## StaticShadow
主にStationaryLight x StaticMeshで扱われるテクスチャに書き込まれるタイプのShadow。

[Stationary Light の影について - だらけ者だらけ](https://darakemonodarake.hatenablog.jp/entry/2015/12/16/UE4/Stationary) の前半(**Static Meshの影について**の項)で触れられている内容

## DynamicShadow
StationaryLight x MovableMesh、もしくはMovableLight x MovableMeshで扱われるShadow。
ライトの種別によって実装は異なります。
- Directional
    - CascadeShadowMap
- Point
    - 影響されるMeshごとにShadow情報を生成し、ShadowMapAtlasに格納する
    - [Stationary Light の影について - だらけ者だらけ](https://darakemonodarake.hatenablog.jp/entry/2015/12/16/UE4/Stationary) の後半（**Movable Meshの影について**の項）で触れられている内容
- Spot
    - Spotが照射される位置をカバーするようなShadowMapがライト毎に1枚生成される

# シャドウやライトに関する制約
## StationaryLightの上限
配置されたStationaryLightは、それぞれの影響範囲が最大で**4つまで**しか**重なる事**が出来ません。5つ目以上はStaticShadowではなく、DynamicShadow扱いになります。これはStaticShadowの情報を1枚のテクスチャに保存するためで、RGBAの4チャンネル分であることから来ています。

重なる事が出来ないだけで、ワールド内で4つ以上のStationaryLightを扱うことは可能ですし、あるStationaryLightから見て4つ以上のライトが重なっている状態は作れます。

### たくさんあるライトのどれが選択される？（StaionaryLight編）
まず、平行光とそれ以外にグループ分けされ、その各グループ内で自分のライト範囲に重なっているライトが多い順に並べ替えられます。平行光のグループから並んだ順の上位から順番にチャンネルを割り当てていきます。
あるライトのチャンネルを割り当てる際に、重なっている相手側のライトを確認して埋まっていないチャンネルを選択していきます。

この時、ソートはStableSortではないので、同じ値（重なる個数）を持つライト同士は順不同です。同じリストであれば同じ結果にはなりますが、１つでもデータが異なれば同じ値の中での順番も入れ替わる可能性があります。
例えば、
> １～９まで9個のライトがあり重なっているライト数が全部同じとします。
> 9個でソートすると4番が先頭に来ました。次に5番を外して残りの8個でソートすると、1番が先頭に来ました

というようなことが起きます。その結果、ライトが一つ無効化されるだけで、選択されるライトがガラッと変わる可能性があります。

![StaticShadow.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/3fa217de-3fc2-0792-5a4b-cd46e5ca13f4.gif)
アニメGIFにしたため、かなり見づらくてすみません。
この動画の奥の方にあるポイントライト群を注視してください。手前のスポットライト群を移動させて重なり具合が変わると、奥のポイントライト群の無効化されるライト（＝❌のついているライト）が変化しています。このように重なっているライトの個数が同じ場合、どのライトが無効化されるかは予測不能です。

### チャンネル選択ロジックの補足
![image-20230523-103843.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/0037e383-d2c4-c36a-78cc-4351b1f10ff5.png)
例えば、上図のように赤線で示した同士が重なっている場合を考えます。この時、AとBのライトの影響範囲には自分を含めて5つのライトが重なっている状態になっていますが、結論から言うと無効化されるライトはありません。

例えば、上図のように赤線で示した同士が重なっている場合を考えます。この時、AとBのライトの影響範囲には自分を含めて5つのライトが重なっている状態になっていますが、結論から言うと無効化されるライトはありません。

ライトの重なっている個数順に並べると **(A=B)>(C=D)>E** という順番でソートされ（=の部分は順不同）、以下の手順でチャンネル割当が行われます。

1. Aのチャンネル割当
    1. まだどのライトにもチャンネルが割り当たって無いので、**チャンネル1**が割り当てられます。
1. Bのチャンネル割当
    1. 重なっているライトA/C/D/Eを確認すると、チャンネル1が占有されているので**チャンネル2**を割り当てます
1. Cのチャンネル割当
    1. 重なっているライトA/B/Dを確認すると、チャンネル1/2が占有されているので、**チャンネル3**を割り当てます
1. Dのチャンネル割当
    1. 重なっているライトA/B/Cを確認すると、チャンネル1/2/3が占有されているので、**チャンネル4**を割り当てます
1. Eのチャンネル割当
    1. 重なっているライトA/Bを確認すると、チャンネル1/2が占有されているので、**チャンネル3**を割り当てます

この結果、 **A=1, B=2, C=3, D=4, E=3** というチャンネル割当がされ、無効化されるライトは発生しません。

### チャンネル選択ロジックの詳細
選択ロジックの全体の流れもまとめました。プログラム寄りの記述になりますが、気になる方は展開して確認してみてください。

<details><summary>選択ロジックの流れ</summary>

[Stationary Light の影について - だらけ者だらけ](https://darakemonodarake.hatenablog.jp/entry/2015/12/16/UE4/Stationary)
より
> どのライトが仲間外れになるのか？　気になる方はULightComponent::ReassignStationaryLightChannels関数の中身を参考にすると良いと思います。

とのことなので、解析してみる。

1. TObjectIteratorでシーン内にあるライトを列挙→ **LightToOverlapMap**へ格納
    1. 指定のWorld内にある＆StationaryLightを抽出
    1. LightingScenarioがある場合は、合致しているかを確認
        1. LightinhScenarioによる無効化されてるライトは除外
    1. AffectWorldやCastShadows(LightFunction含む)、CastStaticShadowsがONかどうかの確認
        1. シーンに影響を与えているかどうかをチェック
1. LightToOverlapMapにあるライト同士の重なりチェックを行う
    1. Lightが持つBounds(SpotならCone、PointならSphere) vs BoundingSphereで重なるかどうかを確認
    1. 重なっている相手の情報をリストに保存
1. LightToOverlapMapをソート（**ここが肝**）
    1. 平行光 or その他ライトで、グループごとに仕分け
    1. グループ内では重なりが多いライトの順にソート
    1. ソートは **StableSortではない** ので、重なり数が同じライト同士は順不同
1. ソートされた順にライトチャンネルを割り当てていく
    1. 重なっている相手側のライトチャンネルを確認して空いている番号を割り当てる
    1. （つまり、あるライトと重なっているライトが3つ以下なら、そのライトは必ず有効になる）
1. 割り当てたチャンネルをPreviewShadowMapChannelに反映（デフォルトはINDEX_NONE）
</details>

### StaionaryLightが無効化された場合

StaionaryLightを扱う場合は、ライティングのビルドが必要になります。ビルドした結果、無効化されるStaionaryLightが検出された場合は、メッセージログに以下のようなログが出ます。
![image-20230522-024732.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/e8eea272-2689-f32a-e85f-e3a17c3a9dd7.png)
次の項目で説明しますが、Forward Renderingの場合はこの状態になったLightについて実質的にShadowを扱えません。

## ForwardRenderingにおけるDynamicShadowの上限

Deferred RenderingのDynamicShadowはLightごとに描画される特性から、GPU負荷さえ許せば上限はありません。

しかし、Forward Renderingの場合はそうはいかず、DynamicShadowの情報も1枚のテクスチャに保存しています。（StationaryLightで扱われるStaticShadowと同じ仕組み）
そのため、RGBAの4チャンネル分で **最大4つまで** しか扱えません。

これもStationaryLightと同じで、重ならければワールド内に4つ以上のライトからDynamicShadowを落とすことは可能です。

## たくさんあるライトのどれが選択される？（DynamicLight編）

この項目もForward RenderingのDynamicShadowについて扱います。

まず、StationaryLightと同じように重なっているライト同士でチャンネルの確認をします。その際、**StationaryLightで選択されたライト**が優先されます。そのため、StaionaryLightで4チャンネル全部を使用済みだとその時点でMovableLightはShadowを扱えません。当然、無効化されたStaionaryLightもShadowは扱えません。

次に、MovableなDirectionalLightはチャンネル1～3だけに保存されるようになっています。（チャンネル4は使われない）
前項と合わせてStaionaryLightが3つ以上重なっている（＝チャンネル1～3が占有済み）と、MovableなDirectionalLightはShadowは扱えません。その際は以下のようなエラーが画面上に表示されます。
![image-20230522-024357.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/10c90d59-c26e-56bd-129d-42c99c24eb04.png)

# ベストプラクティス
以上の制約を踏まえて、意図通りのシャドウイングを行うためのベストプラクティスをまとめます。

## 影を落とすライトを絞る
CastShadowをするライトを選択する際に、ColorやIntensity、Falloffなどは考慮されません。そのため、Intensityが低くて見た目への影響が少ないライトでもCastShadowの対象に選ばれることがあります。
そのようなライトは予めCastShadowをOFFにしておくことをお勧めします。

## 影響範囲が出来るだけ重ならないようにする
ライト自体をできるだけ5つ以上重ねない(4つまでにする)事をお勧めします。

チャンネル選択ロジックの補足で説明した通り、5つ以上重なっても問題ない状況は多々ありますが、ライティングの変更によって意図せず無効化される可能性があります。重なりが4つ以下なら確実に大丈夫なので、

- 基本4つ以下の重なりで構成
- 例外的に5つ以上の場所も考慮する

という方針で進めたほうが、破綻しにくいのではないかと思います。

### ライトの影響範囲を狭める
https://www.docswell.com/s/EpicGamesJapan/5M788K-UE4_UFE2018_UnderstandLightingForEffectiveNPR#p83

このスライドのp83にも書かれている通り、同じ見た目でも影響範囲が異なる設定が可能です。処理負荷の面でも改善が期待できるので、出来るだけ影響範囲（**AttenuationRadius**など）を小さくしましょう。

### 配置の微調整
Stationaryなポイントライトを5つ並べた時を考えます。
|図1|
|:-:|
|![image-20230523-095000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/f60b4d96-57a0-f022-6d0d-dc468b1ee527.png)|

図１の状態だと、赤丸の部分で左端のライト(A)と右端のライト(B)の影響範囲が重なっていることが分かります。その結果、ライトの影響範囲が5つ以上重なる場所が出来ています。そのためライトAは先行するライトにチャンネルを占有されていて、無効化されてしまいました。

|図2|
|:-:|
|![image-20230523-094919.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/d48f82f2-16d5-b2f9-5e03-f2b596ac167d.png)|

そこで、図２のようにライトBを少し移動させて、ライトAと重ならないようにするとライトAとライトBが同じ番号のチャンネルを割り当てることができ、どのライトも無効化されなくなります。

### 壁や床の向こうにも注意
|図3|
|:-:|
|![image-20230523-095719.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/bb70607c-5cf0-f9e4-49ab-9f31cd5f3363.png)|

図３のように、一見3つずつしか重なっていないスポットライトが並んでいる場合でも、無効化されているライトが出ています。

|図4|
|:-:|
|![image-20230523-100416.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431039/8b3cc68b-2f2b-9777-50c2-985d5edf0a41.png)|

ここで設定されているライトは、図４のように地面の下に長い影響範囲を持っています。スポットライトに限らず、ポイントライトでも、地面の下や壁の向こうで影響範囲が交差していれば重なっている判定になります。

その点からも影響範囲(**AttenuationRadius**など)は出来るだけ小さくすることをお勧めします。

:::note warn
スポットライト同士の交差判定は正確ではありません。
本来ならCone対Coneで判定をすべきですが、StationaryLightの場合はCone対Sphereを双方向で判定している状態です。DynamicShadowの場合はCone対Sphereの片方向だけなのでより誤判定しやすくなっています。

そのため、特にねじれの位置にあるような場合は誤って重なっている判定をされる事がありますので、注意してください。
:::

# 補足
## 各ライトタイプごとの影響範囲について
### DirectionalLight
平行光源はワールド全体に影響を及ぼすため、ワールド原点を中心に半径WORLD_MAXのBoundingSphereになります
### PointLight
ライト位置を中心に半径 **AttenuationRadius** の **BoundingSphere** になります
### SpotLight
スポットライトのCone形状(**ライト位置、AttenuationRadius、OuterConeAngle**で定義される)と、それを内包する最小のBoundingSphereを計算して使用します

## GPU負荷について
ベストプラクティスに従って設計していけば、キャストシャドウを有効にしたライトを多数扱えます。しかし、その分GPU負荷も上がります。

Shadow(=Depth)だけとはいえ影響範囲にある全てのMovable/Stationary（＝Static以外）のメッシュを描画します。
逆に言うと、ライトの影響範囲内にStaticに設定したMeshしかなければシャドウに関して追加のGPU負荷は掛からないということになります。

GPU負荷を下げるためには、ライト側の

* Movability
* CastShadowsのON/OFF
    * CastDynamicShadowsのON/OFF

の設定の確認に加えて、メッシュ側の

* Movability

も無駄にStationaryやMovableになっている物がないか、確認すると良さそうです。

## 設定変更時のチャンネル更新タイミング
DynamicShadowのチャンネル更新は即時反映されないパターンがあります。
特にForward Renderingの場合はStationaryLightのチャンネル選択状況にも左右されます。設定を変更してStationaryLightで占有されていたチャンネルが空いても、MovableなLightがチャンネルを占有しない事があります。

その際は、**ライティングのビルド**を行うことで正しい状態になりますので、Lightmapを使っていなくても適宜 **ライティングのビルド**は実行してください

## 参考情報

https://darakemonodarake.hatenablog.jp/entry/2015/12/16/UE4/Stationary

今回の調査をするにあたって、かなり参考になりました。2015年に書かれた記事ですが、UE5になった現在でも通用する内容です。ちなみに、記事が書かれた当時はUE4.10ぐらい？

https://qiita.com/EGJ-Nori_Shinoyama/items/caf3e83521b325770662

ライトのMobilityに関する内容が整理されていて、これも重宝しました。特にLightとObjectのMobilityの組み合わせで、どういうライティングが使われるかの表が調査する過程で役に立ちました。

# あとがき

ライトの重なりについては、なんとなく「4つまで」みたいな認識でしたが、よくよく調べてみるとそうではなかったり、ForwardRenderingを使った場合はDynamicShadowに別の制約があったりと意外と知らないことがありました。
また今回調べた結果、なんとなく調整してライトに❌が付いたらライトを動かす。といった場当たり的な対応から脱出できそうです。

UnrealEngineのライティングもLumenやVirtualShadowMapなど新しい技術がどんどん採用されているので、この記事の内容もすぐに時代遅れになりそうです。
というか、「シーン内のライト全部CastShadowONにできる！」という感じの未来が来てほしいですね。