1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unreal Engine 5のNiagaraで採用されたPosition型とは何なのか

Last updated at Posted at 2024-08-10

Position型の疑問

Unreal Engine 5になり、NiagaraにPosition型が導入されました。

Unreal Engine 5のLWC(Large World Coordinate)対応の一環と読めますが、既存のVector型とはどう異なるのでしょうか。

特にエンジニアとして気になるのは、Position型を使う事に伴うパフォーマンス低下や精度低下はないのかかと思うので、そこを中心に見てみました。

尚、検証に使ったUnreal Engineのバージョンは5.3.2です。

Unreal Engine 5ではLWCの導入に伴い、C++やBlueprintのVector型は倍精度浮動小数点数に変更されました。しかし、倍精度が使われるのはCPUが処理する領域に限定されるようです。先に答えを書くと、NiagaraのPosition型やVector型は単精度(float3)が使われ続けます。ただし、属するタイルがNiagara Systemのインスタンス毎に保持されており、LWCの中で正しい位置に描画がされるようになっています。

Position型の定義

まず、Position型の定義を確認します。

上の公式の解説を見ると、Position型はエミッタのLocal Spaceにチェックを入れているかどうかで解釈が変わります。デフォルトではLocal Spaceチェックは入っておらず、そのエミッタをWorld Space Emitterと呼びます。Local Spaceにチェックを入れると、そのエミッタはLocal Space Emitterと呼びます。

Local Space Emitterの場合は、Position型の原点はNiagara Systemのインスタンスの場所とされており、それはNiagara Componentの配置されている場所になります。一方、World Space Emitterの場合は、Position型の原点はNiagara Systemのインスタンスが属するタイルの原点になります。

Local SpaceへのチェックがPositionの解釈にどう影響するか簡単な実験をしてみます。Vortex Originを0,0,0としたVortex Velocityモジュールを入れました。これで、Positionの原点を中心にパーティクルが回転するはずです。

image.png

Local Space Emitterにすると、Niagara Actor(Niagara Component)を中心にパーティクルが回転しました。
Qiita-Local.gif

World Space Emitterにすると、ワールド原点にいるマネキンを中心にパーティクルが回転しました。
Qiita-World.gif

World Space Emitterの時は、Niagara Componentの中心はSimulation Position, もしくは、
image.png
というアトリビュートでPositionの原点からNiagara Componentの距離が管理されており、エミッタやパーティクルがどこに配置されてもPositionの原点の位置に関わらず意図通り実行できるようになっています。また、Local Space Emitterの場合、SimulationPositionは0,0,0が入っているものと思われます。

VFX制作者にとってのLocal Space EmitterとWorld Space Emitterの違いは
gameDev Outpostさんの解説がわかりやすいです。
https://www.youtube.com/watch?v=KEJx7ZX25gY

Position型もVector型もfloat3

出力されたHLSLを見てみると、Position型もVector型もfloat3として定義されていました。諸々考えると納得です。時に万単位のパーティクルを処理する事を考えると倍精度を使わない事はパフォーマンス面で合理的であり、純粋な視覚効果であるNiagaraは基本的にカメラ付近でのみ実行されるため、倍精度がそもそも不要と思えます。

以下では、MyPositionはPosition型、MyVectorはVector型で宣言しています。

HLSL
/*0102*/		struct FParamMap0_Particles
/*0103*/		{
/*0104*/			float Age;
/*0105*/			float Lifetime;
/*0106*/			float Mass;
/*0107*/			float MaterialRandom;
/*0108*/			float3 MyPosition;
/*0109*/			float3 MyVector;
/*0110*/			float NormalizedAge;
/*0111*/			float3 Position;
/*0112*/			FParamMap0_Particles_Previous Previous;
/*0113*/			float RibbonUVDistance;
/*0114*/			int UniqueID;
/*0115*/		};

上のコードは、適当なScratch Pad Moduleを作り、適当に値を宣言して生成しました。
image.png

PositionとVectorを相互変換するとどんなコードが生成されるか

Position型からVector型に変換するそれらしき名前のノードが2つありますが、中身は全くの別物なので要注意です。それぞれを使用してMyVectorにPositionを代入するコードを見てみます。

image.png

まず、Position -> Vector ノードを使用した時は、以下のようにfloat3同士を直接代入するのと同等のコードが出力されました。

