Unreal EngineのマテリアルがHLSLとしてGPUで実行されるまで

## 概要
マテリアルがHLSLに変換され、それがどのように各パスと連動して描画されるかを辿ってみました。
エンジンはUnreal Engine 5.4を使います。

## MaterialTemplate.ushへの展開

マテリアルエディタで作成されたマテリアルはHLSLに変換されます。その変換されたHLSLコードを見るにはマテリアルエディタのメニューバーから Window -> Shader Code -> HLSL Code を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66160/33f666cf-c748-f495-4e2d-6b6ba2514eb7.png)

すると以下のようなHLSLが表示されたウィンドウが開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66160/15882bad-e615-90e4-854b-18febd73d676.png)


このマテリアルから生成されたHLSLにエントリーポイントはなく、vertex shaderとpixel shaderどちらでもない、双方のコードが生成されていきます。

このHLSLはエンジンフォルダ下にある MaterialTemplate.ush というファイルの中身を合わせて見ると仕組みが見えてきます。例えば、
```
%{calc_pixel_material_inputs_other_inputs}
```
のような構文が至る所に見られます。これら %{...} は明らかにHLSLではない構文であることから推測できるように、この構文で書かれた位置はマテリアルのノードや設定値で置き換えられていきます。


## 各パスのシェーダに取り込まれる過程

マテリアルの実体は、シェーダコード各所にある以下の一文によって取り込まれて利用されます。
```HLSL
#include "/Engine/Generated/Material.ush"
```
GeneratedとはOS上には存在しない仮想のフォルダで、シェーダーコンパイラが内部的に動的生成したコードが格納されます。Material.ushとは、マテリアルのノードが展開挿入された後のMaterialTemplate.ushを参照する仮想のファイルです。

Material.ushをincludeする主なシェーダファイルは以下の通りです。各パスで使われるシェーダファイル郡からMaterial.ushがincludeされており、マテリアルとパスの関係は多対多であることがわかります。

* BasePassVertexShader.usf
* BasePassPixelShader.usf
* DepthOnlyVertexShader.usf
* DepthOnlyPixelShader.usf
* ShadowDepthVertexShader.usf
* ShadowDepthPixelShader.usf

## ノードのHLSL化

一部を除いてほとんどのノードはpixel shaderとなります。

マテリアルから生成されたHLSLがMaterialTemplate.ushのどの関数のどのシンボルに挿入されるかは以下の表のとおりです。

|関数|置き換えるシンボル|説明|
|---|---|---|
|CalcPixelMaterialInputs|%{calc_pixel_material_inputs_initial_calculations} %{calc_pixel_material_inputs_normal} %{calc_pixel_material_inputs_other_inputs} | ほとんどのpixel shaderの計算はこの関数内に生成される|
|GetCustomInterpolators| %{get_custom_interpolators} | vertex shader, VertexInterpolatorノードを使うと生成される
|GetMaterialCustomizedUVs| %{get_material_customized_u_vs} | vertex shader, CustomizedUVもしくはUVを使うだけで生成される
|GetMaterialWorldPositionOffsetRaw|%{get_material_world_position_offset_raw}|vertex shader, World Position Offsetピン

CalcPixelMaterialInputsで計算される結果はGBufferに書き込まれる値です。

GetCustomInterpolators及びGetMaterialCustomizedUVsは、どちらもvertex shaderで計算するものであり、計算結果はpixel shaderに補間されつつ受け渡されます。

興味深いことに、MaterialTemplate.ushはGBufferへアクセスするコードは一切持ちません。また、頂点データに直接アクセスするセマンティクスや、vertex shaderからpixel shaderへの値を受け渡し(頂点補間)を行うセマンティクスも存在していません。これは、呼び出し側(BasePassPixelShader.usf等)に値の受け渡しの一切を委ね、マテリアルはマテリアル計算のみを受け持つ設計のためと思われます。同様の理由で、各シェーディングモデルに応じたライティングや、最終的な画面に表示する色を決定する機能もマテリアルは持っていません。

## マテリアル計算におけるVertex Factoryの役割

前項で、MaterialTemplate.ushは頂点データ入力や頂点補間のセマンティクスを一切持たない事に触れましたが、これらセマンティクスはVertex Factoryで定義されます。

Vertex Factoryとは、BasePassPixelShader.usf等から参照されるもので、マテリアルと合わせてしばしば以下のように並んでincludeされます。

```C++
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"
```

見ての通りGeneratedフォルダであり、シェーダコンパイラによってマテリアル同様にVertex Factoryは動的に差し替えられます。以下が差し替えられる主なVertex Factoryの例です。

* LocalVertexFactory.ush
* GpuSkinVertexFactory.ush
* HairStrandsVertexFactory.ush

各Vertex FactoryはそれぞれFVertexFactoryInterpolantsVSToPSという名前の構造体を定義します。FVertexFactoryInterpolantsVSToPSはその名の通りvertex shaderからpixel shaderに受け渡す値を定義しています。一方、マテリアルはCustomizedUVやVertexInterpolatorノードの使用有無やその数を自分自身で決定しています。実は、FVertexFactoryInterpolantsVSToPSはマテリアルによるこれらの使用有無やその数を参照し、マテリアルに代わってそれらを受け渡す変数を持っています。（これが、マテリアルがVertex Factoryより先にincludeされている一つの理由と考えられます）

Vertex Factoryにおける、その定義部分の一例はLocalVertexFactoryCommon.ushにあります。
```hlsl
struct FVertexFactoryInterpolantsVSToPS
{
...
#if NUM_TEX_COORD_INTERPOLATORS
	float4	TexCoords[(NUM_TEX_COORD_INTERPOLATORS+1)/2]	: TEXCOORD0;
...
```
NUM_TEX_COORD_INTERPOLATORS は MaterialTemplate.ush に以下のように定義されています。

```C++
#define NUM_TEX_COORD_INTERPOLATORS %{num_tex_coord_interpolators}
```

%{num_tex_coord_interpolators} は、UVの使用数やVertexInterpolatorノードの使用により増減します。float2が何個分必要かという意味で定義され、Vertex Factoryはこれらをまとめてfloat4の配列にパッキングし、TEXCOORD0というセマンティクスで中継していることがわかります。

:::note info
古いUE4のバージョンではVertexInterpolatorノードが存在せず、pixel shaderの計算をvertex shaderに逃がすためUV計算に限らずCustomizedUVが使われていました。現在使いやすさにおいてCustomizedUVはVertexInterpolatorに取って代わられましたが、VertexInterpolatorノードの実装を見るとそれはCustomizedUVそのものであり一種の「シンタックスシュガー」であることがわかります。
:::