#序文
みなさん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から呼び出せるメンバー関数などを定義することができます。
非常に自由度が高くNiagaraDataInterfaceHoudiniCSVやChaosなど、UE4を構成する他のモジュールとの連携するための仕組みを提供するためにも役に立っています。
もう一つ重要な要素として、Tickを持っていてNiagaraSystem毎に処理を加えることができます。
モジュール変数として定義すると静的なパラメータとして編集が可能です。
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という長い名前のダミーに出力しています。
#使い方
##エミッターにNiagaraPostProcessモジュールスクリプトを追加します。
##追加したモジュールを見てみる
ModuleScriptに追加したパラメータが赤枠で囲われた部分です。
青枠で囲われた部分はModule.PostProcessControlという名前で宣言した、
NiagaraDataInterfacePostProcessのパラメータです(Radius/Settings)。
赤枠で定義されたモジュールパラメータにだけ、緑枠の▼が表示されているのがわかります。
この▼のあるところはNiagaraDirectInputやExpressionを適用することが可能です
##カーブを適用する
▼をクリックしてFloat from Curve
カーブ編集できるようになったので山が二つあるカーブを適用しました。
加えてポストプロセス設定をいじって暗所を真っ赤にするように設定を加えてみました。
#実行
パーティクルの噴出に合わせてポストプロセスが動作し二回暗部が真っ赤になるのが見えます!
ボリュームが設定してあるので範囲外にカメラがいる場合は影響がでません。
#まとめ
NiagaraDataInterfaceを使って独自の実装を行うことで、エンジンを破壊することなくNiagaraにさまざまな処理を加えることが可能です。
ポストプロセスのほかにもカメラに対する処理や、ゲームに対するイベント処理を実装することもできそうですね。
VFXデザイナーがNiagaraを介してゲームに対して様々な影響を直接制御できるようになります。これは捗ります。
プラグインはあくまでサンプルとして公開させていただいております。
例えばカスタムポストプロセスマテリアルをカーブで制御したいんじゃーとか
カラコレをカーブでいじりたいんじゃーとかあると思いますので、
ぜひご自由に実装を追加してポストプロセスセッティングにつっこんであげてください。
無数のPostProcessVolumeが生まれてしまうと見た目もパフォーマンスのひどい事になりえるので、
必要なもの以外を間引く処理や、カリングなどの制御を追加しても良いですね!
それでは Let's Niagara!