Niagara(一):lightweight emitter (Stateless)

代码基于UE 5.6

背景

在Niagara中,可以右键创建lightweight emitter。相比标准emitter,这种emitter采用了stateless的模型,即粒子的模拟不依赖上一帧粒子的状态,不需要读上一帧粒子属性buffer,从而实现了更小的开销。但同样地,这也引入了局限性,由于没有状态,粒子属性只能和时间相关,大大限制了粒子的表达力。这里分析一下它的源码实现。

着色器

先看一下着色器部分。首先是粒子的属性定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Engine\Plugins\FX\Niagara\Shaders\Private\Stateless\NiagaraStatelessCommon.ush
struct FStatelessParticle
{
bool bAlive; // If the particle should remain alive or not (only evaluated as specific points)
uint UniqueIndex; // Unique particle index, sequential based on previous spawn info accumulation
float MaterialRandom; // Random float passed to materials
float Lifetime; // Overall lifetime for the particle
float Age; // Current age for the particle
float NormalizedAge; // Current normalized age for the particle
float PreviousAge; // Previous age for the particle
float PreviousNormalizedAge; // Previous normalized age for the particle

float DeltaTime; // Simulation DT
float InvDeltaTime; // Simulation Inverse DT

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// TBD this might produce sub optimal code, needs investigate and is temporary
#define PARTICLE_ATTRIBUTE_OUTPUT(TYPE, NAME) TYPE NAME##;
#define PARTICLE_ATTRIBUTE_OUTPUT_IMPLICIT(TYPE, NAME)
#define PARTICLE_ATTRIBUTE_TRANSIENT(TYPE, NAME) TYPE NAME##;
PARTICLE_ATTRIBUTES
#undef PARTICLE_ATTRIBUTE
#undef PARTICLE_ATTRIBUTE_OUTPUT_IMPLICIT
#undef PARTICLE_ATTRIBUTE_TRANSIENT
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
};
这里用了宏PARTICLE_ATTRIBUTES实现属性的可扩展性,引擎在NiagaraStatelessSimulationDefault.usf和NiagaraStatelessSimulationExample1.usf分别定义了不同的属性集合。NiagaraStatelessSimulationDefault.usf在C++侧有对应的实现,而NiagaraStatelessSimulationExample1.usf没有,看起来是一个历史遗留文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Engine\Plugins\FX\Niagara\Shaders\Private\Stateless\NiagaraStatelessSimulationDefault.usf
#define PARTICLE_ATTRIBUTES \
PARTICLE_ATTRIBUTE_OUTPUT_IMPLICIT(int, UniqueIndex) \
PARTICLE_ATTRIBUTE_OUTPUT_IMPLICIT(float, MaterialRandom) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, Position) \
PARTICLE_ATTRIBUTE_OUTPUT(float, CameraOffset) \
PARTICLE_ATTRIBUTE_OUTPUT(float4, Color) \
PARTICLE_ATTRIBUTE_OUTPUT(float4, DynamicMaterialParameter0) \
PARTICLE_ATTRIBUTE_OUTPUT(float4, DynamicMaterialParameter1) \
PARTICLE_ATTRIBUTE_OUTPUT(float4, DynamicMaterialParameter2) \
PARTICLE_ATTRIBUTE_OUTPUT(float4, DynamicMaterialParameter3) \
PARTICLE_ATTRIBUTE_OUTPUT(int, MeshIndex) \
PARTICLE_ATTRIBUTE_OUTPUT(float4, MeshOrientation) \
PARTICLE_ATTRIBUTE_OUTPUT(float, RibbonWidth) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, Scale) \
PARTICLE_ATTRIBUTE_OUTPUT(float2, SpriteSize) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, SpriteFacing) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, SpriteAlignment) \
PARTICLE_ATTRIBUTE_OUTPUT(float, SpriteRotation) \
PARTICLE_ATTRIBUTE_OUTPUT(float, SubImageIndex) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, Velocity) \
PARTICLE_ATTRIBUTE_OUTPUT(float, PreviousCameraOffset) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, PreviousPosition) \
PARTICLE_ATTRIBUTE_OUTPUT(float4, PreviousMeshOrientation) \
PARTICLE_ATTRIBUTE_OUTPUT(float, PreviousRibbonWidth) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, PreviousScale) \
PARTICLE_ATTRIBUTE_OUTPUT(float2, PreviousSpriteSize) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, PreviousSpriteFacing) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, PreviousSpriteAlignment) \
PARTICLE_ATTRIBUTE_OUTPUT(float, PreviousSpriteRotation) \
PARTICLE_ATTRIBUTE_OUTPUT(float3, PreviousVelocity) \
这些属性会在不同的模块中被使用到。 接下来看一下Stateless更新的具体逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Engine\Plugins\FX\Niagara\Shaders\Private\Stateless\NiagaraStatelessSimulationTemplate.ush
void StatelessMain(uint3 GroupId : SV_GroupID, int GroupThreadIndex : SV_GroupIndex)
{
// Convert thread ID into linear thread
uint LinearThreadId = GetUnWrappedDispatchThreadId(GroupId, GroupThreadIndex, THREADGROUP_SIZE);

// Initialize our particle data
FStatelessParticle Particle = (FStatelessParticle)0;;
Particle.Age = -1.0f;

// Find which spawn info we belong to and fill out details
uint ModuleSeedOffset = 0;
{
uint SpawnInfoIndex = LinearThreadId;
for ( int i=0; i < NIAGARA_MAX_GPU_SPAWN_INFOS; ++i )
{
const uint SpawnInfoNumActive = GET_SCALAR_ARRAY_ELEMENT(SpawnInfo_NumActive, i);
const uint SpawnInfoParticleOffset = GET_SCALAR_ARRAY_ELEMENT(SpawnInfo_ParticleOffset, i);
const uint SpawnInfoUniqueOffset = GET_SCALAR_ARRAY_ELEMENT(SpawnInfo_UniqueOffset, i);
const float SpawnInfoTime = GET_SCALAR_ARRAY_ELEMENT(SpawnInfo_Time, i);
const float SpawnInfoRate = GET_SCALAR_ARRAY_ELEMENT(SpawnInfo_Rate, i);
const float SpawnInfoLifetimeScale = GET_SCALAR_ARRAY_ELEMENT(SpawnInfo_LifetimeScale, i);
const float SpawnInfoLifetimeBias = GET_SCALAR_ARRAY_ELEMENT(SpawnInfo_LifetimeBias, i);
if ( SpawnInfoIndex < SpawnInfoNumActive )
{
const uint SpawnParticleIndex = SpawnInfoIndex + SpawnInfoParticleOffset;
Particle.UniqueIndex = SpawnInfoIndex + SpawnInfoUniqueOffset + SpawnInfoParticleOffset;
Particle.Age = Common_SimulationTime - (SpawnInfoTime + float(SpawnParticleIndex) * SpawnInfoRate);
GRandomSeedInternal = FNiagaraStatelessDefinitions::MakeRandomSeed(Common_RandomSeed, Particle.UniqueIndex, ModuleSeedOffset++, 0);
Particle.Lifetime = RandomScaleBiasFloat(0, SpawnInfoLifetimeScale, SpawnInfoLifetimeBias);
break;
}

SpawnInfoIndex -= SpawnInfoNumActive;
}
if ( Particle.Age < 0.0f )
{
return;
}
}

// Initialize variables / determine lifetime / etc
if ( Particle.Lifetime <= 0.0f || Particle.Age >= Particle.Lifetime )
{
return;
}

Particle.NormalizedAge = Particle.Age / Particle.Lifetime;

Particle.PreviousAge = max(Particle.Age - Common_SimulationDeltaTime, 0.0f);
Particle.PreviousNormalizedAge = Particle.PreviousAge / Particle.Lifetime;

Particle.DeltaTime = Common_SimulationDeltaTime;
Particle.InvDeltaTime = Common_SimulationInvDeltaTime;

Particle.bAlive = true;

Particle.MaterialRandom = RandomFloat(1);

// Run Particle Simulate
#define PARTICLE_MODULE(NAME) \
GRandomSeedInternal = FNiagaraStatelessDefinitions::MakeRandomSeed(Common_RandomSeed, Particle.UniqueIndex, ModuleSeedOffset++, 0); \
##NAME##_Simulate(Particle);

PARTICLE_MODULES

#undef PARTICLE_MODULE

if ( !Particle.bAlive )
{
return;
}

// Output Particles that are still alive
uint OutputIndex;

//WaveInterlockedAddScalar_(Common_GPUCountBuffer[Common_GPUCountBufferOffset], 1, Particle.OutputIndex);
InterlockedAdd(Common_GPUCountBuffer[Common_GPUCountBufferOffset], 1, OutputIndex);

#define PARTICLE_ATTRIBUTE_OUTPUT(TYPE, NAME) if ( IsValidComponent(Permutation_##NAME##Component) ) { OutputComponentData(OutputIndex, Permutation_##NAME##Component, Particle.##NAME); }
#define PARTICLE_ATTRIBUTE_OUTPUT_IMPLICIT(TYPE, NAME) if ( IsValidComponent(Permutation_##NAME##Component) ) { OutputComponentData(OutputIndex, Permutation_##NAME##Component, Particle.##NAME); }
#define PARTICLE_ATTRIBUTE_TRANSIENT(TYPE, NAME)
PARTICLE_ATTRIBUTES
#undef PARTICLE_ATTRIBUTE
#undef PARTICLE_ATTRIBUTE_OUTPUT_IMPLICIT
#undef PARTICLE_ATTRIBUTE_TRANSIENT
}
这里先通过一个for循环找到当前线程属于哪一组SpawnInfo的粒子(不过这样做存在一定的线程divergence),找到后设置粒子属性,注意这里不一定是新生成的粒子,也可以是旧粒子。这里会有多组SpawnInfo的原因见后文。这里的SpawnInfoNumActive不是活着的粒子数,而是最大的预留的粒子数。对于已经死的粒子,就直接return,否则调用Simulate相关的Module,Module同样通过宏展开,还给了每个Module独立的随机数种子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Defines modules and order of execution
#define PARTICLE_MODULES \
/* Initializer Modules */ \
PARTICLE_MODULE(InitializeParticle) \
PARTICLE_MODULE(InitialMeshOrientation) \
PARTICLE_MODULE(ShapeLocation) \
/* Before Solve */ \
PARTICLE_MODULE(Null) /* AddVelocity */ \
PARTICLE_MODULE(Null) /* AccelerationForce */ \
PARTICLE_MODULE(Null) /* CurlNoiseForce */ \
PARTICLE_MODULE(Null) /* Drag */ \
PARTICLE_MODULE(Null) /* GravityForce */ \
PARTICLE_MODULE(SolveVelocitiesAndForces) \
/* Post Solve */ \
PARTICLE_MODULE(CameraOffset) \
PARTICLE_MODULE(Null) /* DecalAttributes */ \
PARTICLE_MODULE(DynamicMaterialParameters) \
PARTICLE_MODULE(Null) /* LightAttributes */ \
PARTICLE_MODULE(MeshIndex) \
PARTICLE_MODULE(MeshRotationRate) \
PARTICLE_MODULE(ScaleColor) \
PARTICLE_MODULE(ScaleRibbonWidth) \
PARTICLE_MODULE(ScaleSpriteSize) \
PARTICLE_MODULE(ScaleSpriteSizeBySpeed) \
PARTICLE_MODULE(ScaleMeshSize) \
PARTICLE_MODULE(ScaleMeshSizeBySpeed) \
PARTICLE_MODULE(SpriteFacingAndAlignment) \
PARTICLE_MODULE(SpriteRotationRate) \
PARTICLE_MODULE(SubUVAnimation) \

