12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Epic Games Japan #3Advent Calendar 2019

Day 25

[UE4]Merry Niagara Post Process!!!

Last updated at Posted at 2019-12-24

#序文
みなさんNiagaraでエキサイティンしていますか!
NiagaraはNiagara Module Script や Expressionなどプログラマブルな部分が大変多くひじょーーーーーに多彩なVFXを作るための基盤になるシステムです。
4.24ではNiagara Overviewも追加されてすごく編集しやすくなりましたね。
今回はNiagaraの機能を拡張して、エンジン改造なしでポストプロセスをNiagaraから適用できるようにモジュールを作ってみたいと思います!

#使用上のご注意!(重要)
1.Niagara自体は4.24現在ベータ版です。製品に利用するのはパフォーマンス面や不具合などの点でリスクを伴い得るので、ご注意お願いします。
2.このプラグインは4.24用です。※ソースファイルに関しては4.23以前でも動作する可能性があります。
3.このプラグインはQAなどを行ったものではありません。そのまま製品にご利用しないようにお願いします。
4.バイナリファイルも含んでいますが、Launcher版のUE4.24.1で作成したものです。

#ソース
https://github.com/wankotank/NiagaraPostProcessPlugin

#プラグインの重要なポイント解説

今回重要になるのは UNiagaraDataInterface です。
誤解を恐れずに大雑把に説明すると、Niagaraの中でクラスのようなものとして機能を提供する仕組みです。
内部にメンバ変数や静的なパラメータ、NiagaraModuleScriptから呼び出せるメンバー関数などを定義することができます。
非常に自由度が高くNiagaraDataInterfaceHoudiniCSVChaosなど、UE4を構成する他のモジュールとの連携するための仕組みを提供するためにも役に立っています。
もう一つ重要な要素として、Tickを持っていてNiagaraSystem毎に処理を加えることができます。

追加すると Make New のメニューの中に現れます。
ファイル名

モジュール変数として定義すると静的なパラメータとして編集が可能です。
ファイル名

NiagaraModuleScirpt内ではメンバー関数を呼び出すことも可能です。
ファイル名

#ネイティブコード部解説
##データインターフェイス宣言部

まずはデータインターフェイス自体を宣言します。
インスタンスパラメータと静的なパラメータを定義します。
静的な部分は直接NiagaraVMには露出せずネイティブコードやNiagaraEmitterのエディタ上だけで見えるので割と自由に書けます。
コメントにもありますが、PerInstanceDataSizeサイズを0にするとPerInstanceTickは呼び出されません。注意してください。


//データインターフェイス本体。
//データインターフェイスにUPROPERTY定義されているものは
//モジュールパラメータにしたときにNiagaraEmitter/NiagaraSystemで設定できる静的なパラメータになります。
UCLASS(EditInlineNew, Category = "PostProcess", meta = (DisplayName = "Post Process Control"))
class UNiagaraDataInterfacePostProcess : public UNiagaraDataInterface
{
	GENERATED_UCLASS_BODY()
public:

	UPROPERTY(EditAnywhere, Category = "Setting")
	float	Radius;

	UPROPERTY(EditAnywhere, Category = "Setting")
	FPostProcessSettings	Settings;

	UPROPERTY(EditAnywhere, Category = "Setting")
	FString VariableName;

	/* instance parameter type*/
	struct FPerInstanceData
	{
		float Amount;		//Amount on runtime
		float ParameterB;	//Unused
		float ParameterC;	//Unused
		float ParameterD;	//Unused
	};

	//UObject Interface
	virtual void PostInitProperties() override;
	virtual void PostLoad() override;
#if WITH_EDITOR
	virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
	//UObject Interface End

	/** Initializes the per instance data for this interface. Returns false if there was some error and the simulation should be disabled. */
	virtual bool InitPerInstanceData(void* PerInstanceData, FNiagaraSystemInstance* InSystemInstance) override;
	/** Destroys the per instence data for this interface. */
	virtual void DestroyPerInstanceData(void* PerInstanceData, FNiagaraSystemInstance* InSystemInstance) {}

	/** Ticks the per instance data for this interface, if it has any. */
	virtual bool PerInstanceTick(void* PerInstanceData, FNiagaraSystemInstance* SystemInstance, float DeltaSeconds);
	virtual bool PerInstanceTickPostSimulate(void* PerInstanceData, FNiagaraSystemInstance* SystemInstance, float DeltaSeconds);

