こんにちは。トイロジックプログラマのHです。トイロジックではUE4でのゲーム開発を行っており、現在のニーズに合ったリアルな絵作りを目指すために、積極的に最新のツールを使用しています。

その中でもUE4エフェクト作成ツール「ナイアガラ」は、美麗なエフェクトを作成可能でカスタマイズも細かく行えるため、プロジェクトによって採用を行っています。今回は開発中に発生した機能拡張の作成事例として、動的にエフェクト内のマテリアルを変更する方法について紹介したいと思います。(執筆時点でのUE4バージョンは4.26.2)

ナイアガラでマテリアルを差し替えるには?

では実際にシンプルなナイアガラシステムアセットを使用してどのように行っていくのかを見ていきましょう。

通常ナイアガラでエミッターを作成する際には、一つのRendererモジュールにスプライトやメッシュ、それに紐づくマテリアルを設定します。そのRendererモジュールに指定されたマテリアルをデフォルトで設定されているものから変える際には「User Param Binding」を使用します。

こちらの機能を用いることでエミッターの「Sprite Renderer」「Mesh Renderer」等にはデフォルトで指定するマテリアルとは別に、任意に設定できるマテリアルを指定可能です。つまり「User Param Binding」で指定するマテリアルを動的に変更してあげれば行いたいことが実現できそう、ということがわかります。まずは事前準備として上書き用マテリアルインターフェイスを作り、エミッターのRendererに紐づけを行っておきましょう。

専用データ構造体の作成

さて、用意した上書き用マテリアルに差し替え先のマテリアルを指定するため、専用のデータ構造体を作成する必要があります。こちらにはC++側で対応していきます。ナイアガラで使用できるデータ構造体はUNiagaraDataInterfaceを継承したクラスで作ることができます。

こちらを使用することでエンジン改造を行わずにナイアガラの機能拡張を行うことができます。まずはナイアガラの機能を使用するため[プロジェクト.Build.cs]にモジュールの追加を行いましょう。

以下の例は切替用のマテリアルインスタンスを複数保持することのできる汎用的なデータ構造体の宣言部です。


// ナイアガラ用 汎用マテリアル切り替え
UCLASS(EditInlineNew,meta = (DisplayName = "Test Change Material"), Blueprintable, BlueprintType)
class UTestNiagaraDataInterfaceChangeMaterial : public UNiagaraDataInterface
{
	GENERATED_BODY()
public:
	// 実行時用のインスタンスパラメータ
	struct FPerInstanceData
	{
		void Update(UTestNiagaraDataInterfaceChangeMaterial* Parent, FNiagaraSystemInstance* SystemInstance, float DeltaSeconds);
		void SetMaterial(UTestNiagaraDataInterfaceChangeMaterial* Parent, FNiagaraSystemInstance* SystemInstance, const int32 SwitchIndex);

		int32 prevSwitchIndex_ = -1;
	};

	// コンストラクタ
	UTestNiagaraDataInterfaceChangeMaterial(const FObjectInitializer& ObjectInitializer);

	virtual void PostInitProperties() override;
	virtual bool InitPerInstanceData(void* PerInstanceData, FNiagaraSystemInstance* InSystemInstance) override;
	virtual void DestroyPerInstanceData(void* PerInstanceData, FNiagaraSystemInstance* InSystemInstance) override {}
	virtual bool PerInstanceTick(void* PerInstanceData, FNiagaraSystemInstance* SystemInstance, float DeltaSeconds) override;
	virtual int32 PerInstanceDataSize() const override { return sizeof(FPerInstanceData); }
	virtual bool Equals(const UNiagaraDataInterface* Other) const override;
	virtual bool HasPreSimulateTick() const { return true; }

	// 切替マテリアル値の変更
	void SetSwitchIndex(const int32 SwitchIndex) { switchIndex_ = SwitchIndex; }

protected:
	virtual bool CopyToInternal(UNiagaraDataInterface* Destination) const override;

protected:

	// 切替マテリアル値
	UPROPERTY(EditAnywhere)
	int32 switchIndex_ = 0;
	// 切替先マテリアルの参照を設定
	UPROPERTY(EditAnywhere)
	FNiagaraUserParameterBinding materialVariable_;
	// 切替マテリアル配列
	UPROPERTY(EditAnywhere)
	TArray changeMaterials_;
};

データインターフェイスにUPROPERTYで定義している変数はUE4エディタ上で設定を行うことができるパラメータになります。定義側は以下のようになります。まずは初期化部です。


UTestNiagaraDataInterfaceChangeMaterial::UTestNiagaraDataInterfaceChangeMaterial(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// エディタ上での設定方法をマテリアルインターフェイスタイプにします
	FNiagaraTypeDefinition MaterialDef(UMaterialInterface::StaticClass());
	materialVariable_.Parameter.SetType(MaterialDef);
}

void UTestNiagaraDataInterfaceChangeMaterial::PostInitProperties()
{
	// 型としてNiagaraに登録します
	Super::PostInitProperties();
	if (HasAnyFlags(RF_ClassDefaultObject))
	{
		FNiagaraTypeRegistry::Register(FNiagaraTypeDefinition(GetClass()), true, false, false);
	}
}


bool UTestNiagaraDataInterfaceChangeMaterial::InitPerInstanceData(void* PerInstanceData, FNiagaraSystemInstance* InSystemInstance)
{
	auto* piData = new (PerInstanceData) FPerInstanceData;
	return true;
}


bool UTestNiagaraDataInterfaceChangeMaterial::CopyToInternal(UNiagaraDataInterface* Destination) const
{
	if (!Super::CopyToInternal(Destination)) { return false; }
	// この関数の実装がないとUE4Editor上で変更したパラメータがランタイム中に反映されないので注意!
	if (auto* dest = Cast<UTestNiagaraDataInterfaceChangeMaterial>(Destination))
	{
		dest->switchIndex_ = switchIndex_;
		dest->materialVariable_ = materialVariable_;
		dest->changeMaterials_ = changeMaterials_;
		return true;
	}

	return false;
}

bool UTestNiagaraDataInterfaceChangeMaterial::Equals(const UNiagaraDataInterface* Other) const
{
	if (!Super::Equals(Other)) { return false; }

	if (auto* changeMaterial = Cast<UTestNiagaraDataInterfaceChangeMaterial>(Other))
	{
		return changeMaterial->switchIndex_ == switchIndex_ &&
			changeMaterial->materialVariable_ == materialVariable_ &&
			changeMaterial->changeMaterials_ == changeMaterials_;
	}

	return false;
}

更新部では状態を監視しマテリアルを入れ替える処理を行っています。

bool UTestNiagaraDataInterfaceChangeMaterial::PerInstanceTick(void* PerInstanceData, FNiagaraSystemInstance* SystemInstance, float DeltaSeconds)
{
	check(PerInstanceData);
	check(SystemInstance);

	auto* piData = static_cast<FPerInstanceData*>(PerInstanceData);
	piData->Update(this, SystemInstance, DeltaSeconds);
	return false;
}

void UTestNiagaraDataInterfaceChangeMaterial::FPerInstanceData::Update(UTestNiagaraDataInterfaceChangeMaterial* Parent, FNiagaraSystemInstance* SystemInstance, float DeltaSeconds)
{
	if (Parent != nullptr && Parent->materialVariable_.Parameter.IsValid())
	{
		if (auto* niagaraComponent = Cast<UNiagaraComponent>(SystemInstance->GetAttachComponent()))
		{
			if (Parent->switchIndex_ != prevSwitchIndex_)
			{
				SetMaterial(Parent, SystemInstance, Parent->switchIndex_);
				prevSwitchIndex_ = Parent->switchIndex_;
			}
		}
	}
}