每个module都要定义相关的simulate函数对粒子属性进行更新,以及模块所需的shader参数,这些都在NiagaraStatelessModule_模块名.ush中。 可以注意到这里有一些模块使用了Null,这些模块在shader层面不需要做任何操作,因此没有对应的ush,但在C++层面还需要做一些操作,这是为了对齐两侧,维护正确的ModuleSeedOffset,从而维护正确的模块内的GRandomSeedInternal。最后通过Common_GPUCountBuffer的InterlockedAdd记录当前存活粒子数量,得到粒子的输出id。这里通过定义不同于粒子属性定义里的PARTICLE_ATTRIBUTE_OUTPUT实现输出属性。Common_GPUCountBuffer在lightweight里并不涉及到CPU回读,因为粒子数量可以解析算出。 观察这个OutputComponentData函数可以发现

1
2
3
4
5
6
7
8
9
10
11
12
void OutputComponentData(uint OutputIndex, int ComponentOffset, float Value)
{
if (IsValidComponent(ComponentOffset))
{
Common_FloatOutputBuffer[GetComponentOffset(OutputIndex, ComponentOffset)] = Value;
}
}

uint GetComponentOffset(uint OutputIndex, int ComponentOffset)
{
return ComponentOffset * Common_OutputBufferStride + OutputIndex;
}
Stateless在buffer中的存储整体上是一个Structure of Array的结构,不同粒子的同一component(属性可以是float3,每一个float是一个component)会被放在一块,减少cache miss。Common_OutputBufferStride是一个emitter instance内共享的变量,和最大粒子数相关。 可以看到,整个更新过程完全不依赖粒子之前的状态,只需要读取SpawnInfos和模块着色器参数,实现了Stateless。看到这里,可能会很好奇,没有state,它要怎么计算粒子的速度、位置等需要时间累积的量?它的运动模型非常简单,加速度基本为常量,只需要知道初速度和时间,代入\(x=v_0t+\frac{1}{2}at^2\)即可。 先看下它的速度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if ( SolveVelocitiesAndForces_ConeVelocityEnabled != 0 )
{
const float ConeAngle = RandomScaleBiasFloat(5, SolveVelocitiesAndForces_ConeAngleScale, SolveVelocitiesAndForces_ConeAngleBias);
const float ConeRotation = RandomFloat(6) * UE_TWO_PI;

float2 scAng = SinCos(ConeAngle);
float2 scRot = SinCos(ConeRotation);
const float3 Direction = float3(scRot.x * scAng.x, scRot.y * scAng.x, scAng.y);

float VelocityScale = RandomScaleBiasFloat(7, SolveVelocitiesAndForces_ConeVelocityScale, SolveVelocitiesAndForces_ConeVelocityBias);
if ( SolveVelocitiesAndForces_ConeVelocityFalloff > 0.0f )
{
const float pf = pow(saturate(scAng.y), SolveVelocitiesAndForces_ConeVelocityFalloff * 10.0f);
VelocityScale *= lerp(1.0f, pf, SolveVelocitiesAndForces_ConeVelocityFalloff);
}

ModuleData.Velocity += RotateVectorByQuat(Direction, SolveVelocitiesAndForces_ConeQuat) * VelocityScale;
}