	/** If Instance size == 0, PerInstanceTick is never called. */
	virtual int32 PerInstanceDataSize()const { return sizeof(FPerInstanceData); }

##型の登録

FNiagaraTypeRegistry::Register() 関数で型としてNiagaraに登録します。
今回作成したUNiagaraDataInterface継承クラス以外にも構造体型の型も登録されています。
FNiagaraTypeRegistry::Register でエンジンのソースを検索してみると、Niagaraの構造や拡張の勉強になりますので一度検索してみるのをお勧めします。

void UNiagaraDataInterfacePostProcess::PostInitProperties()
{
	Super::PostInitProperties();

	//Can we register data interfaces as regular types and fold them into the FNiagaraVariable framework for UI and function calls etc?
	if (HasAnyFlags(RF_ClassDefaultObject))
	{
		FNiagaraTypeRegistry::Register(FNiagaraTypeDefinition(GetClass()), true, false, false);
	}
}

##内部コピー関数

CopyToInternal() 関数で DataInterfaceからDataInterface に値をコピーする処理を記述します。
この関数の実装がないとUPROPERTY付きでNiagaraエディタ上で編集した静的なプロパティが
ランタイムで使われるオブジェクトの中にコピーされず、値を読み取れないので注意してください!

bool UNiagaraDataInterfacePostProcess::CopyToInternal(UNiagaraDataInterface* Destination) const
{
	if (!Super::CopyToInternal(Destination))
	{
		return false;
	}
	UNiagaraDataInterfacePostProcess* OtherTyped = CastChecked<UNiagaraDataInterfacePostProcess>(Destination);


	OtherTyped->Settings = Settings;
	OtherTyped->Radius = Radius;
	return true;
}

##メンバ関数定義

  • GetFunctions() override 関数

    メンバ関数の入出力パラメータを定義します
  • GetVMExternalFunction() override 関数

    NiagaraVMからGetFunctionで登録された関数が呼び出されたときに、ここでバインディングしたネイティブ関数が呼び出される
  • SetPostProcessParameters() 関数

    ユーザーが定義したネイティブ関数。VMの実行コンテキスト(FVectorVMContext)から入出力用のアクセサを介して、
    ランタイムパラメータにアクセスし、読み出したり書きだしたりする。

    必ず、以下の順番で宣言すること。
    • FExternalFuncInputHandler
    • FUserPtrHandler
    • FExternalFuncRegisterHandler
      このネイティブ関数呼び出しはGameThreadからではなく、TaskThreadから呼びだされます得にParticleに対して呼び出し可能にしている場合
      複数のスレッドから同時に呼び出される可能性があるので注意してください。
void UNiagaraDataInterfacePostProcess::GetFunctions(TArray<FNiagaraFunctionSignature>& OutFunctions)
{
	FNiagaraFunctionSignature Sig3;
	Sig3.Name = TEXT("SetPostProcessParameters");
	Sig3.bMemberFunction = true;
	Sig3.bRequiresContext = false;
	Sig3.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition(GetClass()), TEXT("MyData")));
	Sig3.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("A")));
	Sig3.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("B")));
	Sig3.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("C")));
	Sig3.Inputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetFloatDef(), TEXT("D")));
	Sig3.Outputs.Add(FNiagaraVariable(FNiagaraTypeDefinition::GetBoolDef(), TEXT("Success")));
	Sig3.SetDescription(LOCTEXT("Pass parameters to the post process component", "see NiagaraDataInterfacePostProcess"));
	OutFunctions.Add(Sig3);
}

void UNiagaraDataInterfacePostProcess::GetVMExternalFunction(const FVMExternalFunctionBindingInfo& BindingInfo, void* InstanceData, FVMExternalFunction &OutFunc)
{
	FMyNiagaraDataInstanceData *InstData = (FMyNiagaraDataInstanceData *)InstanceData;
	if (BindingInfo.Name == TEXT("SetPostProcessParameters"))
	{
		OutFunc = FVMExternalFunction::CreateUObject(this, &UNiagaraDataInterfacePostProcess::SetPostProcessParameters);
	}
	else
	{
		UE_LOG(LogMyNiagara, Error, TEXT("Could not find data interface external function. %s\n"),
			*BindingInfo.Name.ToString());
	}
}

void UNiagaraDataInterfacePostProcess::SetPostProcessParameters(FVectorVMContext& Context)
{
	VectorVM::FExternalFuncInputHandler<float> ParamA(Context);
	VectorVM::FExternalFuncInputHandler<float> ParamB(Context);
	VectorVM::FExternalFuncInputHandler<float> ParamC(Context);
	VectorVM::FExternalFuncInputHandler<float> ParamD(Context);

	VectorVM::FUserPtrHandler<FPerInstanceData> InstanceData(Context);

	VectorVM::FExternalFuncRegisterHandler<bool> OutValue(Context);

	for (int32 i = 0; i < Context.NumInstances; ++i)
	{
		InstanceData->A = ParamA.GetAndAdvance();
		InstanceData->B = ParamB.GetAndAdvance();
		InstanceData->C = ParamC.GetAndAdvance();
		InstanceData->D = ParamD.GetAndAdvance();
		*OutValue.GetDest() = true;
		OutValue.Advance();
	}
}

毎Tick毎にEmitterのシミュレーションが終わった後に呼ばれる関数

PerInstanceDataに対して呼び出されるTick関数。
設定されているポストプロセスセッティングを使って、
このインスタンスを持っているアクターにボリュームとポストプロセスコンポーネントを追加し、
Niagaraから入力される値をコンポーネントに設定しています。
この関数はGameThreadから呼び出されます。あまりにも高価な実装を書いてしまうと大きな負荷になりえるので注意!

