[UE5]NiagaraのDecalでDynamicParameterを使えるようにする

:::note info
本記事ではNiagaraのDecalRendererでDynamicMaterialParametersモジュールを使用する方法を紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/6773793b-496b-4a9e-b74f-b5490550e160.png)
:::

# 概要
リッチなNiagaraを作るにあたって、NiagaraからMaterialへ動的に値を伝播させるDynamicMaterialParameterモジュールは結構重要になります。
パラメータはfloat4 x4セット、合計16個しか渡せないとはいえ、あれば表現の幅は大きく変わるはず。

MeshRendererやSpriteRendererでは当然のように使われているDynamicMaterialParameterモジュールですが、現状（UE5.5）DecalRendererでは使用できません。
アーティストは泣く泣くDecalColor（float4）を分解して使うわけですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/8363bbe9-33a3-4f9f-8a68-b8b509dcc9f0.png)


本記事ではDecalRendererでもDynamicMaterialParameterモジュールを使用できるようにする方法をまとめます。
エンジンソースの変更が含まれますので責任の取れるプログラマ向けです。

# 開発環境
UE5.2

# 実装

ソースの変更箇所はそこそこ多いですが、大体は既存の`DecalColor`の処理に則って追加しています。
独自に定義した`DECAL_RENDERER_EXTENSION`で囲った部分が追加したコードです。


## SceneManagement.h
ここでDeferredDecalの更新用パラメータを定義します。

```c++:SceneManagement.h
class FDeferredDecalProxy
{
    :
#if DECAL_RENDERER_EXTENSION
	uint32 DecalMaterialParamValidMask = 0x0000;
	FLinearColor DecalDynamicMaterialParameter0 = FLinearColor::Black;
	FLinearColor DecalDynamicMaterialParameter1 = FLinearColor::Black;
	FLinearColor DecalDynamicMaterialParameter2 = FLinearColor::Black;
	FLinearColor DecalDynamicMaterialParameter3 = FLinearColor::Black;
#endif // DECAL_RENDERER_EXTENSION
    :
};
:
struct FDeferredDecalUpdateParams
{
    :
#if DECAL_RENDERER_EXTENSION
	uint32					MaterialParamValidMask = 0x0000;
	FLinearColor			DynamicMaterialParameter0 = FLinearColor::Black;
	FLinearColor			DynamicMaterialParameter1 = FLinearColor::Black;
	FLinearColor			DynamicMaterialParameter2 = FLinearColor::Black;
	FLinearColor			DynamicMaterialParameter3 = FLinearColor::Black;
#endif // DECAL_RENDERER_EXTENSION
    :
};
```

## RenderScene.cpp
ここではシェーダー側で参照する値を更新しています。

```c++:RenderScene.cpp
void FScene::BatchUpdateDecals(TArray<FDeferredDecalUpdateParams>&& UpdateParams)
{
    ENQUEUE_RENDER_COMMAND(FBatchUpdateDecalsCommand)(
		[Scene=this, UpdateParams_RT=MoveTemp(UpdateParams)] (FRHICommandListBase&)
		{
			for (const FDeferredDecalUpdateParams& DecalUpdate : UpdateParams_RT )
			{
                :
#if DECAL_RENDERER_EXTENSION
				DecalUpdate.DecalProxy->DecalMaterialParamValidMask = DecalUpdate.MaterialParamValidMask;
				DecalUpdate.DecalProxy->DecalDynamicMaterialParameter0 = DecalUpdate.DynamicMaterialParameter0;
				DecalUpdate.DecalProxy->DecalDynamicMaterialParameter1 = DecalUpdate.DynamicMaterialParameter1;
				DecalUpdate.DecalProxy->DecalDynamicMaterialParameter2 = DecalUpdate.DynamicMaterialParameter2;
				DecalUpdate.DecalProxy->DecalDynamicMaterialParameter3 = DecalUpdate.DynamicMaterialParameter3;
#endif // DECAL_RENDERER_EXTENSION
    :
}
```