if ( SolveVelocitiesAndForces_PointVelocityEnabled != 0 )
{
const float3 FallbackDir = RandomUnitFloat3(8);
const float3 Delta = Particle.Position - SolveVelocitiesAndForces_PointOrigin;
const float3 Dir = SafeNormalize(Delta, FallbackDir);

const float VelocityScale = RandomScaleBiasFloat(9, SolveVelocitiesAndForces_PointVelocityScale, SolveVelocitiesAndForces_PointVelocityBias);

ModuleData.Velocity += Dir * VelocityScale;
}
它的Velocity模型比较简单,只依赖粒子生成时的位置。Particle.Position是在shapelocation里初始化的,这样就求出了初速度。这里说加速度基本为常量,是因为还有一种情况,考虑到线性相关。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void SolveVelocitiesAndForces_IntegratePosition(in FStatelessModule_SolveVelocitiesAndForces ModuleData, float Age, inout float3 Position)
{
if (ModuleData.Drag > 0.0001f)
{
const float3 TerminalVelocity = ModuleData.Acceleration * rcp(ModuleData.Drag) + ModuleData.Wind;
const float3 IntVelocity = ModuleData.Velocity - TerminalVelocity;
const float LambdaAge = (1.0f - exp(-(Age * ModuleData.Drag))) * rcp(ModuleData.Drag);
Position += IntVelocity * LambdaAge + TerminalVelocity * Age;
}
else
{
// without drag, we can use the simpler formula for Newtonian motion v*t + 1/2*a*t²
Position += Age * (ModuleData.Velocity + ModuleData.Wind) + 0.5f * ModuleData.Acceleration * Age * Age;
}
}
这里ModuleData.Drag > 0.0001f时实际上的加速度表现为ModuleData.Acceleration - ModuleData.Drag * (ModuleData.Velocity - ModuleData.Wind),除了常量的加速度,还有一项和风的相对速度负相关的阻尼加速度。 至此,着色器部分已经清晰。

参数传递

前面提到,一些模块比如AddVelocity,它们的shader是不做任何事情的,那么它们的C++在做什么?

1
2
3
4
5
6
7
8
9
void UNiagaraStatelessModule_AddVelocity::BuildEmitterData(const FNiagaraStatelessEmitterDataBuildContext& BuildContext) const
NiagaraStateless::FPhysicsBuildData& PhysicsBuildData = BuildContext.GetTransientBuildData<NiagaraStateless::FPhysicsBuildData>();
if (VelocityType == ENSM_VelocityType::Linear)
{
const FNiagaraStatelessRangeVector3 VelocityRange = BuildContext.ConvertDistributionToRange(LinearVelocityDistribution, FVector3f::ZeroVector);
PhysicsBuildData.VelocityCoordinateSpace = CoordinateSpace;
PhysicsBuildData.VelocityRange = BuildContext.ConvertDistributionToRange(LinearVelocityDistribution, FVector3f::ZeroVector);
PhysicsBuildData.LinearVelocityScale = BuildContext.ConvertDistributionToRange(LinearVelocityScale, 1.0f);
}
其他运动和力相关的模块也会设置PhysicsBuildData,最后在UNiagaraStatelessModule_SolveVelocitiesAndForces中传递
1
2
3
4
NiagaraStateless::FPhysicsBuildData& PhysicsBuildData = BuildContext.GetTransientBuildData<NiagaraStateless::FPhysicsBuildData>();

FModuleBuiltData* BuiltData = BuildContext.AllocateBuiltData<FModuleBuiltData>();
BuiltData->PhysicsData = PhysicsBuildData;
PhysicsBuildData在UNiagaraStatelessModule_SolveVelocitiesAndForces::SetShaderParameters中被传递给shader参数。

生成

正如着色器中看到的,Simulate阶段使用PARTICLE_MODULES定义使用到的模块,而Spawn阶段并没有这样的模块,完全根据SpawnInfo进行生成。也就是说GUI界面的每个Spawn模块都对应一个SpawnInfo。

SpawnInfos

