Niagara(二):Sprite粒子渲染

代码基于UE 5.6

在正式进入标准emitter之前,还是先把粒子的渲染给理清,这里以sprite为例。

MeshBatch创建

前文提到,GPU Sim的simulate在GetDataBuffer中分配了gpu buffer,调用栈如下

1
2
3
4
5
6
7
FNiagaraStatelessComputeManager::GetDataBuffer
FNiagaraRendererSprites::PrepareParticleSpriteRenderData
FNiagaraRendererSprites::GetDynamicMeshElements
FNiagaraSystemRenderData::GetDynamicMeshElements
FNiagaraSceneProxy::GetDynamicMeshElements
FDynamicMeshElementContext::GatherDynamicMeshElementsForPrimitive
FDynamicMeshElementContext::LaunchAsyncTask::__l2::<lambda_1>::operator()
FDynamicMeshElementContext::LaunchAsyncTask会遍历所有FPrimitiveSceneInfo*,其中包括niagara的primitive,每个Niagara System Instance都在其中注册了一个primitive,持有FNiagaraSceneProxy
1
2
3
4
5
6
7
8
9
10
11
12
UE::Tasks::FTask FDynamicMeshElementContext::LaunchAsyncTask(FDynamicPrimitiveIndexQueue* PrimitiveIndexQueue, UE::Tasks::ETaskPriority TaskPriority)
{
return Pipe.Launch(UE_SOURCE_LOCATION, [this, PrimitiveIndexQueue]
{
...
while (PrimitiveIndexQueue->Pop(PrimitiveIndex))
{
GatherDynamicMeshElementsForPrimitive(Primitives[PrimitiveIndex.Index], PrimitiveIndex.ViewMask);
}
...
}, TaskPriority);
}
FNiagaraSystemRenderData::GetDynamicMeshElements遍历所有的EmitterRenderers
1
2
3
4
5
6
7
8
9
10
11
void FNiagaraSystemRenderData::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector, const FNiagaraSceneProxy& SceneProxy)
{
for (int32 RendererIdx : RendererDrawOrder)
{
FNiagaraRenderer* Renderer = EmitterRenderers_RT[RendererIdx];
if (Renderer && (Renderer->GetSimTarget() != ENiagaraSimTarget::GPUComputeSim || FNiagaraUtilities::AllowGPUParticles(ViewFamily.GetShaderPlatform())))
{
Renderer->GetDynamicMeshElements(Views, ViewFamily, VisibilityMap, Collector, &SceneProxy);
}
}
}
FNiagaraRendererSprites::GetDynamicMeshElements中,
1
2
3
4
5
6
7
8
9
void FNiagaraRendererSprites::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector, const FNiagaraSceneProxy *SceneProxy) const
{
...
FParticleSpriteRenderData ParticleSpriteRenderData;
PrepareParticleSpriteRenderData(Collector.GetRHICommandList(), ParticleSpriteRenderData, ViewFamily, DynamicDataRender, SceneProxy, ENiagaraGpuComputeTickStage::Last);
...
PrepareParticleRenderBuffers(RHICmdList, ParticleSpriteRenderData, Collector.GetDynamicReadBuffer());
...
}
首先调用FNiagaraRendererSprites::PrepareParticleSpriteRenderData分配particle simulate的FNiagaraDataBuffer并传递给ParticleSpriteRenderData,是否需要cull、sort以及blend mode之类的渲染设置也都被写入ParticleSpriteRenderData。然后调用PrepareParticleRenderBuffers,根据之前获取好的FNiagaraDataBuffer,直接在ParticleSpriteRenderData写更底层的buffer相关的数据,比如SRV、stride之类的。如果是CPU Sim,还把cpu sim完的buffer打包传递到gpu上。
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
void FNiagaraRendererSprites::PrepareParticleRenderBuffers(FRHICommandListBase& RHICmdList, FParticleSpriteRenderData& ParticleSpriteRenderData, FGlobalDynamicReadBuffer& DynamicReadBuffer) const
{
if ( SourceMode == ENiagaraRendererSourceDataMode::Particles )
{
if ( SimTarget == ENiagaraSimTarget::CPUSim )
{
// For CPU simulations we do not gather int parameters inside TransferDataToGPU currently so we need to copy off
// integrate attributes if we are culling on the GPU.
TArray<uint32, TInlineAllocator<1>> IntParamsToCopy;
if (ParticleSpriteRenderData.bNeedsCull)
{
if (ParticleSpriteRenderData.bSortCullOnGpu)
{
if (RendererVisTagOffset != INDEX_NONE)
{
ParticleSpriteRenderData.RendererVisTagOffset = IntParamsToCopy.Add(RendererVisTagOffset);
}
}
else
{
ParticleSpriteRenderData.RendererVisTagOffset = RendererVisTagOffset;
}
}

FParticleRenderData ParticleRenderData = TransferDataToGPU(RHICmdList, DynamicReadBuffer, ParticleSpriteRenderData.RendererLayout, IntParamsToCopy, ParticleSpriteRenderData.SourceParticleData);
const uint32 NumInstances = ParticleSpriteRenderData.SourceParticleData->GetNumInstances();

ParticleSpriteRenderData.ParticleFloatSRV = GetSrvOrDefaultFloat(ParticleRenderData.FloatData);
ParticleSpriteRenderData.ParticleHalfSRV = GetSrvOrDefaultHalf(ParticleRenderData.HalfData);
ParticleSpriteRenderData.ParticleIntSRV = GetSrvOrDefaultInt(ParticleRenderData.IntData);
ParticleSpriteRenderData.ParticleFloatDataStride = ParticleRenderData.FloatStride / sizeof(float);
ParticleSpriteRenderData.ParticleHalfDataStride = ParticleRenderData.HalfStride / sizeof(FFloat16);
ParticleSpriteRenderData.ParticleIntDataStride = ParticleRenderData.IntStride / sizeof(int32);
}
else
{
ParticleSpriteRenderData.ParticleFloatSRV = GetSrvOrDefaultFloat(ParticleSpriteRenderData.SourceParticleData->GetGPUBufferFloat());
ParticleSpriteRenderData.ParticleHalfSRV = GetSrvOrDefaultHalf(ParticleSpriteRenderData.SourceParticleData->GetGPUBufferHalf());
ParticleSpriteRenderData.ParticleIntSRV = GetSrvOrDefaultInt(ParticleSpriteRenderData.SourceParticleData->GetGPUBufferInt());
ParticleSpriteRenderData.ParticleFloatDataStride = ParticleSpriteRenderData.SourceParticleData->GetFloatStride() / sizeof(float);
ParticleSpriteRenderData.ParticleHalfDataStride = ParticleSpriteRenderData.SourceParticleData->GetHalfStride() / sizeof(FFloat16);
ParticleSpriteRenderData.ParticleIntDataStride = ParticleSpriteRenderData.SourceParticleData->GetInt32Stride() / sizeof(int32);

ParticleSpriteRenderData.RendererVisTagOffset = RendererVisTagOffset;
}
}
else
{
ParticleSpriteRenderData.ParticleFloatSRV = FNiagaraRenderer::GetDummyFloatBuffer();
ParticleSpriteRenderData.ParticleHalfSRV = FNiagaraRenderer::GetDummyHalfBuffer();
ParticleSpriteRenderData.ParticleIntSRV = FNiagaraRenderer::GetDummyIntBuffer();
ParticleSpriteRenderData.ParticleFloatDataStride = 0;
ParticleSpriteRenderData.ParticleHalfDataStride = 0;
ParticleSpriteRenderData.ParticleIntDataStride = 0;
}
}
接下来做sort和cull。
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
void FNiagaraRendererSprites::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector, const FNiagaraSceneProxy *SceneProxy) const
{
...
FNiagaraGPUSortInfo SortInfo;
if (ParticleSpriteRenderData.bNeedsSort || ParticleSpriteRenderData.bNeedsCull)
{
InitializeSortInfo(ParticleSpriteRenderData, *SceneProxy, *View, ViewIndex, SortInfo);
}
FMeshCollectorResources* CollectorResources = &Collector.AllocateOneFrameResource<FMeshCollectorResources>();
FNiagaraSpriteVertexFactory& VertexFactory = CollectorResources->VertexFactory;

// Sort/Cull particles if needed.
uint32 NumInstances = SourceMode == ENiagaraRendererSourceDataMode::Particles ? ParticleSpriteRenderData.SourceParticleData->GetNumInstances() : 1;

VertexFactory.SetSortedIndices(nullptr, 0xFFFFFFFF);
FNiagaraGpuComputeDispatchInterface* ComputeDispatchInterface = SceneProxy->GetComputeDispatchInterface();
if (ParticleSpriteRenderData.bNeedsCull || ParticleSpriteRenderData.bNeedsSort)
{
if (ParticleSpriteRenderData.bSortCullOnGpu)
{
SortInfo.CulledGPUParticleCountOffset = ParticleSpriteRenderData.bNeedsCull ? ComputeDispatchInterface->GetGPUInstanceCounterManager().AcquireCulledEntry() : INDEX_NONE;
if (ComputeDispatchInterface->AddSortedGPUSimulation(RHICmdList, SortInfo))
{
VertexFactory.SetSortedIndices(SortInfo.AllocationInfo.BufferSRV, SortInfo.AllocationInfo.BufferOffset);
}
}
else
{
FGlobalDynamicReadBuffer::FAllocation SortedIndices;
SortedIndices = Collector.GetDynamicReadBuffer().AllocateUInt32(NumInstances);
NumInstances = SortAndCullIndices(SortInfo, *ParticleSpriteRenderData.SourceParticleData, SortedIndices);
VertexFactory.SetSortedIndices(SortedIndices.SRV, 0);
}
}
...
}
这里暂时不展开cull和sort。接着就是最关键的meshbatch的创建。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void FNiagaraRendererSprites::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector, const FNiagaraSceneProxy *SceneProxy) const
{
...
if (NumInstances > 0)
{
SetupVertexFactory(RHICmdList, ParticleSpriteRenderData, VertexFactory);
CollectorResources->UniformBuffer = CreateViewUniformBuffer(ParticleSpriteRenderData, *View, ViewFamily, *SceneProxy, VertexFactory);
VertexFactory.SetSpriteUniformBuffer(CollectorResources->UniformBuffer);

const uint32 GPUCountBufferOffset = SortInfo.CulledGPUParticleCountOffset != INDEX_NONE ? SortInfo.CulledGPUParticleCountOffset : ParticleSpriteRenderData.SourceParticleData->GetGPUInstanceCountBufferOffset();
FMeshBatch& MeshBatch = Collector.AllocateMesh();
CreateMeshBatchForView(RHICmdList, ParticleSpriteRenderData, MeshBatch, *View, *SceneProxy, VertexFactory, NumInstances, GPUCountBufferOffset, ParticleSpriteRenderData.bNeedsCull);
Collector.AddMesh(ViewIndex, MeshBatch);
...
}
}
其中SetupVertexFactory设置了facing和alignment。
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
void FNiagaraRendererSprites::CreateMeshBatchForView(
FRHICommandListBase& RHICmdList,
FParticleSpriteRenderData& ParticleSpriteRenderData,
FMeshBatch& MeshBatch,
const FSceneView& View,
const FNiagaraSceneProxy& SceneProxy,
FNiagaraSpriteVertexFactory& VertexFactory,
uint32 NumInstances,
uint32 GPUCountBufferOffset,
bool bDoGPUCulling
) const
{
FNiagaraSpriteVFLooseParameters VFLooseParams;
VFLooseParams.NiagaraParticleDataFloat = ParticleSpriteRenderData.ParticleFloatSRV;
VFLooseParams.NiagaraParticleDataHalf = ParticleSpriteRenderData.ParticleHalfSRV;
VFLooseParams.NiagaraFloatDataStride = FMath::Max(ParticleSpriteRenderData.ParticleFloatDataStride, ParticleSpriteRenderData.ParticleHalfDataStride);

FMaterialRenderProxy* MaterialRenderProxy = ParticleSpriteRenderData.DynamicDataSprites->Material;
check(MaterialRenderProxy);

VFLooseParams.CutoutParameters = VertexFactory.GetCutoutParameters();
VFLooseParams.CutoutGeometry = VertexFactory.GetCutoutGeometrySRV() ? VertexFactory.GetCutoutGeometrySRV() : GFNiagaraNullCutoutVertexBuffer.VertexBufferSRV.GetReference();
VFLooseParams.ParticleAlignmentMode = VertexFactory.GetAlignmentMode();
VFLooseParams.ParticleFacingMode = VertexFactory.GetFacingMode();
VFLooseParams.SortedIndices = VertexFactory.GetSortedIndicesSRV() ? VertexFactory.GetSortedIndicesSRV() : GFNiagaraNullSortedIndicesVertexBuffer.VertexBufferSRV.GetReference();
VFLooseParams.SortedIndicesOffset = VertexFactory.GetSortedIndicesOffset();

FNiagaraGPUInstanceCountManager::FIndirectArgSlot IndirectDraw;
if ((SourceMode == ENiagaraRendererSourceDataMode::Particles) && (GPUCountBufferOffset != INDEX_NONE))
{
FNiagaraGpuComputeDispatchInterface* ComputeDispatchInterface = SceneProxy.GetComputeDispatchInterface();
check(ComputeDispatchInterface);

IndirectDraw = ComputeDispatchInterface->GetGPUInstanceCounterManager().AddDrawIndirect(
RHICmdList,
GPUCountBufferOffset,
NumIndicesPerInstance,
0,
View.IsInstancedStereoPass(),
bDoGPUCulling,
ParticleSpriteRenderData.SourceParticleData->GetGPUDataReadyStage()
);
}
...
}
NumIndicesPerInstance来自于
1
2
3
4
5
6
7
8
9
10
11
12
uint32 UNiagaraSpriteRendererProperties::GetNumIndicesPerInstance() const
{
// This is a based on cutout vertices making a triangle strip.
if (GetNumCutoutVertexPerSubimage() == 8)
{
return 18;
}
else
{
return 6;
}
}
对于非cutout为6,也就是说本身渲染的时候并不是用triangle strip。这里的cutout主要是使用一个更精确的凸包替代矩形sprite覆盖非完全透明区域,减少完全透明区域生成fragment,和我们要探讨的核心问题无关,不再展开。 重点是AddDrawIndirect
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
FNiagaraGPUInstanceCountManager::FIndirectArgSlot FNiagaraGPUInstanceCountManager::AddDrawIndirect(FRHICommandListBase& RHICmdList, uint32 InstanceCountBufferOffset, uint32 NumIndicesPerInstance, uint32 StartIndexLocation, bool bIsInstancedStereoEnabled, bool bCulled, ENiagaraGpuComputeTickStage::Type ReadyTickStage)
{
UE::TScopeLock Lock(AddDrawIndirectGuard);

const ENiagaraDrawIndirectArgGenTaskFlags TaskFlags =
(bIsInstancedStereoEnabled ? ENiagaraDrawIndirectArgGenTaskFlags::InstancedStereo : ENiagaraDrawIndirectArgGenTaskFlags::None)
| (bCulled ? ENiagaraDrawIndirectArgGenTaskFlags::UseCulledCounts : ENiagaraDrawIndirectArgGenTaskFlags::None)
| (ReadyTickStage == ENiagaraGpuComputeTickStage::PostOpaqueRender ? ENiagaraDrawIndirectArgGenTaskFlags::PostOpaque : ENiagaraDrawIndirectArgGenTaskFlags::None);
FNiagaraDrawIndirectArgGenTaskInfo Info(InstanceCountBufferOffset, NumIndicesPerInstance, StartIndexLocation, TaskFlags);

FNiagaraDrawIndirectArgGenSlotInfo* SlotInfo = DrawIndirectArgMap.Find(Info);
if ( SlotInfo == nullptr )
{
// Attempt to allocate a new slot from the pool, or add to the pool if it's full
FIndirectArgsPoolEntry* PoolEntry = DrawIndirectPool.Num() > 0 ? DrawIndirectPool.Last().Get() : nullptr;
if (PoolEntry == nullptr || PoolEntry->UsedEntriesTotal >= PoolEntry->AllocatedEntries)
{
FIndirectArgsPoolEntryPtr NewEntry = MakeUnique<FIndirectArgsPoolEntry>();
NewEntry->AllocatedEntries = PoolEntry ? uint32(PoolEntry->AllocatedEntries * GNiagaraIndirectArgsPoolBlockSizeFactor) : uint32(GNiagaraIndirectArgsPoolMinSize);

INDIRECT_ARG_POOL_LOG("Increasing pool from size %d to %d", PoolEntry ? PoolEntry->AllocatedEntries : 0, NewEntry->AllocatedEntries);

TResourceArray<uint32> InitData;
InitData.AddZeroed(NewEntry->AllocatedEntries * NIAGARA_DRAW_INDIRECT_ARGS_SIZE);
NewEntry->Buffer.Initialize(RHICmdList, TEXT("NiagaraGPUDrawIndirectArgs"), sizeof(uint32), NewEntry->AllocatedEntries * NIAGARA_DRAW_INDIRECT_ARGS_SIZE, EPixelFormat::PF_R32_UINT, kIndirectArgsDefaultState, BUF_Static | BUF_DrawIndirect, &InitData);

PoolEntry = NewEntry.Get();
DrawIndirectPool.Emplace(MoveTemp(NewEntry));
}

Info.IndirectArgsBufferOffset = PoolEntry->UsedEntriesTotal * NIAGARA_DRAW_INDIRECT_ARGS_SIZE;
++PoolEntry->UsedEntriesTotal;

SlotInfo = &DrawIndirectArgMap.Add(Info);
SlotInfo->PoolIndex = DrawIndirectPool.Num() - 1;
SlotInfo->BufferOffset = Info.IndirectArgsBufferOffset * sizeof(uint32);

const ENiagaraGPUCountUpdatePhase::Type CountPhase = ReadyTickStage == ENiagaraGpuComputeTickStage::PostOpaqueRender ? ENiagaraGPUCountUpdatePhase::PostOpaque : ENiagaraGPUCountUpdatePhase::PreOpaque;
DrawIndirectArgGenTasks[CountPhase].Add(Info);
++PoolEntry->UsedEntries[CountPhase];
}

return FIndirectArgSlot(DrawIndirectPool[SlotInfo->PoolIndex]->Buffer.Buffer, DrawIndirectPool[SlotInfo->PoolIndex]->Buffer.SRV, SlotInfo->BufferOffset);
}
这个函数分配好了绘制niagara sprite所需的indirect draw的args buffer以及offset,但buffer内的参数填充通过记入DrawIndirectArgGenTasks延后处理(FNiagaraGPUInstanceCountManager::UpdateDrawIndirectBuffers)。这个buffer基于多个DrawIndirectPool,每个Pool都比前一个大2(GNiagaraIndirectArgsPoolBlockSizeFactor)倍,写满了创建新Pool。 在AddDrawIndirect后,把持有大量sprite渲染相关参数的VFLooseParams通过Uniform Buffer的形式传递给VertexFactory,再进一步传递给MeshBatch。FMeshBatch描述用哪个VF、哪个材质、什么拓扑、什么渲染状态画,FMeshBatchElement描述一次draw具体画哪一段index、多少个instance、是否indirect。
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
void FNiagaraRendererSprites::CreateMeshBatchForView(
FRHICommandListBase& RHICmdList,
FParticleSpriteRenderData& ParticleSpriteRenderData,
FMeshBatch& MeshBatch,
const FSceneView& View,
const FNiagaraSceneProxy& SceneProxy,
FNiagaraSpriteVertexFactory& VertexFactory,
uint32 NumInstances,
uint32 GPUCountBufferOffset,
bool bDoGPUCulling
) const
{
...
if (IndirectDraw.IsValid())
{
VFLooseParams.IndirectArgsBuffer = IndirectDraw.SRV;
VFLooseParams.IndirectArgsOffset = IndirectDraw.Offset / sizeof(uint32);
}
else
{
VFLooseParams.IndirectArgsBuffer = GFNiagaraNullSortedIndicesVertexBuffer.VertexBufferSRV;
VFLooseParams.IndirectArgsOffset = 0;
}

VertexFactory.SetLooseParameterUniformBuffer(FNiagaraSpriteVFLooseParametersRef::CreateUniformBufferImmediate(VFLooseParams, UniformBuffer_SingleFrame));

MeshBatch.VertexFactory = &VertexFactory;
MeshBatch.CastShadow = SceneProxy.CastsDynamicShadow() && bCastShadows;
#if RHI_RAYTRACING
MeshBatch.CastRayTracedShadow = SceneProxy.CastsDynamicShadow() && bCastShadows;
#endif
MeshBatch.bUseAsOccluder = false;
MeshBatch.ReverseCulling = SceneProxy.IsLocalToWorldDeterminantNegative();
MeshBatch.Type = PT_TriangleList;
MeshBatch.DepthPriorityGroup = SceneProxy.GetDepthPriorityGroup(&View);
MeshBatch.bCanApplyViewModeOverrides = true;
MeshBatch.bUseWireframeSelectionColoring = SceneProxy.IsSelected();
MeshBatch.SegmentIndex = 0;

#if WITH_EDITORONLY_DATA
if (bIncludeInHitProxy == false)
{
MeshBatch.BatchHitProxyId = FHitProxyId::InvisibleHitProxyId;
}
#endif

const bool bIsWireframe = View.Family->EngineShowFlags.Wireframe;
if (bIsWireframe)
{
MeshBatch.MaterialRenderProxy = UMaterial::GetDefaultMaterial(MD_Surface)->GetRenderProxy();
}
else
{
MeshBatch.MaterialRenderProxy = MaterialRenderProxy;
}

FMeshBatchElement& MeshElement = MeshBatch.Elements[0];
MeshElement.IndexBuffer = &GParticleIndexBuffer;
MeshElement.FirstIndex = 0;
MeshElement.NumPrimitives = NumIndicesPerInstance / 3;
MeshElement.NumInstances = FMath::Max(0u, NumInstances);
MeshElement.MinVertexIndex = 0;
MeshElement.MaxVertexIndex = 0;
MeshElement.PrimitiveUniformBuffer = SceneProxy.GetCustomUniformBuffer(RHICmdList, IsMotionBlurEnabled());
if (IndirectDraw.IsValid())
{
MeshElement.IndirectArgsBuffer = IndirectDraw.Buffer;
MeshElement.IndirectArgsOffset = IndirectDraw.Offset;
MeshElement.NumPrimitives = 0;
}

if (NumCutoutVertexPerSubImage == 8)
{
MeshElement.IndexBuffer = &GSixTriangleParticleIndexBuffer;
}
...
}
至此Get Dynamic Mesh Elements(GDME)就差不多清楚了。现在看一下indirect args是怎么写的。对于stateless,FNiagaraGPUInstanceCountManager::UpdateDrawIndirectBuffers的调用发生在FGPUSortManager::OnPreRender的收尾阶段(PostPreRenderEvent.Broadcast)(对于ENiagaraGPUCountUpdatePhase::PreOpque),此时simulate已经完成。
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
void FNiagaraGPUInstanceCountManager::UpdateDrawIndirectBuffers(FNiagaraGpuComputeDispatchInterface* ComputeDispatchInterface, FRHICommandList& RHICmdList, ENiagaraGPUCountUpdatePhase::Type CountPhase)
{
...
// 把之前记录好的DrawIndirectArgGenTasks搬到GPU上
INC_DWORD_STAT_BY(STAT_NiagaraIndirectDraws, ArgTasks.Num());

SCOPED_DRAW_EVENT(RHICmdList, NiagaraUpdateDrawIndirectBuffers);

// Allocate task buffer
FReadBuffer TaskInfosBuffer;
{
const uint32 ArgGenSize = ArgTasks.Num() * sizeof(FNiagaraDrawIndirectArgGenTaskInfo);
const uint32 InstanceCountClearSize = bClearCounts ? InstanceCountClearTasks.Num() * sizeof(uint32) : 0;
const uint32 TaskBufferSize = ArgGenSize + InstanceCountClearSize;
TaskInfosBuffer.Initialize(RHICmdList, TEXT("NiagaraTaskInfosBuffer"), sizeof(uint32), TaskBufferSize / sizeof(uint32), EPixelFormat::PF_R32_UINT, BUF_Volatile);

uint8* TaskBufferData = (uint8*)RHICmdList.LockBuffer(TaskInfosBuffer.Buffer, 0, TaskBufferSize, RLM_WriteOnly);
FMemory::Memcpy(TaskBufferData, ArgTasks.GetData(), ArgGenSize);
FMemory::Memcpy(TaskBufferData + ArgGenSize, InstanceCountClearTasks.GetData(), InstanceCountClearSize);
RHICmdList.UnlockBuffer(TaskInfosBuffer.Buffer);
}
...
// 对于每个Pool,dispatch一次FNiagaraDrawIndirectResetCountsCS来生成indirect args
const int32 NumDispatches = FMath::Max(DrawIndirectPool.Num(), 1);
uint32 ArgGenTaskOffset = 0;
for (int32 DispatchIdx = 0; DispatchIdx < NumDispatches; ++DispatchIdx)
{
// Get draw indirect pool UAV
// Note: If we have counts to clear but no indirect args we won't have a DrawIndirectPool entry
FRHIUnorderedAccessView* ArgsUAV = nullptr;
int32 NumArgGenTasks = 0;
if (DrawIndirectPool.IsValidIndex(DispatchIdx))
{
FIndirectArgsPoolEntryPtr& PoolEntry = DrawIndirectPool[DispatchIdx];
ArgsUAV = PoolEntry->Buffer.UAV;
NumArgGenTasks = PoolEntry->UsedEntries[CountPhase];
}
else
{
ArgsUAV = ComputeDispatchInterface->GetEmptyUAVFromPool(RHICmdList, PF_R32_UINT, ENiagaraEmptyUAVType::Buffer);
}

const bool bIsLastDispatch = DispatchIdx == (NumDispatches - 1);
const int32 NumInstanceCountClearTasks = bIsLastDispatch && bClearCounts ? InstanceCountClearTasks.Num() : 0;

// Do we have anything to do for this pool?
if (NumArgGenTasks + NumInstanceCountClearTasks == 0)
{
continue;
}

FNiagaraDrawIndirectArgsGenCS::FParameters ArgsGenParameters;
ArgsGenParameters.TaskInfos = TaskInfosBuffer.SRV;
ArgsGenParameters.CulledInstanceCounts = CulledCountsSRV;
ArgsGenParameters.RWInstanceCounts = CountsUAV;
ArgsGenParameters.RWDrawIndirectArgs = ArgsUAV;
ArgsGenParameters.TaskCount.X = ArgGenTaskOffset;
ArgsGenParameters.TaskCount.Y = NumArgGenTasks;
ArgsGenParameters.TaskCount.Z = NumInstanceCountClearTasks;
ArgsGenParameters.TaskCount.W = NumArgGenTasks + NumInstanceCountClearTasks;

// If the device supports RW Texture buffers then we can use a single compute pass, otherwise we need to split into two passes
if (GRHISupportsRWTextureBuffers)
{
FComputeShaderUtils::Dispatch(RHICmdList, DrawIndirectArgsGenCS, ArgsGenParameters, FIntVector(FMath::DivideAndRoundUp(NumArgGenTasks + NumInstanceCountClearTasks, NIAGARA_DRAW_INDIRECT_ARGS_GEN_THREAD_COUNT), 1, 1));
}
else
{
if (NumArgGenTasks > 0)
{
FComputeShaderUtils::Dispatch(RHICmdList, DrawIndirectArgsGenCS, ArgsGenParameters, FIntVector(FMath::DivideAndRoundUp(NumArgGenTasks, NIAGARA_DRAW_INDIRECT_ARGS_GEN_THREAD_COUNT), 1, 1));
}

if (NumInstanceCountClearTasks > 0)
{
FNiagaraDrawIndirectResetCountsCS::FParameters ClearCountParameters;
ClearCountParameters.TaskInfos = ArgsGenParameters.TaskInfos;
ClearCountParameters.RWInstanceCounts = ArgsGenParameters.RWInstanceCounts;
ClearCountParameters.TaskCount = ArgsGenParameters.TaskCount;
ClearCountParameters.TaskCount.X = 0;

FNiagaraDrawIndirectResetCountsCS::FPermutationDomain PermutationVectorResetCounts;
TShaderMapRef<FNiagaraDrawIndirectResetCountsCS> DrawIndirectResetCountsArgsGenCS(GetGlobalShaderMap(FeatureLevel), PermutationVectorResetCounts);
FComputeShaderUtils::Dispatch(RHICmdList, DrawIndirectResetCountsArgsGenCS, ClearCountParameters, FIntVector(FMath::DivideAndRoundUp(NumInstanceCountClearTasks, NIAGARA_DRAW_INDIRECT_ARGS_GEN_THREAD_COUNT), 1, 1));
}
}

ArgGenTaskOffset += NumArgGenTasks;
}

if (bCountBufferIsValid)
{
RHICmdList.EndUAVOverlap(CountsUAV);
}

// Generate and execute transitions
Transitions.Reset();
for (auto& PoolEntry : DrawIndirectPool)
{
Transitions.Emplace(PoolEntry->Buffer.UAV, ERHIAccess::UAVCompute, kIndirectArgsDefaultState);
}
Transitions.Emplace(CurrentCountBuffer.UAV, ERHIAccess::UAVCompute, kCountBufferDefaultState);
RHICmdList.Transition(Transitions);
}