## NiagaraDecalRendererProperties.h/.cpp
ここではNiagaraのDecalRendererにBindingを追加する処理と、DynamicMaterialParameterモジュールのValueと有効/無効切り替えMaskの更新を追加しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/a85b0272-8b4b-4a58-82cc-09c56d821ae3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/5e337f31-e4f4-47ea-bad7-3952a5f48c34.png)


```c++:NiagaraDecalRendererProperties.h
class UNiagaraDecalRendererProperties : public UNiagaraRendererProperties
{
public:
    :
#if DECAL_RENDERER_EXTENSION
    // ここでNiagaraのDecalRendererにBindingを追加..
	UPROPERTY(EditAnywhere, Category = "Bindings")
	FNiagaraVariableAttributeBinding DynamicMaterialBinding;
	UPROPERTY(EditAnywhere, Category = "Bindings")
	FNiagaraVariableAttributeBinding DynamicMaterialBinding1;
	UPROPERTY(EditAnywhere, Category = "Bindings")
	FNiagaraVariableAttributeBinding DynamicMaterialBinding2;
	UPROPERTY(EditAnywhere, Category = "Bindings")
	FNiagaraVariableAttributeBinding DynamicMaterialBinding3;

    // BindingへのAccessorを追加.
	UPROPERTY()
	uint32 MaterialParamValidMask = 0;
	FNiagaraDataSetAccessor<FVector4f>			MaterialParam0DataSetAccessor;
	FNiagaraDataSetAccessor<FVector4f>			MaterialParam1DataSetAccessor;
	FNiagaraDataSetAccessor<FVector4f>			MaterialParam2DataSetAccessor;
	FNiagaraDataSetAccessor<FVector4f>			MaterialParam3DataSetAccessor;
#endif // DECAL_RENDERER_EXTENSION
};
```


```c++:NiagaraDecalRendererProperties.cpp
namespace NiagaraDecalRendererPropertiesLocal
{
    :
	static void SetupBindings(UNiagaraDecalRendererProperties* Props)
	{
        :
#if DECAL_RENDERER_EXTENSION
		Props->DynamicMaterialBinding = FNiagaraConstants::GetAttributeDefaultBinding(SYS_PARAM_PARTICLES_DYNAMIC_MATERIAL_PARAM);
		Props->DynamicMaterialBinding1 = FNiagaraConstants::GetAttributeDefaultBinding(SYS_PARAM_PARTICLES_DYNAMIC_MATERIAL_PARAM_1);
		Props->DynamicMaterialBinding2 = FNiagaraConstants::GetAttributeDefaultBinding(SYS_PARAM_PARTICLES_DYNAMIC_MATERIAL_PARAM_2);
		Props->DynamicMaterialBinding3 = FNiagaraConstants::GetAttributeDefaultBinding(SYS_PARAM_PARTICLES_DYNAMIC_MATERIAL_PARAM_3);
#endif // DECAL_RENDERER_EXTENSION
    :
    }
}
    
UNiagaraDecalRendererProperties::UNiagaraDecalRendererProperties()
{
	AttributeBindings =
	{
        :
#if DECAL_RENDERER_EXTENSION
		&DynamicMaterialBinding,
		&DynamicMaterialBinding1,
		&DynamicMaterialBinding2,
		&DynamicMaterialBinding3,
#endif // DECAL_RENDERER_EXTENSION
        :
    }
    :
}

void UNiagaraDecalRendererProperties::CacheFromCompiledData(const FNiagaraDataSetCompiledData* CompiledData)
{
    :
#if DECAL_RENDERER_EXTENSION
	InitParticleDataSetAccessor(MaterialParam0DataSetAccessor, CompiledData, DynamicMaterialBinding);
	InitParticleDataSetAccessor(MaterialParam1DataSetAccessor, CompiledData, DynamicMaterialBinding1);
	InitParticleDataSetAccessor(MaterialParam2DataSetAccessor, CompiledData, DynamicMaterialBinding2);
	InitParticleDataSetAccessor(MaterialParam3DataSetAccessor, CompiledData, DynamicMaterialBinding3);

#if WITH_EDITORONLY_DATA	
	auto FindNameInCompiledData = [&](FName BindingName)
	{
		int32 Index = CompiledData->Variables.IndexOfByPredicate(
		[&](const FNiagaraVariable& InVariable)
			{
				return InVariable.GetName() == BindingName;
			}
		);
		return Index != INDEX_NONE ? BindingName : NAME_None;
	};
	
	MaterialParamValidMask = GetDynamicParameterCombinedChannelMask(
		FindNameInCompiledData(DynamicMaterialBinding.GetDataSetBindableVariable().GetName()),
		FindNameInCompiledData(DynamicMaterialBinding1.GetDataSetBindableVariable().GetName()),
		FindNameInCompiledData(DynamicMaterialBinding2.GetDataSetBindableVariable().GetName()),
		FindNameInCompiledData(DynamicMaterialBinding3.GetDataSetBindableVariable().GetName())
	);
#endif // WITH_EDITORONLY_DATA
#endif // DECAL_RENDERER_EXTENSION
```

