薄膜効果のあるマテリアルを作る方法

# 薄膜効果とは
![280px-Soap_Bubble_-_foliage_background_-_iridescent_colours_-_Traquair_040801.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/6acd9c49-0d89-3fde-ecac-2ca886b4eb5e.jpeg)
↑ これです。虹色に光ってとても綺麗。

この現象を理解するにはGDC2023のSubstrateに関する説明を見るのが手っ取り早いです。

https://youtu.be/joOIBteSo1w?si=EwKi0_ZEEeimkEqK&t=1109

つまり物体表面に纏わりつくナノメートルスケールの薄膜が光の波長を乱すことに起因しているようです。

しかもSubstrateなら標準機能で表現できます。でも互換性や負荷を理由に従来のマテリアルを使い続けている環境がほとんどではないでしょうか？

今回はSubstrateの実装を参考にして従来のマテリアルでも高い説得力で薄膜効果を表現する方法を紹介します。低負荷になるよう気を付けているのでモバイルデバイスでも使用できると思います。

作成環境はUE5.4です。

# Substrateの実装を掘り下げる
ContentExamplesに含まれるMaterial_Substrateマップにいい感じのサンプルがあるので薄膜効果を確かめます。

![スクリーンショット 2024-07-05 140121.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/5071a28f-28fa-5d8a-5b34-3cab3a559fbe.png)

右のレザーは少しわかりにくいでしょうか？試しに薄膜効果を削除してみましょう。

![スクリーンショット 2024-07-05 141002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/57fcf1a5-0a4c-a9b7-77b1-fd9703a6d860.png)

違いは明らかですね。一見僅かな変化に見えても比較してみると情報量は段違いに感じます。Substrateなので（多分）物理的にも近い表現になっているはずです。

実はマテリアルの中で直接計算されています。

![スクリーンショット 2024-07-05 170452.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/a9644b7a-824a-d437-eaab-f3c557329a01.png)
 
**Substrate Thin-Film**ノードで薄膜効果のあるスペキュラカラーを作ってるだけ。
このノードはHLSLコードでは以下の関数を呼び出しています。コメントを付け足してます。

```
void SubstrateGetThinFilmF0F90(float NoV, float ThinFilmNormalizedThickness, float ThinFilmIOR, inout float3 OutF0, inout float3 OutF90)
{
	const float3 F0 = OutF0;
	const float3 F90 = OutF90;

    // NoVにのみ依存する
	const float NoL = NoV;
	const float VoH = NoV;
	float3 RThinFilm = F_ThinFilm(NoV, NoL, VoH, F0, F90, ThinFilmIOR, ThinFilmNormalizedThickness);

    // RThinFilmをF0とF90に分離
    // ...

	OutF0 = F0_Thin;
	OutF90 = max(F0_Thin, F90);
}
```

**F_ThinFilm()** は鏡面反射のフレネル項に相当しており、この部分が薄膜効果を含んでいます。
物理ベースの説明に言い換えるとSubstrateではBSDFのフレネル項を光の波長変化を考慮したものに置き換えることが出来ます。[^1]

[^1]: https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html
物理ベースの考え方はこの研究が非常に参考になります

この関数から重要なことが分かりまして、実はカメラの角度（**NoV**）と材質パラメータ（**Thickness**、**IOR**、**反射率**）にのみ依存しています。光の入射角には依存してません。[^2]

[^2]: 本来は光の入射角にも依存していると思われますが省略しているようです

# **Substrate Thin-Film**をベイクする
薄膜効果が光の入射角に依存していないということが分かったので、**NoV**+材質パラメータの中から1つを選んで2次元のLUTを作成することができます。

ということで2軸を **(NoV, Thickness)** としてLUTを生成するマテリアルを以下のように作成します。勿論 **(NoV, IOR)** でも良いのでお好みで。

![スクリーンショット 2024-07-05 165445.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/a8442a52-e15a-a8ca-d46c-b22c9431a9da.png)

カスタムノード（**StrataGetThinFilmF0F90**）の中身はこれです。前述のHLSLコードをコピペしただけです。