// Add free counts back to list as we have cleared them
if ( bClearCounts )
{
FreeEntries.Append(InstanceCountClearTasks);
InstanceCountClearTasks.Empty();
}

DrawIndirectArgGenTasks[CountPhase].Empty();
...
}
这里进行了合批,对一个Pool进行一次dispatch。Shader本身很简单,就是从buffer里读相关的值写入indirect args 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Engine\Plugins\FX\Niagara\Shaders\Private\NiagaraDrawIndirectArgsGen.usf
void MainCS(uint TaskIndex : SV_DispatchThreadID)
{
const uint ArgGenTaskOffset = TaskCount.x;
const uint NumArgGenTasks = TaskCount.y;
const uint NumInstanceCountClearTasks = TaskCount.z;
const uint NumTotalTasks = TaskCount.w;

if (TaskIndex < NumArgGenTasks)
{
const uint InfoOffset = (ArgGenTaskOffset + TaskIndex) * NIAGARA_DRAW_INDIRECT_TASK_INFO_SIZE;
const uint ArgOffset = TaskInfos[InfoOffset + 0];
const uint Flags = TaskInfos[InfoOffset + 4];
const bool bUseCulledCounts = (Flags & FLAG_USE_CULLED_COUNTS) != 0;
const bool bInstancedStereo = (Flags & FLAG_INSTANCED_STEREO) != 0;

uint InstanceCount = 0;
BRANCH
if (bUseCulledCounts)
{
InstanceCount = CulledInstanceCounts[TaskInfos[InfoOffset + 1]];
}
else
{
InstanceCount = RWInstanceCounts[TaskInfos[InfoOffset + 1]];
}

if (bInstancedStereo)
{
InstanceCount *= 2;
}

RWDrawIndirectArgs[ArgOffset + 0] = TaskInfos[InfoOffset + 2]; // NumIndicesPerInstance
RWDrawIndirectArgs[ArgOffset + 1] = InstanceCount;
RWDrawIndirectArgs[ArgOffset + 2] = TaskInfos[InfoOffset + 3]; // StartIndexLocation
RWDrawIndirectArgs[ArgOffset + 3] = 0; // BaseVertexLocation
RWDrawIndirectArgs[ArgOffset + 4] = 0; // StartInstanceLocation
}
...
}