一开始看到着色器的时候有个疑惑点,为什么SpawnInfos会是一个变长数组,按理Spawn Rate和Spawn Burst Instantaneous加起来最多就2个。分析后发现,和Simulate里的模块不同,Spawn里的模块可以重复添加,也就是一个emitter可以有多个Spawn Rate模块。但这并不是唯一因素,这里还涉及到Emitter的loop行为。搜索SpawnInfos.Add可以看到修改SpawnInfos的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void FNiagaraStatelessEmitterInstance::InitSpawnInfosForLoop(float InitializationAge)
{
...
FNiagaraStatelessRuntimeSpawnInfo& NewSpawnInfo = SpawnInfos.AddDefaulted_GetRef();
NewSpawnInfo.Type = ENiagaraStatelessSpawnInfoType::Burst;
NewSpawnInfo.UniqueOffset = UniqueIndexOffset;
NewSpawnInfo.SpawnTimeStart = SpawnTime;
NewSpawnInfo.SpawnTimeEnd = SpawnTime;
NewSpawnInfo.Amount = SpawnAmount;
NewSpawnInfo.LifetimeMin = LifetimeMin;
NewSpawnInfo.LifetimeMax = LifetimeMax;

UniqueIndexOffset += SpawnAmount;
bSpawnInfosDirty = true;
}
}
InitSpawnInfosForLoop出现在以下函数中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
void FNiagaraStatelessEmitterInstance::TickEmitterState()
{
...

// If we are not active we don't need to evaluate loops / scalability anymore
if (InternalExecutionState != ENiagaraExecutionState::Active )
{
return;
}

const FNiagaraEmitterStateData& EmitterState = EmitterData->EmitterState;
...

// Evaluate emitter state
if ( Age >= CurrentLoopAgeEnd )
{
// Do we only execute a single loop?
if (EmitterState.LoopBehavior == ENiagaraLoopBehavior::Once)
{
SetExecutionStateInternal(ENiagaraExecutionState::Inactive);
}
// Multi-loop inject our new spawn infos
else
{
// Keep looping until we find out which loop we are in as a small loop age + large DT could result in crossing multiple loops
do
{
++LoopCount;
if (EmitterState.LoopBehavior == ENiagaraLoopBehavior::Multiple && LoopCount >= EmitterState.LoopCount)
{
SetExecutionStateInternal(ENiagaraExecutionState::Inactive);
break;
}

if (EmitterState.bRecalculateDurationEachLoop)
{
CurrentLoopDuration = EvaluateDistribution(EmitterState.LoopDuration, RandomStream, RendererBindings, DefaultLoopDuration);
CurrentLoopDuration = FMath::Max(CurrentLoopDuration, DefaultLoopDuration);
}

if (EmitterState.bLoopDelayEnabled)
{
if (EmitterState.bDelayFirstLoopOnly)
{
CurrentLoopDelay = 0.0f;
}
else if (EmitterState.bRecalculateDelayEachLoop)
{
CurrentLoopDelay = EvaluateDistribution(EmitterState.LoopDelay, RandomStream, RendererBindings, DefaultLoopDelay);
CurrentLoopDelay = FMath::Max(CurrentLoopDelay, 0.0f);
}
}

CurrentLoopAgeStart = CurrentLoopAgeEnd;
CurrentLoopAgeEnd = CurrentLoopAgeStart + CurrentLoopDelay + CurrentLoopDuration;

InitSpawnInfosForLoop(CurrentLoopAgeStart);
} while (Age >= CurrentLoopAgeEnd);
}
}
}
可以看出,多个SpawnInfo实际上是为了解决Emitter Loop,Emitter一次loop结束时,它发射的particle还没有结束生命,所以仍需保留这部分粒子,新的粒子根据新的SpawnInfo生成。

模拟

模拟路径

在标准Emitter中,可以指定GPU Sim、CPU Sim,但在lightweight emitter中,并没有这样的选项。检查代码发现模拟路径由Renderer决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ENiagaraSimTarget UNiagaraStatelessEmitter::ComputeSimTarget() const
{
for (UNiagaraRendererProperties* Renderer : RendererProperties)
{
if (Renderer && Renderer->GetIsEnabled())
{
if (Renderer->IsSimTargetSupported(ENiagaraSimTarget::GPUComputeSim) == false)
{
return ENiagaraSimTarget::CPUSim;
}
}
}
return ENiagaraSimTarget::GPUComputeSim;
}
Component、Decal、Light Renderer只支持CPU Sim,其他的则支持CPU Sim和GPU Sim,因此会走GPUComputeSim。根据SimTarget进行分支
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void FNiagaraStatelessEmitterInstance::UpdateSimulationData(float DeltaSeconds)
{
...
// If CPU simulation execute immediately
if (SimTarget == ENiagaraSimTarget::CPUSim)
{
...
FNiagaraDataBuffer& DataBuffer = ParticleDataSet->BeginSimulate();

FParticleSimulationContext ParticleSimulation(EmitterData.Get(), ShaderParameters.Get(), RendererBindings.GetParameterDataArray());
ParticleSimulation.Simulate(RandomSeed, Age, DeltaSeconds, SpawnInfos, &DataBuffer);
ParticleDataSet->EndSimulate();
}
// If GPU simulation send data to the RT
else if ( FEmitterInstance_RT* RenderThreadData = RenderThreadDataPtr.Get() )
{
FDataForRenderThread DataForRenderThread;
DataForRenderThread.Age = Age;
DataForRenderThread.ExecutionState = InternalExecutionState;
...
ENQUEUE_RENDER_COMMAND(UpdateStatelessAge)(
[RenderThreadData, EmitterData=MoveTemp(DataForRenderThread)](FRHICommandListImmediate& RHICmdList) mutable
{
RenderThreadData->DeltaTime = FMath::Max(EmitterData.Age - RenderThreadData->Age, 0.0f);
RenderThreadData->Age = EmitterData.Age;
RenderThreadData->ExecutionState = EmitterData.ExecutionState;

if (EmitterData.ShaderParameters)
{
RenderThreadData->ShaderParameters.Reset(EmitterData.ShaderParameters);
}

if (EmitterData.bHasBindingBufferData)
{
RenderThreadData->bBindingBufferDirty = true;
RenderThreadData->BindingBufferData = MoveTemp(EmitterData.BindingBufferData);
}

if (EmitterData.bHasSpawnInfoData)
{
RenderThreadData->SpawnInfos = MoveTemp(EmitterData.SpawnInfos);
}
}
);
}
}
调用栈如下
1
2
3
4
5
6
7
FNiagaraStatelessEmitterInstance::UpdateSimulationData
FNiagaraSystemInstance::Tick_Concurrent
FNiagaraSystemSimulation::FlushTickBatch
FNiagaraSystemSimulation::AddSystemToTickBatch
FNiagaraSystemSimulation::Tick_Concurrent
FNiagaraSystemSimulationTickConcurrentTask::DoTask
TGraphTask<FNiagaraSystemSimulationTickConcurrentTask>::ExecuteTask