HLSL
/*0282*/		void ScratchModule_Emitter_Func_(inout FSimulationContext Context)
/*0283*/		{
/*0284*/			float3 Output1;
/*0285*/			Output1.x = Context.Map.Particles.Position.x;
/*0286*/			Output1.y = Context.Map.Particles.Position.y;
/*0287*/			Output1.z = Context.Map.Particles.Position.z;
/*0288*/			Context.Map.Particles.MyVector = Output1;
/*0289*/		}

一方Position to Vectorを使用した時は、その見た目に反し比較的大掛かりなコードが生成されました。しかしやっていることは簡単で、Context.Map.Particles.Positionに対してContext.Map.Engine.Owner.LWCTileで与えられたタイル座標をオフセットしてContext.Map.Particles.MyVectorに代入していることがわかります。

HLSL
/*0284*/		void NiagaraScript_280_Particle_Func_(float4 In_FLOAT4_VAR, int In_TargetChannel, out float Out_NewOutput, inout FSimulationContext Context)
/*0285*/		{
/*0286*/			float X;
/*0287*/			float Y;
/*0288*/			float Z;
/*0289*/			float W;
/*0290*/			X = In_FLOAT4_VAR.x;
/*0291*/			Y = In_FLOAT4_VAR.y;
/*0292*/			Z = In_FLOAT4_VAR.z;
/*0293*/			W = In_FLOAT4_VAR.w;
/*0294*/			float Constant10 = (0.0);
/*0295*/			float NiagaraFloat_SelectResult = Constant10;
/*0296*/			NiagaraFloat_SelectResult = X;
/*0297*/			if(In_TargetChannel == 1)
/*0298*/			{ 
/*0299*/			NiagaraFloat_SelectResult = Y;
/*0300*/			}
/*0301*/			if(In_TargetChannel == 2)
/*0302*/			{ 
/*0303*/			NiagaraFloat_SelectResult = Z;
/*0304*/			}
/*0305*/			if(In_TargetChannel == 3)
/*0306*/			{ 
/*0307*/			NiagaraFloat_SelectResult = W;
/*0308*/			}
/*0309*/			Out_NewOutput = NiagaraFloat_SelectResult;
/*0310*/		}
/*0311*/		
/*0312*/		void NiagaraScript_279_Particle_Func_(float3 In_Position, float4x4 In_Engine_Owner_SystemLocalToWorld, float4 In_Engine_Owner_LWCTile, out float3 Out_Vector, inout FSimulationContext Context)
/*0313*/		{
/*0314*/			float3 Output1;
/*0315*/			Output1.x = Context.Map.Engine.Owner.LWCTile.x;
/*0316*/			Output1.y = Context.Map.Engine.Owner.LWCTile.y;
/*0317*/			Output1.z = Context.Map.Engine.Owner.LWCTile.z;
/*0318*/			int Constant9 = 3;
/*0319*/			float NiagaraScript_280_Particle_Func_Output_NewOutput;
/*0320*/			NiagaraScript_280_Particle_Func_(Context.Map.Engine.Owner.LWCTile, Constant9, NiagaraScript_280_Particle_Func_Output_NewOutput, Context);
/*0321*/			float3 Result10 = Output1 * NiagaraScript_280_Particle_Func_Output_NewOutput;
/*0322*/			float3 Output11;
/*0323*/			Output11.x = In_Position.x;
/*0324*/			Output11.y = In_Position.y;
/*0325*/			Output11.z = In_Position.z;
/*0326*/			float3 Result11 = Result10 + Output11;
/*0327*/			Out_Vector = Result11;
/*0328*/		}
/*0329*/		
/*0330*/		void ScratchModule_Emitter_Func_(inout FSimulationContext Context)
/*0331*/		{
/*0332*/			float4x4 Constant7 = float4x4(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);
/*0333*/			float4 Constant8 = float4(0,0,0,0);
/*0334*/			float3 NiagaraScript_279_Particle_Func_Output_Vector;
/*0335*/			NiagaraScript_279_Particle_Func_(Context.Map.Particles.Position, Constant7, Constant8, NiagaraScript_279_Particle_Func_Output_Vector, Context);
/*0336*/			Context.Map.Particles.MyVector = NiagaraScript_279_Particle_Func_Output_Vector;
/*0337*/		}

Position to Vectorの実装をみるとより意図が明確にわかります。Positionをワールド座標系に変換しているようです。まず、属するLWCのタイルの座標が加算されます。更にLocal Space Emitterの場合はPosition型の原点の行列を掛けています。
image.png