VS

准备工作就绪,现在看一下vertex shader是怎么实现的。

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
// Engine\Plugins\FX\Niagara\Shaders\Private\NiagaraSpriteVertexFactory.ush

FVertexFactoryInput LoadVertexFactoryInputForHGS(uint TriangleIndex, int VertexIndex)
{
...
// For 4 verts cutout geometry and normal particle geometry, use the typical 6 indices
FVertexFactoryInput Input;

uint IndexBuffer[6] = { 0, 2, 3, 0, 1, 2 };
uint VertexId = IndexBuffer[(TriangleIndex * 3 + VertexIndex) % 6];
if (GetCutoutNumVertices() > 0)
{
Input.TexCoord = NiagaraSpriteVFLooseParameters.CutoutGeometry[VertexId];
}
else
{
float2 TexCoords[4] = { float2(0.0f, 0.0f), float2(0.0f, 1.0f), float2(1.0f, 1.0f), float2(1.0f, 0.0f) };
Input.TexCoord = TexCoords[VertexId];
}
Input.VertexId = VertexId;
Input.InstanceId = TriangleIndex / 2;

return Input;
}

void ComputeBillboardUVs(FVertexFactoryInput Input, float2 ParticleSize, float SubImageIndex, float2 UVScale, out float2 UVForPosition, out float2 UVForTexturing, out float2 UVForTexturingUnflipped)
{
...
// Note: not inverting positions, as that would change the winding order
UVForPosition = Input.TexCoord.xy;
UVForTexturingUnflipped = UVForPosition;
UVForTexturing = float2(0.5f, 0.5f) + ((UVForTexturingUnflipped - float2(0.5f, 0.5f)) * UVScale * sign(ParticleSize));
}

FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
FVertexFactoryIntermediates Intermediates = (FVertexFactoryIntermediates)0;
uint ParticleID = GetInstanceId(GetInstanceIdFromVF(Input)); // NOTE: Handles instanced stereo

if(NiagaraSpriteVFLooseParameters.SortedIndicesOffset != 0xFFFFFFFF)
{
ParticleID = NiagaraSpriteVFLooseParameters.SortedIndices[NiagaraSpriteVFLooseParameters.SortedIndicesOffset + ParticleID];
}

const FLWCVector3 ParticlePosition = GetNiagaraParticlePosition(ParticleID);
const float3 ParticleTranslatedWorldPosition = LWCToFloat(LWCAdd(ParticlePosition, ResolvedView.TileOffset.PreViewTranslation));

const float ParticleRotation = GetNiagaraParticleRotation(ParticleID);
const float2 ParticleSize = GetNiagaraParticleSize(ParticleID);
const float3 ParticleVelocity = GetNiagaraParticleVelocity(ParticleID);
const float SubImageIndex = GetNiagaraParticleSubimage(ParticleID);
const float3 CustomFacing = SafeNormalize(GetNiagaraParticleFacingVector(ParticleID));
const float3 CustomAlignment = SafeNormalize(GetNiagaraParticleAlignmentVector(ParticleID));
const float2 PivotOffset = GetNiagaraPivotOffset(ParticleID);
const float2 UVScale = GetNiagaraUVScale(ParticleID);
const float3 CameraOffset = SafeNormalize(ResolvedView.TranslatedWorldCameraOrigin - ParticleTranslatedWorldPosition) * GetNiagaraCameraOffset(ParticleID);