CPU Sim

CPU Sim在UpdateSimulationData内直接执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void FParticleSimulationContext::Simulate(int32 InEmitterRandomSeed, float EmitterAge, float InDeltaTime, TConstArrayView<FNiagaraStatelessRuntimeSpawnInfo> SpawnInfos, FNiagaraDataBuffer* DestinationData)
{
NumInstances = 0;

FSpawnInfoShaderParameters SpawnParameters;
const uint32 ActiveParticles = EmitterData->CalculateActiveParticles(InEmitterRandomSeed, SpawnInfos, EmitterAge, &SpawnParameters);
if (ActiveParticles > 0)
{
// Setup data buffer pointers
DestinationData->Allocate(ActiveParticles);
BufferStride = DestinationData->GetFloatStride();
BufferFloatData = DestinationData->GetComponentPtrFloat(0);
BufferInt32Data = DestinationData->GetComponentPtrInt32(0);

// Run Simulation
SimulateInternal(InEmitterRandomSeed, EmitterAge, InDeltaTime, SpawnParameters, ActiveParticles);
}

// Set instance count
DestinationData->SetNumInstances(NumInstances);
}
SimulateInternal中调用各模块事先通过BuildEmitterData注册的模块ParticleSimulate函数。
1
2
3
4
5
6
7
8
9
10
11
12
void FParticleSimulationContext::SimulateInternal(int32 InEmitterRandomSeed, float EmitterAge, float InDeltaTime, FSpawnInfoShaderParameters& SpawnParameters, uint32 ActiveParticles)
{
...
// Execute the simulation
for (const auto& Callback : ExecData->SimulateFunctions)
{
BuiltDataOffset = Callback.BuiltDataOffset;
ShaderParameterOffset = Callback.ShaderParameterOffset;
ModuleRandomSeed = Callback.RandomSeedOffset;
Callback.Function(*this);
}
}

GPU Sim

GPU Sim的simulate在GetDataBuffer实现,调用栈如下

1
2
3
4
5
6
FNiagaraStatelessComputeManager::GetDataBuffer
FNiagaraRendererSprites::PrepareParticleSpriteRenderData
FNiagaraRendererSprites::GetDynamicMeshElements
FNiagaraSystemRenderData::GetDynamicMeshElements
FNiagaraSceneProxy::GetDynamicMeshElements
FDynamicMeshElementContext::GatherDynamicMeshElementsForPrimitive
这里的GetDynamicMeshElements并不是指movable的static mesh,而是绘制时的mesh batch(sprite的billboard quad),Niagara的proxy在构造时就会把自己标成Dynamic Relevance。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// Engine\Plugins\FX\Niagara\Source\Niagara\Private\Stateless\NiagaraStatelessComputeManager.cpp
FNiagaraDataBuffer* FNiagaraStatelessComputeManager::GetDataBuffer(FRHICommandListBase& RHICmdList, uintptr_t EmitterKey, const NiagaraStateless::FEmitterInstance_RT* EmitterInstance)
{
...
if (ComputeExecutionPath == EComputeExecutionPath::GPU)
{
FNiagaraGPUInstanceCountManager& CountManager = ComputeInterface->GetGPUInstanceCounterManager();
CountOffset = CountManager.AcquireEntry();
if (CountOffset != INDEX_NONE)
{
GPUGenerationRequests.Emplace(CacheData->DataBuffer, EmitterInstance, ActiveParticles);
CountsToRelease.Add(CountOffset);
}
}

...
CacheData->DataBuffer->AllocateGPU(RHICmdList, ActiveParticles, ComputeInterface->GetFeatureLevel(), TEXT("StatelessSimBuffer"));
...
bool bDidGenerateData = false;

switch (ComputeExecutionPath)
{
case EComputeExecutionPath::CPU:
{
bDidGenerateData = GenerateCPUDataForGPUSim(RHICmdList, EmitterInstance, CacheData->DataBuffer);
break;
}

case EComputeExecutionPath::GPU:
{
if (CountOffset != INDEX_NONE)
{
CacheData->DataBuffer->SetNumInstances(ActiveParticles);
CacheData->DataBuffer->SetGPUInstanceCountBufferOffset(CountOffset);
bDidGenerateData = true;
}
// If we failed to alocate a count we will need to go through the CPU path (if available)
// This should never happen as we reserve a count up front via the compute proxy
// If it does occur this means some other system has used a count slot but not reserved one
else
{
if (FNiagaraUtilities::LogVerboseWarnings())
{
ensureMsgf(false, TEXT("Count reserved for stateless was not available, attemping to generate on the CPU."));
}

const bool bAllowCPUExec = EnumHasAnyFlags(EmitterInstance->EmitterData->FeatureMask, ENiagaraStatelessFeatureMask::ExecuteCPU);
bDidGenerateData = bAllowCPUExec && GenerateCPUDataForGPUSim(RHICmdList, EmitterInstance, CacheData->DataBuffer);
}
break;
}

default:
ensureMsgf(false, TEXT("No execution path was found for stateless emitter, data will not be generated"));
break;
}

return bDidGenerateData ? CacheData->DataBuffer : nullptr;
}
这里需要分清楚两个概念,ENiagaraSimTarget(SimTarget)和EComputeExecutionPath。ENiagaraSimTarget指simulate后用于渲染的buffer在CPU还是GPU上,所以这里始终AllocateGPU,而Simulate本身是在CPU上执行还是在GPU上执行由EComputeExecutionPath决定的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
EComputeExecutionPath DetermineComputeExecutionPath(const FNiagaraStatelessEmitterData* EmitterData, uint32 ActiveParticlesEstimate, bool bAllowGPUGeneration)
{
const bool bAllowGPUExec = EnumHasAnyFlags(EmitterData->FeatureMask, ENiagaraStatelessFeatureMask::ExecuteGPU) && bAllowGPUGeneration;
const bool bUseCPUExec = EnumHasAnyFlags(EmitterData->FeatureMask, ENiagaraStatelessFeatureMask::ExecuteCPU) && (!bAllowGPUExec || (ActiveParticlesEstimate <= uint32(GParticleCountCPUThreshold)));
if (bUseCPUExec)
{
return EComputeExecutionPath::CPU;
}
if (bAllowGPUExec)
{
return EComputeExecutionPath::GPU;
}
return EComputeExecutionPath::None;
}
各模块有GetFeatureMask()说明该模块可以在哪个path上执行,所有feature mask的交集为最终的路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Engine\Plugins\FX\Niagara\Source\Niagara\Private\Stateless\NiagaraStatelessEmitter.cpp
void UNiagaraStatelessEmitter::CacheFromCompiledData()
{
for (const UNiagaraStatelessModule* Module : Modules)
{
if (Module->IsModuleEnabled())
{
StatelessEmitterData->FeatureMask &= Module->GetFeatureMask();
}
}

if (StatelessEmitterData->FeatureMask == ENiagaraStatelessFeatureMask::None)
{
StatelessEmitterData->bCanEverExecute = false;
UE_LOG(LogNiagara, Log, TEXT("Stateless Emitter (%s) can not execute on any available path and will be disabled."), *GetFullName());
}
}