```
// See StrataGetThinFilmF0F90() in Strata.ush

const float NoL = NoV;
const float VoH = NoV;
float3 RThinFilm = F_ThinFilm(NoV, NoL, VoH, F0, F90, IOR, Thickness);

// Compute a F0 which match evaluation Thin-Film reflectance for the current view angle using Schlick's Fresnel
const float FReference = min(0.99f, F_Schlick(0, 1.0f, NoV).x);
RThinFilm = max(RThinFilm, FReference);

float3 F0_Thin = (RThinFilm - F90 * FReference) / (1 - FReference);

// Ensure the generated F0 doesn't cause micro-oclusion
F0_Thin = max(GetF0MicroOcclusionThreshold(), F0_Thin);

// If micro-occlusion was set, re-apply it.		
const float3 MicroOcclusion = F0RGBToMicroOcclusion(F0);
F0_Thin *= MicroOcclusion;

OutF0 = F0_Thin;
OutF90 = max(F0_Thin, F90);

return 0;
```

お好みのIORと反射率の組み合わせでLUTを生成すると以下みたいになると思います。

![スクリーンショット 2024-07-05 165525.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/0ff3d6f8-5a8c-1eae-f6a8-e920126c81e8.png)

それっぽくなってる気がします。IORと反射率は固定になってしまいますがSubstrateの薄膜効果をベイクできました。

あとは従来のマテリアルに組み込むだけです。

# 従来のマテリアルに組み込む
ベイクした時と同じ2軸でLUTを参照すれば従来のマテリアルでもSubstrateと同じ色が取得できます。

![スクリーンショット 2024-07-05 170756.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/35dc36a3-682f-b18b-3bc1-86ac960cc45a.png)

ちなみにLUTはHDR Compressedに設定すると良いです。解像度は512*512もあれば十分。

結果を比較してみましょう。右側が従来のマテリアルです。

![スクリーンショット 2024-07-05 171453.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/185c20bd-f8d8-5a12-640c-8955c402e008.png)

完全に一致です。素晴らしい！
しかもテクスチャサンプル1回だけ！

レザーはどうでしょう？

![スクリーンショット 2024-07-05 234603.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/4a8733f5-cbab-3eb3-d0e4-d9d8e5f122dd.png)

リファレンスと比べないと正確さは測れませんが、色調が豊かになってリッチな印象を作れました。

# 悪い点
しかし従来のマテリアルはシェーディングモデルが根本的に違うので落とし穴もあります。

Substrate実装では薄膜効果は鏡面反射として計算していると説明しました。従来のマテリアルではBaseColorしか持てないので、正確に鏡面反射として反映できるのはMetallic=1の場合に限ります。それ以外では少なからず拡散色に影響してしまうためMetallic=0に近いほど見た目の差異が目立つようになります。

さらに追い打ちをかけるのはベーステクスチャを持っている場合はカラーブレンドする必要があるということです。ブレンド次第でさらに差異が広がります。

これらの理由から実使用ではほとんどのケースで差異がありますし、Metallicの値によってブレンド率を変える等の工夫が必要になると思います。

参考までにMetallic=1と0でどれくらい違うのか載せておきます。

![スクリーンショット 2024-07-05 174820.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/76bd07e3-97f2-5277-0f13-13fc347c37b9.png)

# おわり
- 経験則的に金属か暗いサーフェイスなら良い感じの見た目を作れます。特に黒色は単一のグレー感を払拭できるため効果的です。
黒って200色あんねん。

- 既存の実装を調べてもLUTを使った実装は多くありますが、その度に「そのLUT、物理的に正しいの？」と常々疑問に思っていました。
だからSubstrateを参考にする必要があったんですね。

- 実はSubstrateも本来の薄膜効果をかなり省略しています[^3]が十分な説得力はあると思います

- 5.4から?はInterchangeプラグインに似たようなLUTが入ってます。
`/Interchange/Utilities/New_LUT.New_LUT`
![スクリーンショット 2024-07-05 181705.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127275/63bc61c4-8786-a00d-f045-80dceda0232d.png)
MaterialXで使うんでしょうか？
このテクスチャが何気に30MB以上もあるのでVRAMを圧迫します。正直削除して欲しい。

[^3]: **F_ThinFilm()** のコメントに詳細説明があります