Unlitを活用しよう

### [Unreal Engine (UE) Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ue) の一発目の記事です！

今年もこの季節がやってまいりましたね。おかずさんいつもありがとうございます！
さてさて今回の記事は、
表題の通りUnlit を有効活用しましょうという内容です。
ではいきましょう。

# Unlitを使う目的

私は近年、HDモバイルゲームの背景制作に携わってきました。
モバイルではマップのどこを見てもかなり負荷がカツカツで、できる限りの処理負荷対策を行わねばなりません。

負荷対策の一環として、一番負荷の軽い『Unlit』という物が使えないか目星を付けますが、
のっぺりした絵になってしまい悪目立ちする結果に……。

![SS_No-1547.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/5ed87a79-7a0a-a633-96ea-72f02df8cb72.gif)

そこでUnlitをカスタマイズして、見た目がDefault Litからかけ離れない見た目を担保しつつも、
負荷を抑える模索をしばらくしていました。
そんな表現方法を模索し続けた結果をここに残したいと思います。

#### ☆記事の内容
* 主要どころではないアセットをUnlitに置き換えて負荷対策
* Unlitをカスタマイズして見栄えをある程度担保する
* NormalMapを併用してマスクを作成する (やや中辛)

# Unlit とは

<!-- open属性なし -->
<details><summary>Unlitの基本説明。気になる方はここをクリック！　</summary>