CPU Execute

CPU Execute和CPU Sim一样使用了SimulateInternal进行模块的更新

1
2
3
4
NiagaraStateless::FParticleSimulationContext::SimulateInternal
NiagaraStateless::FParticleSimulationContext::SimulateGPU
NiagaraStatelessComputeManagerPrivate::GenerateCPUDataForGPUSim
FNiagaraStatelessComputeManager::GetDataBuffer
就是在SimulateGPU内通过RHICmdList.LockBuffer、RHICmdList.UnlockBuffer把GPU Buffer转成CPU可写的地址,通过DMA传输给GPU。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void FParticleSimulationContext::SimulateGPU(FRHICommandListBase& RHICmdList, int32 InEmitterRandomSeed, float EmitterAge, float InDeltaTime, TConstArrayView<FNiagaraStatelessRuntimeSpawnInfo> SpawnInfos, FNiagaraDataBuffer* DestinationData)
{
FRWBuffer& FloatBuffer = DestinationData->GetGPUBufferFloat();
FRWBuffer& Int32Buffer = DestinationData->GetGPUBufferInt();

// Setup data buffer pointers
BufferStride = DestinationData->GetFloatStride();
BufferFloatData = FloatBuffer.NumBytes > 0 ? reinterpret_cast<uint8*>(RHICmdList.LockBuffer(FloatBuffer.Buffer, 0, FloatBuffer.NumBytes, RLM_WriteOnly)) : nullptr;
BufferInt32Data = Int32Buffer.NumBytes > 0 ? reinterpret_cast<uint8*>(RHICmdList.LockBuffer(Int32Buffer.Buffer, 0, Int32Buffer.NumBytes, RLM_WriteOnly)) : nullptr;

// Run Simulation
SimulateInternal(InEmitterRandomSeed, EmitterAge, InDeltaTime, SpawnParameters, ActiveParticles);

// Unlock buffers
if (BufferFloatData)
{
RHICmdList.UnlockBuffer(FloatBuffer.Buffer);
}
if (BufferInt32Data)
{
RHICmdList.UnlockBuffer(Int32Buffer.Buffer);
}
}

GPU Execute

这里以Fountain Lightweight为例,注意走的不是SimulateGPU路径。GPU Sim的路径可以搜前面提到的NiagaraStatelessSimulationDefault.usf,定位到C++侧shader FSimulationShaderDefaultCS,从而得到如下的调用栈

1
2
3
4
UNiagaraStatelessEmitterDefault::GetShaderParametersMetadata
FNiagaraStatelessEmitterData::GetShaderParametersMetadata
NiagaraStatelessComputeManagerPrivate::GenerateGPUData
FNiagaraStatelessComputeManager::OnPreRender::__l2::<lambda_1>::operator()
GenerateGPUData内dispatch了相关的shader(外层GPU Marker为FNiagaraStatelessComputeManager_OnPreRender)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void GenerateGPUData(FRHICommandList& RHICmdList, FNiagaraGpuComputeDispatchInterface* ComputeInterface, TConstArrayView<FNiagaraStatelessComputeManager::FStatelessDataGenerationRequest> GenerationRequests)
{
...
for (const FNiagaraStatelessComputeManager::FStatelessDataGenerationRequest& GenerationRequest : GenerationRequests)
{
...
// Update parameters for this compute invocation
NiagaraStateless::FCommonShaderParameters* ShaderParameters = EmitterInstance->ShaderParameters.Get();
ShaderParameters->Common_SimulationTime = EmitterInstance->Age;
ShaderParameters->Common_SimulationDeltaTime = EmitterInstance->DeltaTime;
ShaderParameters->Common_SimulationInvDeltaTime = EmitterInstance->DeltaTime > 0.0f ? (1.0f / EmitterInstance->DeltaTime) : 0.0f;
ShaderParameters->Common_OutputBufferStride = DestinationData->GetFloatStride() / sizeof(float);
ShaderParameters->Common_GPUCountBufferOffset = DestinationData->GetGPUInstanceCountBufferOffset();
ShaderParameters->Common_FloatOutputBuffer = DestinationData->GetGPUBufferFloat().UAV.IsValid() ? DestinationData->GetGPUBufferFloat().UAV.GetReference() : EmptyFloatBufferUAV;
//ShaderParameters->Common_HalfOutputBuffer = DestinationData->GetGPUBufferHalf().UAV;
ShaderParameters->Common_IntOutputBuffer = DestinationData->GetGPUBufferInt().UAV.IsValid() ? DestinationData->GetGPUBufferInt().UAV.GetReference() : EmptyIntBufferUAV;
ShaderParameters->Common_GPUCountBuffer = CountBufferUAV;
ShaderParameters->Common_StaticFloatBuffer = EmitterData->StaticFloatBuffer.SRV;
ShaderParameters->Common_ParameterBuffer = FNiagaraRenderer::GetSrvOrDefaultUInt(EmitterInstance->BindingBuffer.SRV);

// Execute the simulation
TShaderRef<NiagaraStateless::FSimulationShader> ComputeShader = EmitterData->GetShader();
FRHIComputeShader* ComputeShaderRHI = ComputeShader.GetComputeShader();
const uint32 NumThreadGroups = FMath::DivideAndRoundUp<uint32>(GenerationRequest.ActiveParticles, NiagaraStateless::FSimulationShader::ThreadGroupSize);

const FIntVector NumWrappedThreadGroups = FComputeShaderUtils::GetGroupCountWrapped(NumThreadGroups);
FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader, EmitterData->GetShaderParametersMetadata(), *ShaderParameters, NumWrappedThreadGroups);
}
...
}
其中GPUGenerationRequests在FNiagaraStatelessComputeManager::GetDataBuffer进行添加。
1
2
3
4
5
6
FNiagaraStatelessComputeManager::GetDataBuffer
FNiagaraRendererSprites::PrepareParticleSpriteRenderData
FNiagaraRendererSprites::GetDynamicMeshElements
FNiagaraSystemRenderData::GetDynamicMeshElements
FNiagaraSceneProxy::GetDynamicMeshElements
FDynamicMeshElementContext::GatherDynamicMeshElementsForPrimitive
也就是说CPU Execute在GetDataBuffer时会进行simulate,而GPU Execute只是分配了buffer,维护一个pending list,在OnPreRender的时候再进行Simulate。