## DecalRenderingShared.h/.cpp

```c++:DecalRenderingShared.h
struct FTransientDecalRenderData
{
    :
#if DECAL_RENDERER_EXTENSION
	uint32 MaterialParamValidMask;
	FLinearColor DynamicParameter;
	FLinearColor DynamicParameter1;
	FLinearColor DynamicParameter2;
	FLinearColor DynamicParameter3;
#endif // DECAL_RENDERER_EXTENSION
    :
};
```

```c++:DecalRenderingShared.cpp
FTransientDecalRenderData::FTransientDecalRenderData(const FDeferredDecalProxy& InDecalProxy, float InConservativeRadius, float InFadeAlpha, EShaderPlatform ShaderPlatform, ERHIFeatureLevel::Type FeatureLevel)
    :
#if DECAL_RENDERER_EXTENSION
	, MaterialParamValidMask(InDecalProxy.DecalMaterialParamValidMask)
	, DynamicParameter(InDecalProxy.DecalDynamicMaterialParameter0)
	, DynamicParameter1(InDecalProxy.DecalDynamicMaterialParameter1)
	, DynamicParameter2(InDecalProxy.DecalDynamicMaterialParameter2)
	, DynamicParameter3(InDecalProxy.DecalDynamicMaterialParameter3)
#endif // DECAL_RENDERER_EXTENSION
    :
:

class FDeferredDecalPS : public FMaterialShader
{
    :
    FDeferredDecalPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		: FMaterialShader(Initializer)
	{
        :
#if DECAL_RENDERER_EXTENSION
		MaterialParamValidMask.Bind(Initializer.ParameterMap, TEXT("MaterialParamValidMask"));
		DynamicParameter.Bind(Initializer.ParameterMap, TEXT("DynamicParameter"));
		DynamicParameter1.Bind(Initializer.ParameterMap, TEXT("DynamicParameter1"));
		DynamicParameter2.Bind(Initializer.ParameterMap, TEXT("DynamicParameter2"));
		DynamicParameter3.Bind(Initializer.ParameterMap, TEXT("DynamicParameter3"));
#endif // DECAL_RENDERER_EXTENSION
        :
    }
    :
    void SetParameters(FRHIBatchedShaderParameters& BatchedParameters, const FViewInfo& View, const FDeferredDecalProxy& DecalProxy, const FMaterialRenderProxy* MaterialProxy, const FMaterial* MaterialResource, const float FadeAlphaValue = 1.0f, const FScene* Scene = nullptr)
	{
        :
#if DECAL_RENDERER_EXTENSION
		SetShaderValue(BatchedParameters, MaterialParamValidMask, DecalProxy.DecalMaterialParamValidMask);
		SetShaderValue(BatchedParameters, DynamicParameter, DecalProxy.DecalDynamicMaterialParameter0);
		SetShaderValue(BatchedParameters, DynamicParameter1, DecalProxy.DecalDynamicMaterialParameter1);
		SetShaderValue(BatchedParameters, DynamicParameter2, DecalProxy.DecalDynamicMaterialParameter2);
		SetShaderValue(BatchedParameters, DynamicParameter3, DecalProxy.DecalDynamicMaterialParameter3);
#endif // DECAL_RENDERER_EXTENSION
        :
    }

    :
	LAYOUT_FIELD(FShaderParameter, DecalParams);
	LAYOUT_FIELD(FShaderParameter, DecalColorParam);
#if DECAL_RENDERER_EXTENSION
	LAYOUT_FIELD(FShaderParameter, MaterialParamValidMask);
	LAYOUT_FIELD(FShaderParameter, DynamicParameter);
	LAYOUT_FIELD(FShaderParameter, DynamicParameter1);
	LAYOUT_FIELD(FShaderParameter, DynamicParameter2);
	LAYOUT_FIELD(FShaderParameter, DynamicParameter3);
#endif // DECAL_RENDERER_EXTENSION
    :
};
```