逆の変換を見てみます。Vector -> Positionノードは予想通り、float3同士を代入するのと同等のコードを生成しました。

image.png

HLSL
/*0281*/		void ScratchModule_01_Emitter_Func_(inout FSimulationContext Context)
/*0282*/		{
/*0283*/			float3 Output1;
/*0284*/			Output1.x = Context.Map.Particles.MyVector.x;
/*0285*/			Output1.y = Context.Map.Particles.MyVector.y;
/*0286*/			Output1.z = Context.Map.Particles.MyVector.z;
/*0287*/			Context.Map.Particles.Position = Output1;
/*0288*/		}

Vector to Positionという、Position to Vectorの逆の変換を行う関数があり、属するLWCのタイルの座標を減算するHLSLが生成されました。
image.png

HLSL
/*0281*/		void NiagaraScript_64_Particle_Func_(float4 In_FLOAT4_VAR, int In_TargetChannel, out float Out_NewOutput, inout FSimulationContext Context)
/*0282*/		{
/*0283*/			float X;
/*0284*/			float Y;
/*0285*/			float Z;
/*0286*/			float W;
/*0287*/			X = In_FLOAT4_VAR.x;
/*0288*/			Y = In_FLOAT4_VAR.y;
/*0289*/			Z = In_FLOAT4_VAR.z;
/*0290*/			W = In_FLOAT4_VAR.w;
/*0291*/			float Constant10 = (0.0);
/*0292*/			float NiagaraFloat_SelectResult = Constant10;
/*0293*/			NiagaraFloat_SelectResult = X;
/*0294*/			if(In_TargetChannel == 1)
/*0295*/			{ 
/*0296*/			NiagaraFloat_SelectResult = Y;
/*0297*/			}
/*0298*/			if(In_TargetChannel == 2)
/*0299*/			{ 
/*0300*/			NiagaraFloat_SelectResult = Z;
/*0301*/			}
/*0302*/			if(In_TargetChannel == 3)
/*0303*/			{ 
/*0304*/			NiagaraFloat_SelectResult = W;
/*0305*/			}
/*0306*/			Out_NewOutput = NiagaraFloat_SelectResult;
/*0307*/		}
/*0308*/		
/*0309*/		void NiagaraScript_63_Particle_Func_(float3 In_VECTOR_VAR, float4x4 In_Engine_Owner_SystemWorldToLocal, float4 In_Engine_Owner_LWCTile, out float3 Out_Position, inout FSimulationContext Context)
/*0310*/		{
/*0311*/			float3 Output1;
/*0312*/			Output1.x = Context.Map.Engine.Owner.LWCTile.x;
/*0313*/			Output1.y = Context.Map.Engine.Owner.LWCTile.y;
/*0314*/			Output1.z = Context.Map.Engine.Owner.LWCTile.z;
/*0315*/			int Constant9 = 3;
/*0316*/			float NiagaraScript_64_Particle_Func_Output_NewOutput;
/*0317*/			NiagaraScript_64_Particle_Func_(Context.Map.Engine.Owner.LWCTile, Constant9, NiagaraScript_64_Particle_Func_Output_NewOutput, Context);
/*0318*/			float3 Result10 = Output1 * NiagaraScript_64_Particle_Func_Output_NewOutput;
/*0319*/			float3 Result11 = In_VECTOR_VAR - Result10;
/*0320*/			float3 NiagaraPosition;
/*0321*/			NiagaraPosition.x = Result11.x;
/*0322*/			NiagaraPosition.y = Result11.y;
/*0323*/			NiagaraPosition.z = Result11.z;
/*0324*/			Out_Position = NiagaraPosition;
/*0325*/		}
/*0326*/		
/*0327*/		void ScratchModule_01_Emitter_Func_(inout FSimulationContext Context)
/*0328*/		{
/*0329*/			float4x4 Constant7 = float4x4(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);
/*0330*/			float4 Constant8 = float4(0,0,0,0);
/*0331*/			float3 NiagaraScript_63_Particle_Func_Output_Position;
/*0332*/			NiagaraScript_63_Particle_Func_(Context.Map.Particles.MyVector, Constant7, Constant8, NiagaraScript_63_Particle_Func_Output_Position, Context);
/*0333*/			Context.Map.Particles.Position = NiagaraScript_63_Particle_Func_Output_Position;
/*0334*/		}