Intermediates.SceneData = VF_GPUSCENE_GET_INTERMEDIATES(Input);
Intermediates.Position = ParticlePosition;
...
Intermediates.CustomAlignmentVector = SafeNormalize(GetNiagaraParticleAlignmentVector(ParticleID));
Intermediates.UVScale = UVScale;
...
ComputeBillboardUVs(Input, ParticleSize, SubImageIndex, UVScale, UVForPosition, UVForTexturing, UVForTexturingUnflipped);

const float2 Size = abs(Intermediates.Size.xy);
...
// Vertex position
const float2x3 Tangents = float2x3(Intermediates.TangentRight, Intermediates.TangentUp);
const float3 VertexOffset = CameraOffset + mul(Size * PixelSizeRatio * (UVForPosition - PivotOffset), Tangents);

// 得到billboard的顶点坐标
Intermediates.VertexWorldPosition = LWCAdd(ParticlePosition, VertexOffset);
...
return Intermediates;
}

// 从Simulation中获取particle position
FLWCVector3 GetNiagaraParticlePosition(uint InstanceID)
{
float3 ParticlePos = GetNiagaraParticleSimPosition(InstanceID);
return SimToWorldPos(ParticlePos, DFFastToTileOffset(GetPrimitiveDataFromUniformBuffer().LocalToWorld));
}