Buffer

shader中涉及到很多的共享buffer,这里把他们的共享粒度理清

  • Common_GPUCountBuffer: 全局共享(更准确地说,是per-world)。在stateless里主要用于给shader侧分配粒子output index。
  • Common_FloatOutputBuffer/Common_IntOutputBuffer: per emitter instance。粒子属性输出buffer。会优先尝试复用上一帧的,每帧会调用AllocateGPU,但只有当前数量多于原先分配的或小于原先分配的*ShrinkFactor时才会真的重新分配,这部分表现和标准emitter一致(复用代码)。
  • Common_StaticFloatBuffer: per emitter asset。用来存Curve之类的变长静态(编译期)模块参数,InitRenderResources 里建一次。
  • Common_ParameterBuffer:per emitter instance。主要用来存一些占据空间较大,不希望通过uniform传递的参数(比如float3),uniform仅传递offset。RendererBindings同样存在其中。

自定义

之前有意忽略了一个问题,那就是NiagaraStatelessSimulationDefault.usf里已经写死了模块和属性,lightweight emitter又是怎么实现模块、属性增减的?是不是找错了?其实没找错,关键是下面的代码

1
2
3
4
5
6
7
8
9
10
11
void UNiagaraStatelessEmitter::CacheFromCompiledData()
{
...
for (const UNiagaraStatelessModule* Module : Modules)
{
EmitterBuildContext.PreModuleBuild(ShaderParametersBuilder.GetParametersStructSize());
Module->BuildShaderParameters(ShaderParametersBuilder);
Module->BuildEmitterData(EmitterBuildContext);
}
...
}
这里的Modules是全量模块,也就是usf里所有的模块。PreModuleBuild和BuildShaderParameters用于分配模块参数的offset。
1
2
3
4
void UNiagaraStatelessModule_InitialMeshOrientation::BuildShaderParameters(FNiagaraStatelessShaderParametersBuilder& ShaderParametersBuilder) const
{
ShaderParametersBuilder.AddParameterNestedStruct<FParameters>();
}
也就是说模块参数的槽位始终会分配。 BuildEmitterData负责一些参数的计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Engine\Plugins\FX\Niagara\Source\Niagara\Internal\Stateless\Modules\NiagaraStatelessModule_AccelerationForce.h
virtual void BuildEmitterData(const FNiagaraStatelessEmitterDataBuildContext& BuildContext) const override
{
if (!IsModuleEnabled())
{
return;
}

const FNiagaraStatelessRangeVector3 AccelerationRange = AccelerationDistribution.CalculateRange(FVector3f::ZeroVector);

NiagaraStateless::FPhysicsBuildData& PhysicsBuildData = BuildContext.GetTransientBuildData<NiagaraStateless::FPhysicsBuildData>();
PhysicsBuildData.AccelerationCoordinateSpace = CoordinateSpace;
PhysicsBuildData.AccelerationRange.Min += AccelerationRange.Min;
PhysicsBuildData.AccelerationRange.Max += AccelerationRange.Max;
}
对于没有添加的模块,它的IsModuleEnabled()返回false,参数保留默认值。 GPU侧每个模块的simulate依然执行,实际执行时会保证传入默认构造的参数时等价于空操作。 对于输出属性,
1
#define PARTICLE_ATTRIBUTE_OUTPUT(TYPE, NAME) if ( IsValidComponent(Permutation_##NAME##Component) ) { OutputComponentData(OutputIndex, Permutation_##NAME##Component, Particle.##NAME); }
只会输出valid的。具体可以看下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
void UNiagaraStatelessEmitter::BuildCompiledDataSet()
{
...
if (const UNiagaraStatelessEmitterTemplate* EmitterTemplate = StatelessEmitterData->EmitterTemplate)
{
// Gather a list of all the output variables from the modules, this can change based on what is enabled / disabled
TArray<FNiagaraVariableBase> AvailableVariables;
...
// Module based variables
for (UNiagaraStatelessModule* Module : Modules)
{
if (Module->IsModuleEnabled())
{
Module->GetOutputVariables(AvailableVariables);
}
}

// Remove any variables we don't output
// Note: We only need to do this for GPU path as it can't vary the outputs
TConstArrayView<FNiagaraVariableBase> OutputComponents = EmitterTemplate->GetOututputComponents();
if (EnumHasAnyFlags(StatelessEmitterData->FeatureMask, ENiagaraStatelessFeatureMask::ExecuteGPU))
{
for (auto it = AvailableVariables.CreateIterator(); it; ++it)
{
if (!OutputComponents.Contains(*it))
{
UE_LOG(LogNiagara, Log, TEXT("Removed variable '%s' for emitter '%s' as it's not part of the output components"), *it->GetName().ToString(), *GetFullName());
it.RemoveCurrent();
}
}
}

// Force all the attributes in?
if (bForceOutputAllAttributes)
{
for (const FNiagaraVariableBase& Variable : AvailableVariables)
{
ParticleDataSetCompiledData.Variables.Emplace(Variable);
}
}
// Build data set from variables that are used by renderers
else
{
ForEachEnabledRenderer(
[this, &AvailableVariables](UNiagaraRendererProperties* RendererProps)
{
if (AvailableVariables.Num() == 0)
{
return;
}

for (FNiagaraVariableBase BoundAttribute : RendererProps->GetBoundAttributes())
{
// Edge condition with UniqueID which does not contain the Particle namespace from Ribbon Renderer
BoundAttribute.RemoveRootNamespace(FNiagaraConstants::ParticleAttributeNamespaceString);

const int32 Index = AvailableVariables.IndexOfByKey(BoundAttribute);
if (Index != INDEX_NONE)
{
AvailableVariables.RemoveAtSwap(Index, EAllowShrinking::No);
ParticleDataSetCompiledData.Variables.Emplace(BoundAttribute);
if (AvailableVariables.Num() == 0)
{
return;
}
}
}
}
);

if (bForceOutputUniqueID)
{
if (AvailableVariables.Contains(FNiagaraStatelessGlobals::Get().UniqueIDVariable))
{
ParticleDataSetCompiledData.Variables.Emplace(FNiagaraStatelessGlobals::Get().UniqueIDVariable);
}
}
}
//-TODO: We can alias variables in the data set, for example PreviousSpriteFacing could be SpriteFacing in some cases
ParticleDataSetCompiledData.BuildLayout();

// Create mapping from output components to data set for the shader to output
ComponentOffsets.Empty(OutputComponents.Num());
for (const FNiagaraVariableBase& OutputComponent : OutputComponents)
{
if (OutputComponent.GetType() == FNiagaraTypeDefinition::GetIntDef())
{
ComponentOffsets.Add(NiagaraStatelessInternal::GetDataSetIntOffset(ParticleDataSetCompiledData, OutputComponent));
}
else
{
ComponentOffsets.Add(NiagaraStatelessInternal::GetDataSetFloatOffset(ParticleDataSetCompiledData, OutputComponent));
}
}
ComponentOffsets.Shrink();
}
#endif
StatelessEmitterData->ParticleDataSetCompiledData = MakeShared<FNiagaraDataSetCompiledData>(ParticleDataSetCompiledData);
StatelessEmitterData->ComponentOffsets = ComponentOffsets;
}
首先根据启用的模块,得到所有输出变量AvailableVariables,然后根据AvailableVariables调用ParticleDataSetCompiledData.BuildLayout()得到最终的布局,基于layout查找各个OutputComponent的offset,记录到ComponentOffsets,找不到的记录为invalid index。ComponentOffsets进一步在AllocateShaderParameters调用template的SetShaderParameters进行设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
NiagaraStateless::FCommonShaderParameters* UNiagaraStatelessEmitter::AllocateShaderParameters(const FNiagaraStatelessSpaceTransforms& SpaceTransforms, const FNiagaraParameterStore& RendererBindings) const
{
// Allocate parameters
const FShaderParametersMetadata* ShaderParametersMetadata = StatelessEmitterData->GetShaderParametersMetadata();
const int32 ShaderParametersSize = ShaderParametersMetadata->GetSize();
void* UntypedShaderParameters = FMemory::Malloc(ShaderParametersSize, SHADER_PARAMETER_STRUCT_ALIGNMENT);
FMemory::Memset(UntypedShaderParameters, 0, ShaderParametersSize);

// Fill in all of the shader parameters
FNiagaraStatelessSetShaderParameterContext SetShaderParametersContext(
SpaceTransforms,
RendererBindings.GetParameterDataArray(),
StatelessEmitterData->BuiltData,
ShaderParametersMetadata,
static_cast<uint8*>(UntypedShaderParameters)
);

NiagaraStateless::FCommonShaderParameters* CommonParameters = SetShaderParametersContext.GetParameterNestedStruct<NiagaraStateless::FCommonShaderParameters>();

for (UNiagaraStatelessModule* Module : Modules)
{
Module->SetShaderParameters(SetShaderParametersContext);
}

//-TODO: Add a way to set them directly, we should know that the final struct is a series of ints in the order of the provided variables
GetEmitterTemplate()->SetShaderParameters(static_cast<uint8*>(UntypedShaderParameters), StatelessEmitterData->ComponentOffsets);

return CommonParameters;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void UNiagaraStatelessEmitterDefault::SetShaderParameters(uint8* ShaderParametersBase, TConstArrayView<int32> ComponentOffsets) const
{
using namespace NiagaraStateless;
FSimulationShaderDefaultCS::FParameters* ShaderParameters = reinterpret_cast<FSimulationShaderDefaultCS::FParameters*>(ShaderParametersBase);
int iComponent = 0;
ShaderParameters->Permutation_UniqueIndexComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_MaterialRandomComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PositionComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_CameraOffsetComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_ColorComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_DynamicMaterialParameter0Component= ComponentOffsets[iComponent++];
ShaderParameters->Permutation_DynamicMaterialParameter1Component= ComponentOffsets[iComponent++];
ShaderParameters->Permutation_DynamicMaterialParameter2Component= ComponentOffsets[iComponent++];
ShaderParameters->Permutation_DynamicMaterialParameter3Component= ComponentOffsets[iComponent++];
ShaderParameters->Permutation_MeshIndexComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_MeshOrientationComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_RibbonWidthComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_ScaleComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_SpriteSizeComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_SpriteFacingComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_SpriteAlignmentComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_SpriteRotationComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_SubImageIndexComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_VelocityComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousPositionComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousCameraOffsetComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousMeshOrientationComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousRibbonWidthComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousScaleComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousSpriteSizeComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousSpriteFacingComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousSpriteAlignmentComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousSpriteRotationComponent = ComponentOffsets[iComponent++];
ShaderParameters->Permutation_PreviousVelocityComponent = ComponentOffsets[iComponent++];
}

这样设计的好处是始终只有一个shader,没有变体,坏处是存在一定的冗余计算,且不能像标准emitter一样自定义脚本、调整模块执行顺序。

管线位置

粒子Simulate位于管线非常靠前的位置,在场景渲染的一开始,OnPreRender。渲染时机取决于材质类型,由于和标准emitter复用的同样代码,这里不展开介绍,留待后文。


Niagara(一):lightweight emitter (Stateless)
https://jhex-git.github.io/posts/3406797478/
作者
JointHex
发布于
2026年9月18日
许可协议