[Unlit（非ライティング）](https://dev.epicgames.com/documentation/ja-jp/unreal-engine/shading-models-in-unreal-engine#:~:text=%E3%82%8C%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82-,%E9%9D%9E%E3%83%A9%E3%82%A4%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0,-Unlit%20%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%20%E3%83%A2%E3%83%87%E3%83%AB)はざっくり言うと、UEの中で一番処理負荷が軽いマテリアルの名称です。
様々な処理が省略されているため用途が限られますが、その分 描画負荷が軽い特徴があります。

::: note warn
#### 最大の欠点としてはCast Shadowを受けることができません。
![SS_No-1516.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/5ced3d38-7ee3-d084-899b-64a62afa4436.jpeg)


日向にあるものと日陰にある物が同じ色味になるので、
どちらにも必要な場合はDiffuseに輝度調整用のパラメーターを作っておき、
マテリアルインスタンス側で輝度を調整したものを日向、日陰にそれぞれ適用する面倒さがあります。

それを差し置いても負荷的に有利な点も多く、使って損はありません。
:::

UEでマテリアルを作成すると『Default Lit』というマテリアルがまず用意されます。
それを『Unlit』に変更する所から、このお話のスタートです。
![SS_No-1532.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/e742e230-5b96-874e-f6d2-5f4dae528117.jpeg)


使用できる出力はエミッシブカラーのみと、かなり攻めたマテリアルのようです。
![SS_No-1534.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/c76cb3fe-419f-e7da-d4c8-8763acb308ed.jpeg)

この特徴からUnlitがよく使われるシーンとしては、
のっぺりしていても自然でエミッシブさえあればOKなもの――大抵は電球などのライト類で使用されます。
ブルームが入って輪郭がボヤけてしまえば形状ののっぺりさも隠せますから、軽いマテリアルで済ますのが効率的と言えます。
![SS_No-1536.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/2ce1c7a8-5ca5-4824-02dd-d9434b6b5f6e.jpeg)


テクスチャも挿せるようです。
ではエミッシブカラーにテクスチャを繋いだ出力結果を、電球以外のアセットに出してみましょう。
![SS_No-1533.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/f225e0fc-b733-9487-cc72-a2a382ec0e85.jpeg)


このような感じで、陰影の無いのっぺりした物が描画されました。
ん～…通常使用するには厳しい感じです。Default Litと比較すると一目瞭然です。

単体で見ている分にはこれでも使い所がありそうな感じがしますが、
何個も重ねると、奥行きが全く感じられない見た目でかなり厳しそうです。
![SS_No-1535.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/14054087-a4bc-5f9e-e652-c5c94ddc77b8.jpeg)


DefaultLitの質感を完璧でなくとも保持したままUnlitに変更することはできないでしょうか？

</details>

# Unliをカスタマイズする！
のっぺりした見た目になっちゃうUnlitを使用するにあたり改良したい点を考えてみます。
兎にも角にものっぺりを解決したいところですね。

1. 陰影が入るようにしたい
2. ラフネス光沢が欲しい

この2点をUnlitに追加すれば、見た目の違和感を解消できそうな感じです。




## :first_quarter_moon: 1. 陰影を追加
![SS_No-1538.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/a4221699-4a0e-dd23-392b-8a276916b45d.jpeg)
上記サンプルを見ると既にいい感じです。Unlitでも陰影のついた見た目を表現することが可能です。
こちらの作成手順を解説します。

陰影を作る時に大事な点としては、"落ち影(Cast Shadow)と陰影の方向が同一である" ことです。
ここがチグハグになるとすごく違和感が出ますから、このルールを守りつつノードを組んでみます。

##### ババン！
![SS_No-1519.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/c4a950f4-5cad-147d-2fb5-1ba974c2d697.jpeg)
この組み方で陰影を追加することができます。
影を追加するのが、終端ノードのMultiplyに繋がっている下半分のノード群です。

もう少し詳しく見てみましょう。


#### ・陰影方向の取得
陰影を作るには影のマスクを作成するのが簡単そうです。
マスクを作るにあたっては先程も書いたように、
"落ち影(Cast Shadow)と陰影の方向が同一である" 点が大事です。

そのためには DirectionalLight の方向を取得して、
マスクがそれに沿って動くようにするのが早そうです。
これなら太陽が動く時間変化のあるマップでも対応できそうです。
![LightDirection.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/c8040243-76ac-b821-00b1-720ea439c1d1.gif)

DirectionalLight の方向を取得して一致させるためには、
『Sky Atmosphere Light Direction』のノードを使います。


そこに『VertexNormalWS』で各モデルとの内積(Dot)を取得することで陰影を表現できます。
![SS_No-1541.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/21561ee4-3506-3a12-3b4e-313e05f1a274.jpeg)
これだけで陰影をつけることが出来ますが、これだとマスク境界が中心でピシッと入ってしまいます。
Multiplyしたらど真ん中を境にモデル半分が真っ黒になってしまいます:scream:
次に、この境界線をなじませましょう。


#### ・陰影のなじませ方法
これを解消するために0.5をAdd(足すこと)で影の明るさを底上げしています。
その一個手前にあるパラメーター化しているMultiplyの0.5で影の境界のにじみを変更できるようにしています。
これを１にすると真ん中で真っ黒になります。境界の強さは配置場所によって変えたいこともあるでしょうから、私はここをパラメーター化することにしています。
![SS_No-1542.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/4a86c77e-319f-e8fb-7536-6aa6feae965f.jpeg)

:::note info
なお、ノーマルマップがあるモデルならVertexColorをノーマルマップに変更することでより細かい陰影をつけることができます。
ただしその分重くなりますので、必要なモデル、不要なモデルはよく考えて使い分けるべきでしょう。
![SS_No-1543.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/c50c9a0b-a1e5-2fe4-4fcb-25526746936c.jpeg)
:::


標準Unlitのモデルを複数並べただけのものと、
陰影付きUnlitで並べたものを見比べてみましょう。右側はどちらもDefault Litです。

![SS_No-1545.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/4b063587-d996-f618-6836-619eea4973c5.jpeg)

* 上：テクスチャを繋いだだけのもの
* 下：陰影を追加したカスタムUnlit
奥行きがあり、既にいい感じです。

---

### :bar_chart: 処理負荷の比較
気になる処理負荷です。せっかくUnlitにしたのですから、DefaultLitより軽くなってもらわないと困ります。
このテクはモバイルゲームでも威力を発揮するものですから、SM5とiOSの環境で比較してみましょう。

![SS_No-1539.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/0b267f76-72e1-1c61-c65c-a3518acc4185.jpeg)


#### ええやん！
Unlitはやはり負荷対策に効果があるようです。
SM5だと落ちてはいますがそこまでの差はなさそうですが、特筆すべきはモバイルシェーダーです。
特にモバイル環境ではラフネスの負荷がかなり重いので、Unlitにすることで非マスク部分が黄緑色にまで落ちています。
なにより顕著なのが白い部分がないという点。

:::note
白色部分は処理負荷がMAXまで行っているので、目立つくらいあるなら修正するようエンジニアから言わることが多いです。
マスター前とかに修正が入ると大変なので、先に潰しておきましょう。
Maskedマテリアル＋ラフネスありは比較的すぐに白色に到達します。

:::

![SS_No-1544.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/95e29bb8-1ca6-506d-2f74-78f25a5a532f.jpeg)

次に、先程の複数配置の方で見るとより顕著です。
右側が遠景でずらーーーーっと配置されている所を想像すると、おそらくエンジニアはぶっ倒れます。
Unlitにしつつも陰影を追加することで、処理負荷を落としながらグラフィッククオリティをある程度維持することができます。
モバイルに限らず、こういう白くなった部分が目立つ場合はどんなハードであってもケアしてあげるべきです。

:::note
### 遠景負荷対策のもっと突っ込んだお話（ある程度の経験者向け）
このテクはPS5などの高スペックでも等しく必要な場面があります。

高スペックの場合、グラフィッククオリティの点からDefault Litで作らないといけないシチュエーションも多くあると思います。
その場合、森林ステージなどのアルファ抜き植物が大量にあるようなレベルを作っていると、
あっという間に遠方が真っ白になってしまうことが予測されます。

Maskedマテリアルで白色が目立つ場合は、
LOD2以降は陰影付きのUnlitに変更する等、プロジェクトで仕様を設けましょう。

クオリティ優先で近い部分はもう白色が多少出ることを許容しつつ、
遠景ではきちんと負荷を落としてあげればかなりの負荷対策が出来ます。
~~エンジニアへの「こいつらちゃんと対策やってんなぁ～」というアピールにもなります~~

UnlitでCastShadowが落ちないことが大きな欠点となる場合は、
Default LitにラフネスをカットするためのStatic Switch Parameterを追加してください。
ラフネスをカットすると共にマスクテクスチャ自体を無効化することでドローコールも減らします。
マテリアルインスタンス側でスイッチをTrueにした物を用意し、LODに適用します。

LODスロットを用意したせいでドローコールが増えてしまった分、
スイッチで使用テクスチャを間引いて少しでも相殺するイメージです。

距離によるラフネスカットもifやDistance等を使えばノード上で作れそうですが、
これだとマスクテクスチャは生き続けたままになる上、常にGPUに距離計算をさせ続けることになるのであまりオススメしません。

![SS_No-1550.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/c2d3d250-eab6-f4d9-7a89-9554de67ccca.jpeg)

Static Switch Parameterは、[昔からUEに馴染みのある人ほどトラウマ](https://qiita.com/EGJ-Nori_Shinoyama/items/743af2637cff878f098b)があって使いにくいですが、前やってた案件ではバリバリ使っていました。
エンジニアから問題が報告されたことは無かったので、もはや大丈夫なのでしょう:thumbsup_tone1:


:::


## :sparkles: 2. ラフネス反射を追加

次は、擬似的なラフネス反射を追加します。
先ほどの球体で使っていたテクスチャと同じものを立方体に割り当ててみました。

まず下記のGIFをご覧ください。
どちらかがUnlitで、もう片方がDefault Litです。
違いは分かりますか？
![MyProject - Unreal Editor 2024-11-26 23-28-45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/bbb9e88d-5886-3989-faf1-6faffa1c388f.gif)


左がUnlitで、右がDefault Litです。
Unlitでもラフネスのシミュレートはここまで出来るようです。
あくまで疑似ラフネスなので多少変な部分も発生しますが、概ね問題なく使えます。
![SS_No-1567.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/b79198b5-a722-eed4-2b67-897f1300d35f.jpeg)


疑似ラフネスでやりたいことを考えてみます。
* DirectionalLightと同じ向きに反射光があること
* 反射色を変更できるようにする
* 実際のラフネスと同様、ツヤ感の範囲を変えられるようにする

これらは最低限欲しいですね。
ノードで構成すると下記のような感じです。
![SS_No-1568.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/4958f32d-2281-3357-077a-cc36dbd3bd69.jpeg)


構成を一つずつ見ていきましょう。

### ・DirectionalLightと同じ向きに反射光があること

これは先程使った『Sky Atmosphere Light Direction』を再利用します。
ラフネスの場合、カメラに合わせてツヤが変化する必要があるので『Camera Vector』をAddします。
次に、その結果でノーマルマップとの内積（Dot)を計算します。
これでほぼ正しくラフネスのシミュレートを行うことができました。
![SS_No-1569.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/a1132a44-83fb-d7ff-959a-3bf337a00e69.jpeg)



### ・反射色を変更できるようにする

3floatをパラメーター化しておき、AddでDiffuseに色を加算します。
Diffuseそのまま出しと、ツヤが加算されたものをLerpで繋ぐ算段です。
![SS_No-1570.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/8332fbfe-e737-22cb-e6bf-ee2280b8b4e3.jpeg)

::: note warn
反射色はDirectionalLightのカラーをgetすれば作業効率をあげられますが、
Unlitを使う目的は処理負荷対策です。
せっかく軽くしているので、何らかの情報を引っ張ってくるのは極力少なくしたい所です。
私だったらLightのカラーくらいは手入力でやっちゃうかな～という温度感です。
角度は流石に面倒すぎるので『Sky Atmosphere Light Direction』でgetしちゃいました。
ただ、時間変化のあるマップの場合は反射色もgetしてくるのが良さそうです。
:::



### ・実際のラフネスと同様、ツヤ感の範囲を変えられるようにする

* Areaで境界線の絞りをパラメーター化
* Contrastでコントラストをパラメーター化
![SS_No-1563.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/9e1741f3-001b-e3f9-e50b-bc706ac85db7.jpeg)


---
### :bar_chart: 処理負荷の比較
またまた気になる処理負荷です。
先程の項目では、モバイル環境においてはラフネスがネックになる旨を説明しましたが、
疑似ラフネスであればどうでしょうか？
![SS_No-1565.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/9de71c50-0313-302a-4fbd-478f910a783d.jpeg)

SM5は微妙な差ですがモバイル環境は見るからに軽そうです。
あくまで疑似ではありますが、上のgifくらいシュミレートできており、
この緑色が実現できるのであれば使い所が沢山ありそうです。



これでUnlitに追加したい項目はほぼ完成です。
この次の項目はオマケ的なお話となります。

---

# NormalMapを併用してマスクを作成する (やや中辛)

![SS_No-1553.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/dec578cb-9c81-6146-dc3a-0ed7fa868056.jpeg)
ノーマルマップは一見、青っぽい色をしています。
色だけ見るとBチャンネルに秘密がありそうな気がしますが、実はBチャンネルの情報はUEでは破棄されています。

ノーマルマップは縦方向と横方向の陰影がRとGチャンネルに保存されており、その２チャンネルの情報を読み取れば180°の凹凸を実現できるため事足りるのです。


UEの場合、圧縮設定が自動的に『Normalmap (BC5)』に設定されます。
この『BC5』の時点でBchの破棄が決定付けられてしまいます。
![SS_No-1551.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/0542074c-74d9-c55c-b9a7-7aca80600e0e.jpeg)

じゃあBchには何が保存されているのか？
Bchには凹凸の深度情報が保存されています。

深度情報……！　ということはこれをHeightMap代わりに使えばドローコールの節約が出来そうな感じです！！！
ですが実装時にはBchが破棄されているため直接引っ張ってくることができなさそうです……。

Bchを直に持ってくることはできませんが、同様の結果を出力できるノードがあります。
![SS_No-1555.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/dd9944f7-3ca0-c8b0-0831-b5a764e6ec16.jpeg)

『DeriveNormalZ』というノードを噛ましてComponentMaskでBのみ出力することで、
ノーマルマップのBchと同一結果を出力することができます。
ノードの入力値に『intXY』とある通り、RchとGchから生成することができます。

この出力結果を元に、MultiplyとContrastで色差を絞ってあげると凹凸に沿ったマスクを生成することができます。
![SS_No-1558.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/7a342632-361a-9af2-507a-1c72c12b3ce0.jpeg)


HeightMapほど正確ではありませんが、海の白波が立つエリアを指定したり、意外と役立ちますので活用してみてください。
海のはるか遠方で立つ白波や、遠景の雪山の冠雪などはこれで十分いい感じになります。
![ChangeColorforNormal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/383f89d9-c561-2671-0c78-7c064cfdb549.gif)

:::note
今回の例ではカラーの上乗せとして使っていますが、
ラフネスの差を出すマスクとしての使い勝手もなかなか良い感じです！

ラフネスマップがない時、Diffuseを反転してラフネスとする手がよく使われますが、
ノーマルマップの高さから作ったラフネスはまた違った雰囲気に仕上がります。
ノイズが少なめで、ぼけ気味のグラデーションマスクが山～谷にかけて入ります。

頭頂部分のツヤを強化したい時や、AOとして使って谷底部分を暗くしたい時にも使える可能性を秘めています。
:::

これはUnlitに限らず使用できるので、
HeightMapやマスクテクスチャのドローコールを捻出できない場合は試してみてください:wink:

:::note alert
ノーマルマップやマスクテクスチャのアルファチャンネルにHeightMapを入れておく案は非推奨です。
アルファ付きテクスチャはメモリを２倍占有します。

それなら別テクスチャを用意した方がRGBチャンネルの３つ使える分、有効的です。
アルファに入れるのはなしにしましょう。
:::

:::note warn
## ご存じの方がいらっしゃれば…
私は過去、Bchが破棄されることを存ぜず、
直にBchから引っ張ってきたものでマスクを生成していました。
![SS_No-1559.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/1a26e89e-c666-7a85-1741-de2adb2a60c2.jpeg)


UEマニュアルの説明では、BC5はBchが破棄されるとあるため、
仮にBchのままクックすれば、実機での結果はマスクなしになるはずです。
ところが、実際に実機に出してマスクが生きていることを確認しています:cold_sweat:

知った以後は、破棄がある前提で『DeriveNormalZ』を使用していますが、
もしこれが破棄されず実機で通るようであれば変換作業をスキップでき、
より軽量化が実現します。

私には、仕様が変わったのか、本来出しちゃいけないけどバグって通っているだけなのかまだ検証できていません。
もしこのあたりに明るい方がいらっしゃればお手数ですがコメントを残して頂けますと幸いです:bow_tone1:

:::

---

最後に、
今回説明した項目を全て繋げたノードを載せておきます。
もしお力になれたようであればコメントやXで絡んでいただけるととっても嬉しいです！
お読みいただきありがとうございました。良いお年を:santa_tone1::wave_tone1:　:shinto_shrine:

![SS_No-1571.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2973089/28867c3d-9770-7eaa-5aee-334d4c3149ef.jpeg)


## 自己紹介
フリーランスでデザイナーをしています。
近年はHDゲームとモバイルゲームどちらも経験しており、
リッチな表現とカツカツスペックでの表現どちらもやってます～

ホームページ：
https://westernborn.my.canva.site/

X（Twitter）：
https://x.com/BonChan_3DCG

### 以上！