bool UNiagaraDataInterfacePostProcess::PerInstanceTickPostSimulate(void* PerInstanceData, FNiagaraSystemInstance* InSystemInstance, float DeltaSeconds)
{
	//インスタンスデータを取り出して
	FPerInstanceData*PIData = static_cast<FPerInstanceData*>(PerInstanceData);
	UE_LOG(LogMyNiagara, Verbose, TEXT("A %8.2f B %8.2f C %8.2f D %8.2f"), PIData->Amount, PIData->B, PIData->C, PIData->D);

	//このSystemInstanceを持っているアクターを取得
	UNiagaraComponent* Component = InSystemInstance->GetComponent();
	AActor* Actor = Component ? Component->GetOwner() : nullptr;
	if( Actor )
	{
		//アクターがポストプロセスコンポーネントを持ってるか調べて、
		UPostProcessComponent* PostProcess = Actor->FindComponentByClass<UPostProcessComponent>();
		if( PostProcess == nullptr )
		{
			//なかった時にランタイムでコンポーネントを生成
			USphereComponent* Sphere = Cast<USphereComponent>( NewObject<USphereComponent>(Actor,FName("Sphere") ) );
			Sphere->SetSphereRadius( Radius );
			Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
			Sphere->RegisterComponent();
			Sphere->AttachToComponent(Actor->GetRootComponent(),FAttachmentTransformRules::KeepRelativeTransform);

			PostProcess = Cast<UPostProcessComponent>( NewObject<UPostProcessComponent>(Actor,FName("PostProcess") ) );
			PostProcess->bUnbound = false;
			PostProcess->RegisterComponent();
			PostProcess->AttachToComponent(Sphere, FAttachmentTransformRules::KeepRelativeTransform);
		}
		//更新情報をコンポーネントに設定する
		USphereComponent* Sphere = Cast<USphereComponent>( PostProcess->GetAttachParent() );
		if( Sphere )
		{
			Sphere->SetSphereRadius( Radius );
		}
		PostProcess->Settings = Settings;
		PostProcess->BlendWeight = FMath::Clamp(PIData->A,0.0f,1.0f);
	}

	return false;
}

#Niagara module script部解説

NiagaraPostProcessスクリプトモジュールの解説!
すごくシンプルです。Moduleパラメータに作成したNiagaraDataInterfacePostProcessをまず宣言。
カーブを適用したりしたいパラメータをさらにModuleパラメータとして宣言(Module.Amount / Module.ParameterBなど)
関数呼び出しの戻り値として、BoolパラメータをMapSetに入力してEmitterパラメータとして出力しています。
本来このSetPostProcessPrameters関数はInstanceDataに書き込むだけで十分なため戻り値は不要ですが、
なんらかの値をモジュール外に出力しないと関数呼び出し自体が、影響を及ぼさないと判断されるのか、関数呼び出しが行われません。
対策としてEmitter.DummyOutputFromPostProcessという長い名前のダミーに出力しています。

image.png

#使い方

##エミッターにNiagaraPostProcessモジュールスクリプトを追加します。

image.png

##追加したモジュールを見てみる

ModuleScriptに追加したパラメータが赤枠で囲われた部分です。
青枠で囲われた部分はModule.PostProcessControlという名前で宣言した、
NiagaraDataInterfacePostProcessのパラメータです(Radius/Settings)。

赤枠で定義されたモジュールパラメータにだけ、緑枠の▼が表示されているのがわかります。
この▼のあるところはNiagaraDirectInputやExpressionを適用することが可能です

image.png

##カーブを適用する
▼をクリックしてFloat from Curve

image.png

カーブ編集できるようになったので山が二つあるカーブを適用しました。
image.png
加えてポストプロセス設定をいじって暗所を真っ赤にするように設定を加えてみました。

#実行
パーティクルの噴出に合わせてポストプロセスが動作し二回暗部が真っ赤になるのが見えます!

NiagaraPostProcess.gif

ボリュームが設定してあるので範囲外にカメラがいる場合は影響がでません。

#まとめ

NiagaraDataInterfaceを使って独自の実装を行うことで、エンジンを破壊することなくNiagaraにさまざまな処理を加えることが可能です。
ポストプロセスのほかにもカメラに対する処理や、ゲームに対するイベント処理を実装することもできそうですね。
VFXデザイナーがNiagaraを介してゲームに対して様々な影響を直接制御できるようになります。これは捗ります。

プラグインはあくまでサンプルとして公開させていただいております。
例えばカスタムポストプロセスマテリアルをカーブで制御したいんじゃーとか
カラコレをカーブでいじりたいんじゃーとかあると思いますので、
ぜひご自由に実装を追加してポストプロセスセッティングにつっこんであげてください。
無数のPostProcessVolumeが生まれてしまうと見た目もパフォーマンスのひどい事になりえるので、
必要なもの以外を間引く処理や、カリングなどの制御を追加しても良いですね!

それでは Let's Niagara!

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?