float3 GetNiagaraParticleSimPosition(uint InstanceID)
{
return SafeGetVec3(NiagaraSpriteVF.PositionDataOffset, InstanceID, NiagaraSpriteVF.DefaultPos.xyz);
}
float3 SafeGetVec3(int RegisterIndex, uint InstanceID, float3 DefaultValue)
{
return RegisterIndex == -1 ? DefaultValue : float3(GetFloat(RegisterIndex, InstanceID), GetFloat(RegisterIndex+1, InstanceID), GetFloat(RegisterIndex+2, InstanceID));
}
float GetFloat(int RegisterIdx, uint InstanceID)
{
RegisterIdx &= (~(1u << 31));
return ParticleDataFloatBuffer[(RegisterIdx * ParticleDataFloatStride + InstanceID)]; // 这里符合我们之前说的把float3拆分成component,同component紧挨着放
}
不过看起来并没有任何shader include了NiagaraSpriteVertexFactory.ush,唯一用到它的是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Engine\Plugins\FX\Niagara\Source\NiagaraVertexFactories\Private\NiagaraSpriteVertexFactory.cpp
IMPLEMENT_VERTEX_FACTORY_TYPE(FNiagaraSpriteVertexFactory,"/Plugin/FX/Niagara/Private/NiagaraSpriteVertexFactory.ush",
EVertexFactoryFlags::UsedWithMaterials
| EVertexFactoryFlags::SupportsDynamicLighting
| EVertexFactoryFlags::SupportsRayTracing
| EVertexFactoryFlags::SupportsRayTracingDynamicGeometry
| EVertexFactoryFlags::SupportsPSOPrecaching
);

