概要
Niagaraのnamespaceを、生成されるHLSLを確認しながら、どのように実装されているかを見ていきます。
Niagaraにおけるnamespaceとは、Particles, Transient, LocalなどのUnreal Engineで定義済みの名前です。
のように、すべて大文字で表記されます。
HLSLにおいてはParticlesやTransient等のnamespaceは、
Context.Map.Particles.Position
のように、入れ子の構造体として実装されています。
namespace modifierはnamespaceを更にもう一段入れ子にします。
Context.Map.Particles.Initial.Position
namespace及びnamespace modifierは単なる名前ではなく、Niagaraによって名前ごとの機能が与えられており、それら機能を以下で紐解いていきます。エンジンのバージョンは5.3を使用しました。
一般的なプログラミング言語で言うnamespaceとは意味が異なる点は注意します。一般的なプログラミング言語のnamespaceは名前衝突を避ける以上の機能はありませんが、Niagaraにおいては各namespaceはそれぞれ明確に用途が定められており、namespaceが違うと生成するコードにも違いが生じます。
各namespaceについて
Particles
パーティクル単位で値を持ち、フレームを超えて値が保持されるのが特徴です。
Particles Update Scriptでは以下のように定義されます。
/*0112*/ struct FParamMap0_Particles
/*0113*/ {
/*0114*/ float Age;
/*0115*/ float Lifetime;
/*0116*/ float Mass;
/*0117*/ float MaterialRandom;
/*0118*/ float MyParticlesFloat; //←ここ
/*0119*/ float NormalizedAge;
/*0120*/ float3 Position;
/*0121*/ FParamMap0_Particles_Previous Previous;
/*0122*/ float RibbonUVDistance;
/*0123*/ FParamMap0_Particles_ShapeLocation ShapeLocation;
/*0124*/ int UniqueID;
/*0125*/ };
PositionやAgeなど、エピックゲームズで定義しているものが主ですが、VFX開発者が自由に定義することも出来、MyParticlesFloatがそれになります。
FParamMap0_Particles各値はフレームを超えて持ち越されます。そのためのコードが以下の通り自動的に生成されます。
Context.Map.Particles.MyParticlesFloat = InputDataFloat(0, 4);
...
OutputDataFloat(0, 4, TmpWriteIndex, Context.Map.Particles.MyParticlesFloat);
Transient
パーティクル単位で値を持てるのは Particlesと同等ですが、フレームを超えて値を持ち越すことができません。
Particles Update Scriptでは以下のように定義されます。
/*0086*/ struct FParamMap0_Transient
/*0087*/ {
/*0088*/ bool FirstFrame;
/*0089*/ float MyTransientFloat; //←ここ
/*0090*/ };
デフォルトで定義されているものは bool FirstFrame; だけで、VFX開発者が定義するものが主になります。
Local
パーティクル単位で値を持てるのは Transientと同等ですが、モジュールのインスタンス毎に領域が割り当てられる点においてTransientとは異なっています。よって、別のモジュールに同名の変数を定義した場合でも、別の変数として扱われます。
以下は2つのモジュールから、TransientとLocalで同名変数を作り、それぞれ値を代入した場合に生成されるコードです。
struct FParamMap0_Transient
{
bool FirstFrame;
float MyTransientFloat;
};
...
struct FParamMap0_Local_MyScratchModule2
{
float MyLocalFloat;
};
struct FParamMap0_Local_MyScratchModule1
{
float MyLocalFloat;
};
struct FParamMap0_Local
{
FParamMap0_Local_MyScratchModule1 MyScratchModule1;
FParamMap0_Local_MyScratchModule2 MyScratchModule2;
};
...
struct FParamMap0
{
FParamMap0_Array Array;
FParamMap0_DataInstance DataInstance;
FParamMap0_Emitter Emitter;
FParamMap0_Engine Engine;
FParamMap0_Local Local;
FParamMap0_OUTPUT_VAR OUTPUT_VAR;
FParamMap0_Particles Particles;
FParamMap0_ParticleState ParticleState;
FParamMap0_Transient Transient;
};
...
void MyScratchModule1_Emitter_Func_(inout FSimulationContext Context)
{
float Constant8 = 0;
float Constant9 = 0;
Context.Map.Local.MyScratchModule1.MyLocalFloat = Constant8;
Context.Map.Transient.MyTransientFloat = Constant9;
}
void MyScratchModule2_Emitter_Func_(inout FSimulationContext Context)
{
float Constant10 = 1;
float Constant11 = 1;
Context.Map.Local.MyScratchModule2.MyLocalFloat = Constant10;
Context.Map.Transient.MyTransientFloat = Constant11;
}
2つのモジュールから各変数に値を代入するコードが生成されました。Transientは同一の変数に対して操作が行われる一方で、Localは異なる構造体のインスタンスに対して操作を行っていることがわかります。
Emitter
Particlesに似ていますが、emitter stageにおいてのみ書き込みが可能です。
particle stageにおいては、Map Getは可能ですが、Map Setは不可能です。
例えば、particle stageに以下のノードを配置したとします。
Map SetでEmitter namespaceを指定したことでエラーになってしまいました。
そこで以下のように、Map SetをLocalネームスペースに変更し、コンパイルを通してHLSLを見てみます。
cbuffer FNiagaraExternalParameters
{
float Emitter_MyEmitterFloat;
}
...
struct FParamMap0_Emitter
{
float Age;
float MyEmitterFloat;
int RandomSeed;
};
....
void MyScratchModule2_Emitter_Func_(inout FSimulationContext Context)
{
Context.Map.Local.MyScratchModule2.MyLocalFloat = Context.Map.Emitter.MyEmitterFloat;
}
....
void SimulateMain()
{
....
Context.Map.Emitter.MyEmitterFloat = Emitter_MyEmitterFloat;
....
}
Emitter namespaceはparticle stageにおいて書き込めない理由、それは実体がcbufferであるためということがわかりました。
もちろん、emitter stageにおいてはEmitter namespaceにMap Setすることが可能となっています。
StackContext
StackContext namespaceは、emitter stageではEmitter namespace、particle stageではParticles namespaceに翻訳されるようです。
STACKCONTEXTにマウスポインタをあてると説明文が出てきました。
実験として、emitter stageの "Emitter Update"で以下のノードを書いてみます。
結論から言うと、MyEmitterFloatとMyStackContextFloatは全く同等のコードを生成しました。
struct FParamMap0_Particles
{
float MyParticlesFloat;
};
...
struct FParamMap0_Empty
{
float Age;
float CurrentLoopDelay;
float CurrentLoopDuration;
float DistanceTraveled;
int ExecutionState;
int ExecutionStateSource;
int LoopCount;
float LoopedAge;
float MyEmitterFloat;
float MyStackContextFloat;
float NormalizedLoopAge;
FParamMap0_Empty_Scalability Scalability;
FParamMap0_Empty_SpawnRate SpawnRate;
};
...
void MyScratchModuleEmitterUpdate_Empty_Func_(inout FSimulationContext Context)
{
float Constant68 = 0;
float Constant69 = 0;
float Constant70 = 0;
Context.Map.Empty.MyEmitterFloat = Constant68;
Context.Map.Particles.MyParticlesFloat = Constant69;
Context.Map.Empty.MyStackContextFloat = Constant70;
}
...
void SimulateMain()
{
...
Context.Map.Empty.MyEmitterFloat = InputDataFloat(0, 5);
Context.Map.Empty.MyStackContextFloat = InputDataFloat(0, 6);
...
OutputDataFloat(0, 5, TmpWriteIndex, Context.Map.Empty.MyEmitterFloat);
OutputDataFloat(0, 6, TmpWriteIndex, Context.Map.Empty.MyStackContextFloat);
...
}
Context.Map.Particles.MyParticlesFloat は、コンパイルエラーこそ出さなかったものの、これを保存するコードがないため代入した値は捨てられるものと思われます。
また、同様にparticles stageの Particle Updateでも同様の実験をしてみました。
案の定、MyParticlesFloatとMyStackContextFloatは同等のコードを生成しました。
Particle Update Scriptは以下のとおりです。
struct FParamMap0_Particles
{
float Age;
float Lifetime;
float Mass;
float MaterialRandom;
float MyParticlesFloat;
float MyStackContextFloat;
float NormalizedAge;
float3 Position;
FParamMap0_Particles_Previous Previous;
float RibbonUVDistance;
FParamMap0_Particles_ShapeLocation ShapeLocation;
int UniqueID;
};
...
void MyScratchModule1_Emitter_Func_(inout FSimulationContext Context)
{
float Constant7 = 0;
float Constant8 = 0;
float Constant9 = 0;
float Constant10 = 0;
Context.Map.Particles.MyParticlesFloat = Constant7;
Context.Map.Local.MyScratchModule1.MyLocalFloat = Constant8;
Context.Map.Transient.MyTransientFloat = Constant9;
Context.Map.Particles.MyStackContextFloat = Constant10;
}
...
void SimulateMain()
{
...
Context.Map.Particles.MyParticlesFloat = InputDataFloat(0, 4);
Context.Map.Array.MyParticlesFloat = 4;
Context.Map.Particles.MyStackContextFloat = InputDataFloat(0, 5);
Context.Map.Array.MyStackContextFloat = 5;
...
OutputDataFloat(0, 4, TmpWriteIndex, Context.Map.Particles.MyParticlesFloat);
OutputDataFloat(0, 5, TmpWriteIndex, Context.Map.Particles.MyStackContextFloat);
...
}
また、Particle UpdateからEmitter Updateで設定した StackContext namespace のMyStackContextFloat に Emitter namespaceでアクセスできることを確認しました。
emitter stageで設定した値をparticle stageで読み込み、Particles.Colorに代入して表示される色が変化することを確認しました。
この際、cbufferとしてemitter stageから値が渡されていることがわかります。
cbuffer FNiagaraExternalParameters
{
float Emitter_MyStackContextFloat;
}
ここで以下のようにStackContext namespaceを指定すると、ここはparticle stageであるためParticles namespaceと翻訳され、Particles.MyStackContextFloatが読み込まれることになります。
Input
モジュールの引数として使われるInput namespaceを見てみます。
特に引数に何も指定していなかった場合、cbufferが実体となりました。
cbuffer FNiagaraExternalParameters
{
float Constants_Emitter_MyScratchModule1_MyInputFloat;
}
...
void MyScratchModule1_Emitter_Func_(inout FSimulationContext Context)
{
Context.Map.Particles.MyParticlesFloat = Constants_Emitter_MyScratchModule1_MyInputFloat;
}
定数ではなく変数を引数に渡してみます。
すると、FParamMap0_MyScratchModule1というモジュールの引数を集めた構造体を経由して、値が渡されるようになりました。
struct FParamMap0_MyScratchModule1
{
float MyInputFloat;
};
...
struct FParamMap0
{
FParamMap0_Array Array;
FParamMap0_DataInstance DataInstance;
FParamMap0_Emitter Emitter;
FParamMap0_Engine Engine;
FParamMap0_Local Local;
FParamMap0_MyScratchModule1 MyScratchModule1;
FParamMap0_OUTPUT_VAR OUTPUT_VAR;
FParamMap0_Particles Particles;
FParamMap0_ParticleState ParticleState;
FParamMap0_Transient Transient;
};
...
void MyScratchModule1_Emitter_Func_(inout FSimulationContext Context)
{
Context.Map.Particles.MyParticlesFloat = Context.Map.MyScratchModule1.MyInputFloat;
}
void Simulate(inout FSimulationContext Context)
{
...
Context.Map.MyScratchModule1.MyInputFloat = Context.Map.Particles.Age;
...
}
Output
Output namespaceを指定すると、
という、間に MODULE というキーワードが挟まります。
ノードは以下のように組みました。
このHLSLを見てみます。
struct FParamMap0_OUTPUT_VAR_MyScratchModule1
{
float MyOutputFloat;
};
struct FParamMap0_OUTPUT_VAR
{
FParamMap0_OUTPUT_VAR_MyScratchModule1 MyScratchModule1;
FParamMap0_OUTPUT_VAR_ParticleState ParticleState;
};
...
struct FParamMap0
{
FParamMap0_Array Array;
FParamMap0_DataInstance DataInstance;
FParamMap0_Emitter Emitter;
FParamMap0_Engine Engine;
FParamMap0_Local Local;
FParamMap0_MyScratchModule1 MyScratchModule1;
FParamMap0_OUTPUT_VAR OUTPUT_VAR;
FParamMap0_Particles Particles;
FParamMap0_ParticleState ParticleState;
FParamMap0_Transient Transient;
};
...
void MyScratchModule1_Emitter_Func_(inout FSimulationContext Context)
{
Context.Map.OUTPUT_VAR.MyScratchModule1.MyOutputFloat = Context.Map.MyScratchModule1.MyInputFloat;
}
値はフレームを跨ぐための保存がなされないので、生成されるコードの性質はTransientに近いです。
Transientと異なるのは、その使い方です。
Output namespaceはMap Getで値を取得することはできず、そのかわりにモジュールの引数に指定できます。
その際の名前は、
のように、モジュール内で書き込む時には MODULE とされていた部分がモジュールの別名に置き換わっています。
この別名が便利で、同じ種類のモジュールを複数組み込んだ場合、モジュールの別名が異なることによりそれぞれの出力値が上書きされることなく保持されます。
具体的な使用例としては、以下の様に Static Mesh Locationを2つ使用して二者をLerp Positionというdynamic inputで線形補間します。この例により、モジュールの別名があることで2つのStatic Mesh Locationの出力値を区別できていることがわかります。
User
Blueprint等、外部から値を設定出来る変数になります。
cbufferで実装されていました。つまり、エミッタやパーティクルからは読み込み専用であり、原理的に書き込みはできません。
cbuffer FNiagaraExternalParameters
{
float4 User_MyParticleColor;
}
struct FParamMap0_User
{
float4 MyParticleColor;
};
...
void SimulateMain()
{
...
Context.Map.User.MyParticleColor = User_MyParticleColor;
...
Simulate(Context);
...
}
System
エミッタを束ねる Niagara Systemから設定できる値となります。
下記のように、パーティクルの色をRGをNiagara Systemから、GをNiagara Systemから設定しつつもStackcontext namespaceとし、AをUserから設定しました。
System stageのStackcontext namespaceはSystem namespaceと解釈されそうですが、当方で試した結果正しく値が設定されませんでした。Stackcontext namespaceをSystem namespaceに変更すると正しく動作しました。
コードを見ると、UserとSystemは同一のcbufferに配置されていました。
cbuffer FNiagaraExternalParameters
{
float System_MySystemFloat;
float System_MySystemStackcontextFloat;
float System_MySecondSystemFloat;
float User_MyUserFloat;
}
FNiagaraExternalParametersは、emitter stageからparticle stageに値が渡る場合のその値の格納場所でもあり、モジュールの引数に定数を用いた場合にその定数が格納される場所でもあります。つまり、定数値、User namespace、他のstageからの値渡しはみな同じcbufferを用いるため、パフォーマンスに大差ないと推測できます。
Engine
Unreal Engineから与えられ、VFX開発者からは読み取り専用の一連の変数です。以下はfloat型変数をengineの名前でフィルタリングした結果です。
Engine namespaceで一括りにされてはいますが、コードを確認すると複数のcbufferに分割されています。
cbuffer FNiagaraGlobalParameters
{
float Engine_WorldDeltaTime;
float Engine_DeltaTime;
float Engine_InverseDeltaTime;
float Engine_Time;
float Engine_RealTime;
int Engine_QualityLevel;
int Engine_PaddingInt32_0;
int Engine_PaddingInt32_1;
}
cbuffer FNiagaraSystemParameters
{
float Engine_Owner_TimeSinceRendered;
float Engine_Owner_LODDistance;
float Engine_Owner_LODDistanceFraction;
float Engine_System_Age;
int Engine_Owner_ExecutionState;
int Engine_System_TickCount;
int Engine_System_NumEmitters;
int Engine_System_NumEmittersAlive;
int Engine_System_SignificanceIndex;
int Engine_System_RandomSeed;
int Engine_System_CurrentTimeStep;
int Engine_System_NumTimeSteps;
float Engine_System_TimeStepFraction;
int Engine_System_NumParticles;
int Engine_System_PaddingInt32_0;
int Engine_System_PaddingInt32_1;
}
cbuffer FNiagaraOwnerParameters
{
float4x4 Engine_Owner_SystemLocalToWorld;
float4x4 Engine_Owner_SystemWorldToLocal;
float4x4 Engine_Owner_SystemLocalToWorldTransposed;
float4x4 Engine_Owner_SystemWorldToLocalTransposed;
float4x4 Engine_Owner_SystemLocalToWorldNoScale;
float4x4 Engine_Owner_SystemWorldToLocalNoScale;
float4 Engine_Owner_Rotation;
float3 Engine_Owner_Position;
int Engine_Owner_PaddingInt32_0;
float3 Engine_Owner_Velocity;
int Engine_Owner_PaddingInt32_1;
float3 Engine_Owner_SystemXAxis;
int Engine_Owner_PaddingInt32_2;
float3 Engine_Owner_SystemYAxis;
int Engine_Owner_PaddingInt32_3;
float3 Engine_Owner_SystemZAxis;
int Engine_Owner_PaddingInt32_4;
float3 Engine_Owner_Scale;
int Engine_Owner_PaddingInt32_5;
float4 Engine_Owner_LWCTile;
}
cbuffer FNiagaraEmitterParameters
{
int Engine_Emitter_NumParticles;
int Engine_Emitter_TotalSpawnedParticles;
float Engine_Emitter_SpawnCountScale;
float Emitter_Age;
int Emitter_RandomSeed;
int Engine_Emitter_InstanceSeed;
int Engine_Emitter_PaddingInt32_0;
int Engine_Emitter_PaddingInt32_1;
}
おそらく、FNiagaraGlobalParametersは全てのNiagara Systemのインスタンスで共通、FNiagaraEmitterParametersはエミッタ単位、FNiagaraSystemParametersはNiagara System単位、FNiagaraOwnerParametersはアクターかNiagara Component単位でcbufferが生成されているのではと推測します。
Namespace Modifierについて
namespaceと変数名の間にもう一つ名前が挟まることがあります。これをNamespace Modifierと呼びます。
Initial
Initial Namespace Modifierは対応する名前の変数の初期値を保持します。
Particles.Initial.Position等の形をとります。エディタで見ると、
のように見えます。
Initial Namespace Modifierは明示的に生成する必要はありません。仮想的にはParticle Spawnステージの最後の時点で存在しているParticle namespaceの変数の全てを対象とし、Initial Namespace Modifierが付与されてその初期値が複製されます。
例えば、Particle Spawnステージの最後でParticle.Colorが存在していると、Particle.Initial.Colorに自動的に値が複製されます。
Particle Spawnステージの最後の時点の値がInitial Namespace Modifierが付与されて複製されることを実験で確かめてみます。Particles.MyColorという変数を三箇所で設定し、Particles.Initial.MyColorをパーティクルの色として設定するようにします。
プレビューに表示されたパーティクルの色から、Particle Spawnステージの最後で設定している水色がParticles.Initial.MyColorに複製されている事が確認できました。
仮想的にはParticle Spawnステージの最後で存在するParticles namespaceの変数全てがInitial Namespace Modifierが付与されて複製されます。しかし、実際の使用実績がないとコードは省略されるようです。
例えば、Particle Updateステージでfloat型の変数の一覧を見ると、Lifetime、Mass、MaterialRandomが選択肢にあります。
生成されたコードを見てみます。
struct FParamMap0_Particles_Initial
{
float4 MyColor;
};
struct FParamMap0_Particles
{
float Age;
float4 Color;
FParamMap0_Particles_Initial Initial;
float Lifetime;
float Mass;
float MaterialRandom;
float4 MyColor;
float NormalizedAge;
float3 Position;
FParamMap0_Particles_Previous Previous;
float RibbonUVDistance;
FParamMap0_Particles_ShapeLocation ShapeLocation;
float2 SpriteSize;
int UniqueID;
};
...
void SimulateMain()
{
...
Context.Map.Particles.Initial.MyColor.r = InputDataFloat(0, 6);
Context.Map.Array.Initial.MyColor = 6;
Context.Map.Particles.Initial.MyColor.g = InputDataFloat(0, 7);
Context.Map.Array.Initial.MyColor = 7;
Context.Map.Particles.Initial.MyColor.b = InputDataFloat(0, 8);
Context.Map.Array.Initial.MyColor = 8;
Context.Map.Particles.Initial.MyColor.a = InputDataFloat(0, 9);
Context.Map.Array.Initial.MyColor = 9;
...
Simulate(Context);
...
OutputDataFloat(0, 6, TmpWriteIndex, Context.Map.Particles.Initial.MyColor.r);
OutputDataFloat(0, 7, TmpWriteIndex, Context.Map.Particles.Initial.MyColor.g);
OutputDataFloat(0, 8, TmpWriteIndex, Context.Map.Particles.Initial.MyColor.b);
OutputDataFloat(0, 9, TmpWriteIndex, Context.Map.Particles.Initial.MyColor.a);
...
}
Initial namespace modifierが付与された変数のうち、実際にコードとして生成されていたのはParticle Updateステージで使用実績のあったParticles.Initial.MyColorのみです。一方、変数の一覧にあったParticles.Initial.Lifetimeその他は、仮想的には存在しているもののその実体を裏付けるコードは生成されていません。
Previous
Previous namespace modifierは対応する名前の変数の前回の値を保持します。
Particles.Previous.Position等の形をとります。エディタで見ると、
のように見えます。
Previous namespace modifierは明示的に生成する必要がありません。Particle Updateステージで使用されるParticles namespaceの変数であれば、対応するPrevious namespace modifierが付与された変数が仮想的に存在しています。
値が複製されるタイミングは、Particle Updateステージの最初です。値に変更を加える前であるため、前フレームの値となるわけです。
Previous namespace modifierも実際に使用することがコード実体を自動生成する条件となっています。例えば以下のようにMap GetでPrevious Namespace Modifierを指定して値を取得します。
このモジュールを使うと、以下のようなコードが得られました。
struct FParamMap0_Particles_Previous
{
float4 Color;
float Mass;
float3 Position;
float2 SpriteSize;
float3 Velocity;
};
...
struct FParamMap0_Particles
{
float Age;
float4 Color;
float DistanceTraveled;
float Lifetime;
float Mass;
float MaterialRandom;
float NormalizedAge;
float3 Position;
FParamMap0_Particles_Presolve Presolve;
FParamMap0_Particles_Previous Previous;
float RibbonUVDistance;
FParamMap0_Particles_ShapeLocation ShapeLocation;
float2 SpriteSize;
int UniqueID;
float3 Velocity;
};
...
void SimulateMain()
{
...
Context.Map.Particles.Previous.Color = Context.Map.Particles.Color;
Context.Map.Particles.Previous.Mass = Context.Map.Particles.Mass;
Context.Map.Particles.Previous.Position = Context.Map.Particles.Position;
Context.Map.Particles.Previous.SpriteSize = Context.Map.Particles.SpriteSize;
Context.Map.Particles.Previous.Velocity = Context.Map.Particles.Velocity;
...
Simulate(Context);
...
OutputDataFloat(0, 22, TmpWriteIndex, Context.Map.Particles.Previous.Color.r);
OutputDataFloat(0, 23, TmpWriteIndex, Context.Map.Particles.Previous.Color.g);
OutputDataFloat(0, 24, TmpWriteIndex, Context.Map.Particles.Previous.Color.b);
OutputDataFloat(0, 25, TmpWriteIndex, Context.Map.Particles.Previous.Color.a);
OutputDataFloat(0, 26, TmpWriteIndex, Context.Map.Particles.Previous.Mass);
OutputDataFloat(0, 27, TmpWriteIndex, Context.Map.Particles.Previous.Position.x);
OutputDataFloat(0, 28, TmpWriteIndex, Context.Map.Particles.Previous.Position.y);
OutputDataFloat(0, 29, TmpWriteIndex, Context.Map.Particles.Previous.Position.z);
OutputDataFloat(0, 30, TmpWriteIndex, Context.Map.Particles.Previous.SpriteSize.x);
OutputDataFloat(0, 31, TmpWriteIndex, Context.Map.Particles.Previous.SpriteSize.y);
OutputDataFloat(0, 32, TmpWriteIndex, Context.Map.Particles.Previous.Velocity.x);
OutputDataFloat(0, 33, TmpWriteIndex, Context.Map.Particles.Previous.Velocity.y);
OutputDataFloat(0, 34, TmpWriteIndex, Context.Map.Particles.Previous.Velocity.z);
...
}
フレームの最初にParticles.Previous.ColorとContext.Map.Particles.Previous.Massへ、
それぞれ対応する変数から値を代入していることが確認できました。Particles.Previous.Velocity等も同様に処理されていますが、これはSolve Forces And Velocity等の既存モジュールの中で使われているために生成されています。
Module
Module namespace modifierをモジュール内で使うと、エミッタで使用時にその名前がエミッタにおけるモジュールの別名に置き換わります。これは、Output namespaceで自動的に付与されるModuleというキーワードの正体です。
実は、Module namespace modifierはOutput namespace以外と組み合わせて使うことができます。
Particles namespaceとModule namespace modifierを組み合わせた例です。Output namespaceと異なるのは、フレームを超えてその値が保持されるコードが生成されることです。例えば以下のようなモジュールNMS_ColorModuleを作成します。
このモジュールをエミッタに3つ連ねて配置しました。
生成されるコードを確認します。
struct FParamMap0_Particles_NMS_ColorModule
{
float4 ModuleColor;
};
struct FParamMap0_Particles
{
float Age;
float Lifetime;
float Mass;
float MaterialRandom;
FParamMap0_Particles_NMS_ColorModule NMS_ColorModule;
FParamMap0_Particles_NMS_ColorModule001 NMS_ColorModule001;
FParamMap0_Particles_NMS_ColorModule002 NMS_ColorModule002;
float NormalizedAge;
float3 Position;
FParamMap0_Particles_Previous Previous;
float RibbonUVDistance;
int UniqueID;
};
...
void SimulateMain()
{
...
Context.Map.Particles.NMS_ColorModule.ModuleColor.r = InputDataFloat(0, 4);
Context.Map.Array.NMS_ColorModule.ModuleColor = 4;
Context.Map.Particles.NMS_ColorModule.ModuleColor.g = InputDataFloat(0, 5);
Context.Map.Array.NMS_ColorModule.ModuleColor = 5;
Context.Map.Particles.NMS_ColorModule.ModuleColor.b = InputDataFloat(0, 6);
Context.Map.Array.NMS_ColorModule.ModuleColor = 6;
Context.Map.Particles.NMS_ColorModule.ModuleColor.a = InputDataFloat(0, 7);
Context.Map.Array.NMS_ColorModule.ModuleColor = 7;
Context.Map.Particles.NMS_ColorModule001.ModuleColor.r = InputDataFloat(0, 8);
Context.Map.Array.NMS_ColorModule001.ModuleColor = 8;
Context.Map.Particles.NMS_ColorModule001.ModuleColor.g = InputDataFloat(0, 9);
Context.Map.Array.NMS_ColorModule001.ModuleColor = 9;
Context.Map.Particles.NMS_ColorModule001.ModuleColor.b = InputDataFloat(0, 10);
Context.Map.Array.NMS_ColorModule001.ModuleColor = 10;
Context.Map.Particles.NMS_ColorModule001.ModuleColor.a = InputDataFloat(0, 11);
Context.Map.Array.NMS_ColorModule001.ModuleColor = 11;
Context.Map.Particles.NMS_ColorModule002.ModuleColor.r = InputDataFloat(0, 12);
Context.Map.Array.NMS_ColorModule002.ModuleColor = 12;
Context.Map.Particles.NMS_ColorModule002.ModuleColor.g = InputDataFloat(0, 13);
Context.Map.Array.NMS_ColorModule002.ModuleColor = 13;
Context.Map.Particles.NMS_ColorModule002.ModuleColor.b = InputDataFloat(0, 14);
Context.Map.Array.NMS_ColorModule002.ModuleColor = 14;
Context.Map.Particles.NMS_ColorModule002.ModuleColor.a = InputDataFloat(0, 15);
Context.Map.Array.NMS_ColorModule002.ModuleColor = 15;
...
Simulate(Context);
...
OutputDataFloat(0, 4, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule.ModuleColor.r);
OutputDataFloat(0, 5, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule.ModuleColor.g);
OutputDataFloat(0, 6, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule.ModuleColor.b);
OutputDataFloat(0, 7, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule.ModuleColor.a);
OutputDataFloat(0, 8, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule001.ModuleColor.r);
OutputDataFloat(0, 9, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule001.ModuleColor.g);
OutputDataFloat(0, 10, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule001.ModuleColor.b);
OutputDataFloat(0, 11, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule001.ModuleColor.a);
OutputDataFloat(0, 12, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule002.ModuleColor.r);
OutputDataFloat(0, 13, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule002.ModuleColor.g);
OutputDataFloat(0, 14, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule002.ModuleColor.b);
OutputDataFloat(0, 15, TmpWriteIndex, Context.Map.Particles.NMS_ColorModule002.ModuleColor.a);
...
}
3つのモジュールのインスタンス毎にフレームを超えて値を保存するためのコードが生成されています。同一のモジュールの異なるインスタンスにそれぞれ異なるパラメータを与え、それぞれ独立に何かをシミュレートさせる、という用途に活用できそうです。
Custom
namespace modifierは自由に名前をつけることもできます。その場合は特にnamespace modifierとしての機能が付与されるわけではなく、単に名前の一部になります。
例えば以下は、 Particles.MyTestModifier.MyTest という変数に値を設定しています。
生成されるコードは以下の通りです。他のnamespace modifier同様に入れ子の構造体になります。それ以外は通常の変数と何もかわりません。
struct FParamMap0_Particles_MyTestModifier
{
float MyTest;
};
struct FParamMap0_Particles
{
float Age;
float4 Color;
float Lifetime;
float Mass;
float MaterialRandom;
FParamMap0_Particles_MyTestModifier MyTestModifier;
float NormalizedAge;
float3 Position;
FParamMap0_Particles_Previous Previous;
float RibbonUVDistance;
FParamMap0_Particles_ShapeLocation ShapeLocation;
float2 SpriteSize;
int UniqueID;
};
...
void SimulateMain()
{
...
Context.Map.Particles.MyTestModifier.MyTest = InputDataFloat(0, 8);
Context.Map.Array.MyTestModifier.MyTest = 8;
...
Simulate(Context);
...
OutputDataFloat(0, 8, TmpWriteIndex, Context.Map.Particles.MyTestModifier.MyTest);
...
}
その他
Output namespaceには2つ目のnamespace modifierを追加で付けることができます。
出力値は他のモジュールの引数として使用できました。
2つ目のnamespace modifierにもModuleという名前をつけることもできます。
その際、モジュールの別名に置き換えられるのは1つ目のModule namespace modifierのみとなりました。