void UTestNiagaraDataInterfaceChangeMaterial::FPerInstanceData::SetMaterial(UTestNiagaraDataInterfaceChangeMaterial* Parent, FNiagaraSystemInstance* SystemInstance, const int32 SwitchIndex)
{
	if (Parent != nullptr && SystemInstance != nullptr)
	{
		// マテリアル差し替え
		if (auto* niagaraComponent = Cast<UNiagaraComponent>(SystemInstance->GetAttachComponent()))
		{
			UMaterialInterface* changeMaterial = nullptr;
			if (SwitchIndex < Parent->changeMaterials_.Num())
			{
				changeMaterial = Parent->changeMaterials_[SwitchIndex];
			}
			if (changeMaterial)
			{
				niagaraComponent->SetVariableMaterial(Parent->materialVariable_.Parameter.GetName(), changeMaterial);
			}
		}
	}
}

マテリアルの入れ替え条件となる値の取得方法は様々な方法で変更できますが、今回の例のように直接このクラスに値を入れることで再生したエフェクトごとに別のマテリアルを入れてあげることも可能です。実際にマテリアルの値を直接切り替える際のコード例は以下のようになります。


const FNiagaraParameterStore& overrideParameters = NiagaraComponent->GetOverrideParameters();

		// 例えばスプライトのマテリアル変更
		{
			FNiagaraVariable Variable(FNiagaraTypeDefinition(UTestNiagaraDataInterfaceChangeMaterial::StaticClass()), TEXT("ChangeSpriteMaterial"));
			int32 index = overrideParameters.IndexOf(Variable);
			if (index != INDEX_NONE)
			{
				if (UTestNiagaraDataInterfaceChangeMaterial* changeMaterialInterface = Cast<UTestNiagaraDataInterfaceChangeMaterial>(overrideParameters.GetDataInterface(index)))
				{
					changeMaterialInterface->SetSwitchIndex(SwitchIndex);
				}
			}
		}
		// 例えばメッシュのマテリアル変更
		{
			FNiagaraVariable Variable(FNiagaraTypeDefinition(UTestNiagaraDataInterfaceChangeMaterial::StaticClass()), TEXT("ChangeMeshMaterial"));
			int32 index = overrideParameters.IndexOf(Variable);
			if (index != INDEX_NONE)
			{
				if (UTestNiagaraDataInterfaceChangeMaterial* changeMaterialInterface = Cast<UTestNiagaraDataInterfaceChangeMaterial>(overrideParameters.GetDataInterface(index)))
				{
					changeMaterialInterface->SetSwitchIndex(SwitchIndex);
				}
			}
		}

これでC++側の対応は完了です。作成した構造体をナイアガラで使用しましょう。

UE4エディタで設定してみよう

最後にこのデータ構造体を使ってUE4エディタで設定を行うと以下のような形になります。

C++で用意した構造体をナイアガラ上で追加し、切替用の適当なマテリアルをいくつか割り当ててみました。先に作っておいたマテリアルインターフェイスを紐づけることで、マテリアルが切り替わるように設定完了です。実行中に番号を更新することで、単一のアセットを動的にマテリアルを切り替えて表現を行うことができました。

最後に

いかがでしたでしょうか。今回の紹介はあくまで汎用的なものでしたが、マテリアルを切り替える条件となる値を状況によって変えたり制約を加えたりと応用も効くものかと思います。

このようにゲーム開発の現場ではプロジェクトによって様々な拡張要望に応えることがあります。より開発を効率的に進めるために今後もいろんな技術も学んでいきたいですね。今回の内容が皆さんの知見となっていれば嬉しいです。ありがとうございました!

著者紹介 H
2011年にトイロジックに新卒入社。『Happy Wars』ではUIリードプログラマ、未発表タイトルではリードプログラマを務める。現在はプレイングマネージャーとしてマネージャー業務を行いつつ、UE4を使用した開発に携わっている。

トイロジックでは現在、一緒に働くプログラマーを募集しています。

不明点などもお気軽にお問い合わせくださいフルリモート採用も行っております、ご応募お待ちしております!