// Engine\Source\Runtime\RenderCore\Public\VertexFactory.h
#define IMPLEMENT_VERTEX_FACTORY_TYPE(FactoryClass, ShaderFilename, Flags) \
FVertexFactoryType FactoryClass::StaticType( \
TEXT(#FactoryClass), \
TEXT(ShaderFilename), \
Flags, \
IMPLEMENT_VERTEX_FACTORY_VTABLE(FactoryClass) \
); \
FVertexFactoryType* FactoryClass::GetType() const { return &StaticType; }

FVertexFactoryType中include了ush

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Engine\Source\Runtime\RenderCore\Public\VertexFactory.h
void ModifyCompilationEnvironment(const FVertexFactoryShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) const
{
// Set up the mapping from VertexFactory.usf to the vertex factory type's source code.
FString VertexFactoryIncludeString = FString::Printf( TEXT("#include \"%s\""), GetShaderFilename() );
OutEnvironment.IncludeVirtualPathToContentsMap.Add(TEXT("/Engine/Generated/VertexFactory.ush"), VertexFactoryIncludeString);

if (IncludesFwdShaderFile())
{
FString VertexFactoryFwdIncludeString = FString::Printf(TEXT("#include \"%s\""), GetShaderFwdFilename());
OutEnvironment.IncludeVirtualPathToContentsMap.Add(TEXT("/Engine/Generated/VertexFactoryFwd.ush"), VertexFactoryFwdIncludeString);
OutEnvironment.SetDefine(TEXT("USE_VERTEX_FACTORY_FWD"), 1);
}
else
{
OutEnvironment.IncludeVirtualPathToContentsMap.Add(TEXT("/Engine/Generated/VertexFactoryFwd.ush"), TEXT("#include \"/Engine/Private/VertexFactoryDefaultFwd.ush\""));
}

OutEnvironment.SetDefine(TEXT("HAS_PRIMITIVE_UNIFORM_BUFFER"), 1);

(*ModifyCompilationEnvironmentRef)(Parameters, OutEnvironment);
}
在下面的函数中进行了设置,这样就清楚了。
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
static void PrepareMeshMaterialShaderCompileJob(EShaderPlatform Platform,
EShaderPermutationFlags PermutationFlags,
const FMaterial* Material,
const FMaterialShaderMapId& MaterialShaderMapId,
FSharedShaderCompilerEnvironment* MaterialEnvironment,
const FShaderPipelineType* ShaderPipeline,
const FString& DebugGroupName,
const TCHAR* DebugDescription,
const TCHAR* DebugExtension,
FShaderCompileJob* NewJob)
{
...
// apply the vertex factory changes to the compile environment
check(VertexFactoryType);
VertexFactoryType->ModifyCompilationEnvironment(FVertexFactoryShaderPermutationParameters(Platform, MaterialParameters, VertexFactoryType, ShaderType, PermutationFlags), ShaderEnvironment);

Material->SetupExtraCompilationSettings(Platform, NewJob->Input.ExtraSettings);

//update material shader stats
UpdateMaterialShaderCompilingStats(Material);

UE_LOG(LogShaders, Verbose, TEXT(" %s"), ShaderType->GetName());

// Allow the shader type to modify the compile environment.
ShaderType->SetupCompileEnvironment(Platform, MaterialParameters, VertexFactoryType, Key.PermutationId, PermutationFlags, ShaderEnvironment);

bool bAllowDevelopmentShaderCompile = Material->GetAllowDevelopmentShaderCompile();

// Compile the shader environment passed in with the shader type's source code.
::GlobalBeginCompileShader(
DebugGroupName,
VertexFactoryType,
ShaderType,
ShaderPipeline,
Key.PermutationId,
ShaderType->GetShaderFilename(),
ShaderType->GetFunctionName(),
FShaderTarget(ShaderType->GetFrequency(), Platform),
NewJob->Input,
bAllowDevelopmentShaderCompile,
DebugDescription,
DebugExtension
);
}

VS to PS

VS输出的属性如下

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
struct FVertexFactoryInterpolantsVSToPS
{
// First row of the tangent to world matrix, Interp_Sizer used by SUBUV_PARTICLES in w
float4 TangentToWorld0AndInterp_Sizer : TANGENTTOWORLD0;
// Last row of the tangent to world matrix in xyz
float4 TangentToWorld2 : TANGENTTOWORLD2;

#if (DYNAMIC_PARAMETERS_MASK & 1)
nointerpolation float4 DynamicParameter : PARTICLE_DYNAMIC_PARAM0;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 2)
nointerpolation float4 DynamicParameter1 : PARTICLE_DYNAMIC_PARAM1;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 4)
nointerpolation float4 DynamicParameter2 : PARTICLE_DYNAMIC_PARAM2;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 8)
nointerpolation float4 DynamicParameter3 : PARTICLE_DYNAMIC_PARAM3;
#endif

#if NEEDS_PARTICLE_COLOR
float4 Color : TEXCOORD0;
#endif

#if NUM_TEX_COORD_INTERPOLATORS
float4 TexCoords[(NUM_TEX_COORD_INTERPOLATORS + 1) / 2] : TEXCOORD1;
#endif

//Not sure this is actually being used now and it's awkward to slot in now we're supporting custom UVs so I'm just giving this its own interpolant.
#if LIGHTMAP_UV_ACCESS
float2 LightMapUVs : LIGHTMAP_UVS;
#endif

#if USE_PARTICLE_SUBUVS
float4 ParticleSubUVs : PARTICLE_SUBUVS;
#endif

#if USE_PARTICLE_POSITION
/** Cam-relative (translated) particle center and radius */
nointerpolation float4 ParticleTranslatedWorldPositionAndSize : PARTICLE_POSITION;
#endif

#if USE_PARTICLE_VELOCITY
nointerpolation float4 ParticleVelocity : PARTICLE_VELOCITY;
#endif

#if USE_PARTICLE_TIME
nointerpolation float RelativeTime : PARTICLE_TIME;
#endif

#if USE_PARTICLE_LIGHTING_OFFSET
float3 LightingPositionOffset : PARTICLE_LIGHTING_OFFSET;
#endif

#if USE_PARTICLE_SIZE
nointerpolation float2 ParticleSize : PARTICLE_SIZE;
#endif

#if USE_PARTICLE_SPRITE_ROTATION
nointerpolation float ParticleSpriteRotation : PARTICLE_SPRITE_ROTATION;
#endif

#if USE_PARTICLE_RANDOM
nointerpolation float ParticleRandom : PARTICLE_RANDOM;
#endif
};
输出属性由以下函数进行设置,主要是拷贝Intermediates中的内容,包括DynamicParameter等。
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
FVertexFactoryInterpolantsVSToPS VertexFactoryGetInterpolantsVSToPS(FVertexFactoryInput Input, FVertexFactoryIntermediates Intermediates, FMaterialVertexParameters VertexParameters)
{
FVertexFactoryInterpolantsVSToPS Interpolants;

// Initialize the whole struct to 0
Interpolants = (FVertexFactoryInterpolantsVSToPS)0;

#if NUM_TEX_COORD_INTERPOLATORS
float2 CustomizedUVs[NUM_TEX_COORD_INTERPOLATORS];
GetMaterialCustomizedUVs(VertexParameters, CustomizedUVs);
GetCustomInterpolators(VertexParameters, CustomizedUVs);

UNROLL
for (int CoordinateIndex = 0; CoordinateIndex < NUM_TEX_COORD_INTERPOLATORS; CoordinateIndex++)
{
SetUV(Interpolants, CoordinateIndex, CustomizedUVs[CoordinateIndex]);
}
#endif

#if LIGHTMAP_UV_ACCESS
Interpolants.LightMapUVs = Intermediates.TexCoord.xy;
#endif

#if USE_PARTICLE_SUBUVS
Interpolants.ParticleSubUVs.xy = VertexParameters.Particle.SubUVCoords[0];
Interpolants.ParticleSubUVs.zw = VertexParameters.Particle.SubUVCoords[1];
#endif

// Calculate the transform from tangent to world space.
// Note that "local" space for particles is actually oriented in world space! Therefore no rotation is needed.
float3x3 TangentToWorld = Intermediates.TangentToLocal;

Interpolants.TangentToWorld0AndInterp_Sizer.xyz = TangentToWorld[0];
Interpolants.TangentToWorld0AndInterp_Sizer.w = Intermediates.SubImageLerp;
Interpolants.TangentToWorld2 = float4(TangentToWorld[2], sign(determinant(Intermediates.TangentToLocal)));

#if NEEDS_PARTICLE_COLOR
Interpolants.Color = Intermediates.Color;
#endif

#if (DYNAMIC_PARAMETERS_MASK & 1)
Interpolants.DynamicParameter = Intermediates.DynamicParameter;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 2)
Interpolants.DynamicParameter1 = Intermediates.DynamicParameter1;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 4)
Interpolants.DynamicParameter2 = Intermediates.DynamicParameter2;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 8)
Interpolants.DynamicParameter3 = Intermediates.DynamicParameter3;
#endif

#if USE_PARTICLE_POSITION
Interpolants.ParticleTranslatedWorldPositionAndSize = Intermediates.TranslatedWorldPositionAndSize;
#endif