## NiagaraRendererDecals.cpp
ここではPropertiesで追加したBindingの値を`FDeferredDecalUpdateParams`に設定しています。
大体`DecalColor`の処理と同様なので細かい部分は割愛します。
```c++:NiagaraRendererDecals.cpp
FNiagaraDynamicDataBase* FNiagaraRendererDecals::GenerateDynamicData(const FNiagaraSceneProxy* Proxy, const UNiagaraRendererProperties* InProperties, const FNiagaraEmitterInstance* Emitter) const
{
    :
#if DECAL_RENDERER_EXTENSION
	const FLinearColor LocalDefaultDynamicMaterialParameter0 = ParameterStore.GetParameterValueOrDefault(RendererProperties->DynamicMaterialBinding.GetParamMapBindableVariable(), FLinearColor::Black);
	const FLinearColor LocalDefaultDynamicMaterialParameter1 = ParameterStore.GetParameterValueOrDefault(RendererProperties->DynamicMaterialBinding1.GetParamMapBindableVariable(), FLinearColor::Black);
	const FLinearColor LocalDefaultDynamicMaterialParameter2 = ParameterStore.GetParameterValueOrDefault(RendererProperties->DynamicMaterialBinding2.GetParamMapBindableVariable(), FLinearColor::Black);
	const FLinearColor LocalDefaultDynamicMaterialParameter3 = ParameterStore.GetParameterValueOrDefault(RendererProperties->DynamicMaterialBinding3.GetParamMapBindableVariable(), FLinearColor::Black);
#endif // DECAL_RENDERER_EXTENSION
    :
	if (RendererProperties->SourceMode == ENiagaraRendererSourceDataMode::Particles)
	{
        :
#if DECAL_RENDERER_EXTENSION
		const auto DynamicParameterReader = RendererProperties->MaterialParam0DataSetAccessor.GetReader(DataSet);
		const auto DynamicParameterReader1 = RendererProperties->MaterialParam1DataSetAccessor.GetReader(DataSet);
		const auto DynamicParameterReader2 = RendererProperties->MaterialParam2DataSetAccessor.GetReader(DataSet);
		const auto DynamicParameterReader3 = RendererProperties->MaterialParam3DataSetAccessor.GetReader(DataSet);
#endif // DECAL_RENDERER_EXTENSION
        :
		for (uint32 ParticleIndex = 0; ParticleIndex < DataToRender->GetNumInstances(); ++ParticleIndex)
		{
            :
#if DECAL_RENDERER_EXTENSION
			UpdateParams.DynamicMaterialParameter0 = DynamicMaterialParameter0;
			UpdateParams.DynamicMaterialParameter1 = DynamicMaterialParameter1;
			UpdateParams.DynamicMaterialParameter2 = DynamicMaterialParameter2;
			UpdateParams.DynamicMaterialParameter3 = DynamicMaterialParameter3;
#endif // DECAL_RENDERER_EXTENSION
        }
    }
    :
}
```

