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) \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
}
1 | |
每个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
12void 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;
}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
29if ( 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;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void 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.Acceleration - ModuleData.Drag * (ModuleData.Velocity - ModuleData.Wind),除了常量的加速度,还有一项和风的相对速度负相关的阻尼加速度。
至此,着色器部分已经清晰。
参数传递
前面提到,一些模块比如AddVelocity,它们的shader是不做任何事情的,那么它们的C++在做什么?
1
2
3
4
5
6
7
8
9void 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);
}UNiagaraStatelessModule_SolveVelocitiesAndForces中传递
1
2
3
4NiagaraStateless::FPhysicsBuildData& PhysicsBuildData = BuildContext.GetTransientBuildData<NiagaraStateless::FPhysicsBuildData>();
FModuleBuiltData* BuiltData = BuildContext.AllocateBuiltData<FModuleBuiltData>();
BuiltData->PhysicsData = PhysicsBuildData;
生成
正如着色器中看到的,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
16void 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
61void 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);
}
}
}
模拟
模拟路径
在标准Emitter中,可以指定GPU Sim、CPU Sim,但在lightweight
emitter中,并没有这样的选项。检查代码发现模拟路径由Renderer决定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14ENiagaraSimTarget UNiagaraStatelessEmitter::ComputeSimTarget() const
{
for (UNiagaraRendererProperties* Renderer : RendererProperties)
{
if (Renderer && Renderer->GetIsEnabled())
{
if (Renderer->IsSimTargetSupported(ENiagaraSimTarget::GPUComputeSim) == false)
{
return ENiagaraSimTarget::CPUSim;
}
}
}
return ENiagaraSimTarget::GPUComputeSim;
}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
46void 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
7FNiagaraStatelessEmitterInstance::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
21void 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
12void 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
6FNiagaraStatelessComputeManager::GetDataBuffer
FNiagaraRendererSprites::PrepareParticleSpriteRenderData
FNiagaraRendererSprites::GetDynamicMeshElements
FNiagaraSystemRenderData::GetDynamicMeshElements
FNiagaraSceneProxy::GetDynamicMeshElements
FDynamicMeshElementContext::GatherDynamicMeshElementsForPrimitive1
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;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14EComputeExecutionPath 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
4NiagaraStateless::FParticleSimulationContext::SimulateInternal
NiagaraStateless::FParticleSimulationContext::SimulateGPU
NiagaraStatelessComputeManagerPrivate::GenerateCPUDataForGPUSim
FNiagaraStatelessComputeManager::GetDataBuffer1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void 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
4UNiagaraStatelessEmitterDefault::GetShaderParametersMetadata
FNiagaraStatelessEmitterData::GetShaderParametersMetadata
NiagaraStatelessComputeManagerPrivate::GenerateGPUData
FNiagaraStatelessComputeManager::OnPreRender::__l2::<lambda_1>::operator()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
30void 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);
}
...
}1
2
3
4
5
6FNiagaraStatelessComputeManager::GetDataBuffer
FNiagaraRendererSprites::PrepareParticleSpriteRenderData
FNiagaraRendererSprites::GetDynamicMeshElements
FNiagaraSystemRenderData::GetDynamicMeshElements
FNiagaraSceneProxy::GetDynamicMeshElements
FDynamicMeshElementContext::GatherDynamicMeshElementsForPrimitive
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
11void UNiagaraStatelessEmitter::CacheFromCompiledData()
{
...
for (const UNiagaraStatelessModule* Module : Modules)
{
EmitterBuildContext.PreModuleBuild(ShaderParametersBuilder.GetParametersStructSize());
Module->BuildShaderParameters(ShaderParametersBuilder);
Module->BuildEmitterData(EmitterBuildContext);
}
...
}1
2
3
4void UNiagaraStatelessModule_InitialMeshOrientation::BuildShaderParameters(FNiagaraStatelessShaderParametersBuilder& ShaderParametersBuilder) const
{
ShaderParametersBuilder.AddParameterNestedStruct<FParameters>();
}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;
}1
#define PARTICLE_ATTRIBUTE_OUTPUT(TYPE, NAME) if ( IsValidComponent(Permutation_##NAME##Component) ) { OutputComponentData(OutputIndex, Permutation_##NAME##Component, Particle.##NAME); }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
100void 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;
}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
29NiagaraStateless::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 | |
这样设计的好处是始终只有一个shader,没有变体,坏处是存在一定的冗余计算,且不能像标准emitter一样自定义脚本、调整模块执行顺序。
管线位置
粒子Simulate位于管线非常靠前的位置,在场景渲染的一开始,OnPreRender。渲染时机取决于材质类型,由于和标准emitter复用的同样代码,这里不展开介绍,留待后文。