#if USE_PARTICLE_VELOCITY
Interpolants.ParticleVelocity = Intermediates.ParticleVelocity;
#endif

#if USE_PARTICLE_TIME
Interpolants.RelativeTime = Intermediates.RelativeTime;
#endif

#if USE_PARTICLE_LIGHTING_OFFSET
Interpolants.LightingPositionOffset = Intermediates.LightingPositionOffset;
#endif

#if USE_PARTICLE_SIZE
Interpolants.ParticleSize = Intermediates.ParticleSize;
#endif

#if USE_PARTICLE_SPRITE_ROTATION
Interpolants.ParticleSpriteRotation = Intermediates.ParticleSpriteRotation;
#endif

#if USE_PARTICLE_RANDOM
Interpolants.ParticleRandom = Intermediates.ParticleRandom;
#endif

return Interpolants;
}
PS能够根据这个计算出所有材质属性
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
FMaterialPixelParameters GetMaterialPixelParameters(FVertexFactoryInterpolantsVSToPS Interpolants, float4 SvPosition)
{
// GetMaterialPixelParameters is responsible for fully initializing the result
FMaterialPixelParameters Result = MakeInitializedMaterialPixelParameters();

#if USE_PARTICLE_SUBUVS
#if NUM_TEX_COORD_INTERPOLATORS
UNROLL
for( int CoordinateIndex = 0; CoordinateIndex < NUM_TEX_COORD_INTERPOLATORS; CoordinateIndex++ )
{
Result.TexCoords[CoordinateIndex] = Interpolants.SubUV0AndTexCoord0.zw;
}
#endif
Result.Particle.SubUVCoords[0] = Interpolants.SubUV0AndTexCoord0.xy;
Result.Particle.SubUVCoords[1] = Interpolants.SubUV1AndLerp.xy;
Result.Particle.SubUVLerp = Interpolants.SubUV1AndLerp.z;
#elif NUM_TEX_COORD_INTERPOLATORS
UNROLL
for (int CoordinateIndex = 0; CoordinateIndex < NUM_TEX_COORD_INTERPOLATORS / 2; ++CoordinateIndex)
{
Result.TexCoords[CoordinateIndex * 2] = Interpolants.TexCoords[CoordinateIndex].xy;
Result.TexCoords[CoordinateIndex * 2 + 1] = Interpolants.TexCoords[CoordinateIndex].wz;
}
#if NUM_TEX_COORD_INTERPOLATORS & 1
Result.TexCoords[NUM_TEX_COORD_INTERPOLATORS - 1] = Interpolants.TexCoords[NUM_TEX_COORD_INTERPOLATORS / 2].xy;
#endif // #if NUM_TEX_COORD_INTERPOLATORS & 1
#endif

half3 TangentToWorld0 = Interpolants.TangentToWorld0.xyz;
half4 TangentToWorld2 = Interpolants.TangentToWorld2;
Result.UnMirrored = TangentToWorld2.w;

#if INTERPOLATE_VERTEX_COLOR
Result.VertexColor = Interpolants.VertexColor;
#else
Result.VertexColor = 0;
#endif

#if NEEDS_PARTICLE_COLOR
Result.Particle.Color = Interpolants.ParticleColor;
#endif

#if (DYNAMIC_PARAMETERS_MASK != 0)
Result.Particle.DynamicParameterValidMask = NiagaraMeshVF.MaterialParamValidMask;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 1)
Result.Particle.DynamicParameter = Interpolants.DynamicParameter;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 2)
Result.Particle.DynamicParameter1 = Interpolants.DynamicParameter1;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 4)
Result.Particle.DynamicParameter2 = Interpolants.DynamicParameter2;
#endif
#if (DYNAMIC_PARAMETERS_MASK & 8)
Result.Particle.DynamicParameter3 = Interpolants.DynamicParameter3;
#endif

#if USE_PARTICLE_POSITION
Result.Particle.TranslatedWorldPositionAndSize.xyz = Interpolants.ParticleTranslatedWorldPosition;
Result.Particle.TranslatedWorldPositionAndSize.w = 1;
Result.Particle.PrevTranslatedWorldPositionAndSize = Result.Particle.TranslatedWorldPositionAndSize;
#endif

#if USE_PARTICLE_VELOCITY
Result.Particle.Velocity = Interpolants.ParticleVelocity;
#endif

#if USE_PARTICLE_TIME
Result.Particle.RelativeTime = Interpolants.RelativeTime;
#endif

#if USE_PARTICLE_RANDOM
Result.Particle.Random = Interpolants.ParticleRandom;
#else
Result.Particle.Random = 0.0f;
#endif

#if USE_PARTICLE_LOCAL_TO_WORLD
//-TODO: LWC Precision Loss
FLWCMatrix ParticleToWorld = LWCPromote(transpose(float4x4(Interpolants.ParticleToWorld[0], Interpolants.ParticleToWorld[1], Interpolants.ParticleToWorld[2], float4(0.0f, 0.0f, 0.0f, 1.0f))));
#if NEEDS_PARTICLE_LOCAL_TO_WORLD
Result.Particle.ParticleToWorld = DFFromTileOffset(ParticleToWorld);
#endif
#if NEEDS_INSTANCE_LOCAL_TO_WORLD_PS
Result.InstanceLocalToWorld = DFFromTileOffset(ParticleToWorld);
#endif
#endif

#if USE_PARTICLE_WORLD_TO_LOCAL
//-TODO: LWC Precision Loss
FLWCInverseMatrix WorldToParticle = LWCPromoteInverse(transpose(float4x4(Interpolants.WorldToParticle[0], Interpolants.WorldToParticle[1], Interpolants.WorldToParticle[2], float4(0.0f, 0.0f, 0.0f, 1.0f))));
#if NEEDS_PARTICLE_WORLD_TO_LOCAL
Result.Particle.WorldToParticle = DFFromTileOffset(WorldToParticle);
#endif
#if NEEDS_INSTANCE_WORLD_TO_LOCAL_PS
Result.InstanceWorldToLocal = DFFromTileOffset(WorldToParticle);
#endif
#endif

Result.Particle.MotionBlurFade = 1.0f;
Result.TangentToWorld = AssembleTangentToWorld( TangentToWorld0, TangentToWorld2 );
Result.TwoSidedSign = 1;

#if USE_WORLDVERTEXNORMAL_CENTER_INTERPOLATION
Result.WorldVertexNormal_Center = Interpolants.TangentToWorld2_Center.xyz;
#endif

#if VF_USE_PRIMITIVE_SCENE_DATA
Result.PrimitiveId = Interpolants.PrimitiveId;
#endif

return Result;
}
至此,绘制流程就完全清晰了。


Niagara(二):Sprite粒子渲染
https://jhex-git.github.io/posts/1219731573/
作者
JointHex
发布于
2026年9月19日
许可协议