## DeferredDecal.usf
ついにシェーダーコードです。でも大したことはしていません。
パラメータを追加して、Niagaraで使用する`MaterialParameter.Particle`に設定しているだけです。
ここで定義したパラメータ名は[DecalRenderingShared.h/cpp](https://qiita.com/drafts/a993b257241f88d18d47/edit#decalrenderingsharedhcpp)で指定します。

```c++:DeferredDecal.usf
:
// Decal color can be accessed using the material node
float4 DecalColorParam;

#if DECAL_RENDERER_EXTENSION
uint MaterialParamValidMask;
float4 DynamicParameter;
float4 DynamicParameter1;
float4 DynamicParameter2;
float4 DynamicParameter3;
#endif // DECAL_RENDERER_EXTENSION
:
void FPixelShaderInOut_MainPS(inout FPixelShaderIn In, inout FPixelShaderOut Out, uint ArrayIndex)
{
    :
#if DECAL_RENDERER_EXTENSION
#if (DYNAMIC_PARAMETERS_MASK != 0)
	MaterialParameters.Particle.DynamicParameterValidMask = MaterialParamValidMask;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 1)
	MaterialParameters.Particle.DynamicParameter = DynamicParameter;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 2)
	MaterialParameters.Particle.DynamicParameter1 = DynamicParameter1;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 4)
	MaterialParameters.Particle.DynamicParameter2 = DynamicParameter2;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 8)
	MaterialParameters.Particle.DynamicParameter3 = DynamicParameter3;
#endif
#endif // DECAL_RENDERER_EXTENSION
    :
}
```

## MaterialTemplate.ush
ここではDynamicParameterを使用する条件を変更しています。
デフォルトでは`FNiagaraVertexFactoryBase`を継承したクラスで有効にされるフラグをみる条件になっていますが、Decalは既存のDeferredDecalの処理を使用している関係でVertexFactoryは使用していません。
そのため条件になっているVertexFactory関連のフラグをコメントアウトしたり、DecalRendererで有効になるフラグを使用するようにしています。

```c++:MaterialTemplate.ush
:
struct FMaterialParticleParameters
{
    :
// #if DECAL_RENDERER_EXTENSION ifプリプロセッサを挟むためコメントアウト.
// 追加コード.
#if DYNAMIC_PARAMETERS_MASK != 0
// 元コード.
//#if NIAGARA_PARTICLE_FACTORY && (DYNAMIC_PARAMETERS_MASK != 0)
	uint DynamicParameterValidMask;
// #endif // DECAL_RENDERER_EXTENSION
    :
};
:
float4 GetDynamicParameter(FMaterialParticleParameters Parameters, float4 Default, int ParameterIndex=0)
{
// #if DECAL_RENDERER_EXTENSION ifプリプロセッサを挟むためコメントアウト.
// 追加コード.
#if NIAGARA_PARTICLE_FACTORY || (DECAL_PRIMITIVE)
// 元コード.
//#if (NIAGARA_PARTICLE_FACTORY)
// #endif // DECAL_RENDERER_EXTENSION
```

変更箇所多すぎィ！

# 結果

DynamicParameterのRGBをベースカラー、Aをオパシティとして使用するシンプルなDeferredDecalマテリアルを用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/0697f2da-58fd-4884-9c0a-484c9de2bbcf.png)

DynamicParameterのデフォルト値は[1.0, 0.0, 0.0, 0.5]にしてあるので、マテリアルを直接レベルに配置すると半透明の赤いDecalが出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/58f95af3-0e0f-4fd0-870a-515a864ec029.png)

このマテリアルをNiagaraのDecalRendererに設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/730cdf4d-e2c9-4063-9c1a-49bc82993a8f.png)

サイズが違うのはNiagara側でDecalSizeを設定しているからです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/500d69a0-e8da-47ce-ba53-eb6b4918d97b.png)

DynamicParameterModuleを追加して任意の値を設定します。
今回は[0.0, 0.0, 1.0, 1.0]としました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/9264f871-19b7-4333-8e07-7450da7ae839.png)

NiagaraのDynamicParameterModuleの値が反映されましたね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/90cb44f4-372e-4428-b117-05b05f9ca5bb.png)

# まとめ
DecalRendererでもDynamicParameterが使用できるようになりました。
これでDecalエフェクトの表現の幅が広がりますね。

変更箇所が多いので運用・保守にはご注意ください。