Vector to Positionの実装を見ると、ワールド座標系であるVectorを属するタイルからの相対座標であるPositionに変換しています。

image.png

Vector to Positionノードも、Vector -> Positionノードとは全く異なる処理を行っているので要注意です。

LWC関連コード

生成されたHLSLを見ると、

HLSL
Context.Map.Engine.Owner.LWCTile

という、float4の変数が存在します。この変数は、Position to VectorまたはVector to Positionを使わない限りは生成されたHLSL内で利用されている形跡はありません。

Position to Vector等から生成されたHLSLの計算内容から推測するに、LWCTileのxyzはタイルのインデックス、wはタイル1枚あたりのサイズが入っているようです。

LWCTileはおそらくレンダラで使うのが主であり、VFX制作者はその存在を意識する必要はほぼなさそうです。

Vector to Position 及び、Position to Vector について

大抵のケースで使う必要はなく、むしろ間違って使われる事を危惧してしまいます。

考えられる実用的用途としては、

  • エンジニアがデバッグの用途で使う
  • LWCを使うゲームにおいてワールド座標を直打ち ※ただし精度低下には注意

などには使えそうです。

しかし、一方で以下のデメリットがあります。

  • NiagaraのVectorは単精度なので、ワールド座標をVectorに扱わせた瞬間から精度低下が起こる
  • VFX制作者が(Niagara内で)ワールド座標系やLWCを意識せずともよい事のメリットを壊してしまう

そして、その取り違えやすいノード名が気になりました。
Vector -> Position や、Position -> Vector と名前がとても似ています。
LWCTileがワールド原点にあるときはVector -> Position や Position -> Vectorと同じ結果が得られてしまうので、うっかり取り違えて気づかない、なんてことも起こりがちである気がします。

まとめ

新設されたPosition型は、Worldの任意地点を原点とした座標として明確に意味が定義され、Vector型とは区別されました。HLSLではVector型と同等のfloat3であり、相互に変換が可能で、変換の際に誤差が発生することもありません。

VFX制作者は、Vector to Positionノード等で意識的にワールド座標を使わない限りLWCを意識する必要はありません。LWC導入に伴って計算精度やパフォーマンスなどを追加で気にする事もありません。Position型がLWC対応に必須要素であったかというと、そういうものでもなさそうです。

Position型が明確な定義を持ち、可読性が上がったと言えます。つまり、Position型の登場は「学習コストが上がる怖いもの」ではなく、「可読性が上がる嬉しいもの」でした。

追記1:Position原点をワールド原点以外に乗せる検証

ワールド原点以外にある別のタイルがPosition原点になる様子を観察してみます。タイルサイズはLargeWorldRenderPosition.hに定義されており、約21キロメートルです。

LargeWorldRenderPosition.h
static constexpr double UE_LWC_RENDER_TILE_SIZE = 2097152.0;

目印のマネキンを2097152,0,0へ、Niagara Systemも同じX座標でYをマネキンから少々ずらして配置してみました。エミッタはWorld Position Emitterです。マネキンを中心に回転しています。

Qiita-2097152.gif

同じ条件で、目印のマネキンを2095000,0,0に、Niagara Systemも同じX座標でYを少々ずらして置きました。依然として2097152,0,0を中心に回転しています。どうやら最も近いタイル原点がPositionの原点として採択されるようです。

Qiita-2095000.gif

追記2:Convert Vector to Positionについて

Position型のモジュールの引数にVector型を指定したい時、Convert Vector to PositionというDynamic Inputを利用してPosition型に変換できます。

image.png

Convert Vector to Positionの第2引数で第1引数のVectorにワールド座標からの変換を入れるかどうかを選択できます。

image.png

Passthrough as Non Large World Positionを選択するとVector -> Position相当、Interpret as a Large World Position VectorでVector to Position相当のHLSLが生成されます。

Convert Vector to Positionの主な用途はUpdate Mesh Reproduction Spriteが出力するMesh PositionをPosition型の引数として使う時かと思います。この変数はその名に反してVector型です。どちらかというとConvert Vector to Positionを型変換に使う状況が何か間違っており、よってUpdate Mesh Reproduction Spriteのほうが修正されるべきと思うのですが、現時点の最新版であるUE5.4でもMesh PositionはVector型のままのようです。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?