// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #pragma once #include "../ParticleEmitterGraph.h" #include "Engine/Particles/ParticlesSimulation.h" #include "Engine/Particles/ParticlesData.h" #include "Engine/Visject/VisjectGraph.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Threading/ThreadLocal.h" struct RenderContext; class ParticleEffect; class ParticleEmitterGraphCPU; class ParticleEmitterGraphCPUBase; class ParticleEmitterGraphCPUNode; class ParticleEmitterGraphCPUExecutor; // The root node type identifier #define PARTICLE_EMITTER_ROOT_NODE_TYPE GRAPH_NODE_MAKE_TYPE(14, 1) // The maximum amount of particle modules used per context #define PARTICLE_EMITTER_MAX_MODULES 32 // The maximum amount of used particles attributes per graph node #define PARTICLE_EMITTER_MAX_ATTRIBUTES_REFS_PER_NODE 4 // The maximum amount of used asset references per graph node #define PARTICLE_EMITTER_MAX_ASSET_REFS_PER_NODE 8 #define PARTICLE_EMITTER_MAX_CALL_STACK 100 class ParticleEmitterGraphCPUBox : public VisjectGraphBox { }; class ParticleEmitterGraphCPUNode : public ParticleEmitterGraphNode> { public: /// /// The sorted indices buffer offset used by the rendering modules to point the sorted indices buffer start to use for rendering. /// uint32 SortedIndicesOffset; union { int32 CustomDataOffset; int32 RibbonOrderOffset; }; /// /// True if this node uses the per-particle data resolve instead of optimized whole-collection fetch. /// FORCE_INLINE bool UsePerParticleDataResolve() const { return UsesParticleData || !IsConstant; } }; /// /// The Particle Emitter Graph used to simulate CPU particles. /// class ParticleEmitterGraphCPU : public ParticleEmitterGraph, ParticleEmitterGraphCPUNode, Variant> { friend ParticleEmitterGraphCPUExecutor; typedef ParticleEmitterGraph, ParticleEmitterGraphCPUNode, Variant> Base; private: struct NodeState { union { int32 SpiralProgress; }; }; Array _defaultParticleData; public: // Size of the custom pre-node data buffer used for state tracking (eg. position on spiral arc progression). int32 CustomDataSize = 0; /// /// Creates the default surface graph (the main root node) for the particle emitter. Ensure to dispose the previous graph data before. /// void CreateDefault(); /// /// Gets the position attribute offset from the particle data layout start (in bytes). /// FORCE_INLINE int32 GetPositionAttributeOffset() const { return _attrPosition != -1 ? Layout.Attributes[_attrPosition].Offset : -1; } /// /// Gets the age attribute offset from the particle data layout start (in bytes). /// FORCE_INLINE int32 GetAgeAttributeOffset() const { return _attrAge != -1 ? Layout.Attributes[_attrAge].Offset : -1; } public: // [ParticleEmitterGraph] bool Load(ReadStream* stream, bool loadMeta) override; void InitializeNode(Node* node) override; }; /// /// The CPU particles emitter graph evaluation context. /// struct ParticleEmitterGraphCPUContext { float DeltaTime; uint32 ParticleIndex; ParticleEmitterInstance* Data; ParticleEmitter* Emitter; ParticleEffect* Effect; class SceneRenderTask* ViewTask; Array> GraphStack; Dictionary Functions; byte AttributesRemappingTable[PARTICLE_ATTRIBUTES_MAX_COUNT]; // Maps node attribute indices to the current particle layout (used to support accessing particle data from function graph which has different layout). int32 CallStackSize = 0; VisjectExecutor::Node* CallStack[PARTICLE_EMITTER_MAX_CALL_STACK]; }; /// /// The Particle Emitter Graph simulation on a CPU. /// class ParticleEmitterGraphCPUExecutor : public VisjectExecutor { private: ParticleEmitterGraphCPU& _graph; // Per-thread context to allow async execution static ThreadLocal Context; public: /// /// Initializes a new instance of the class. /// /// The graph to execute. explicit ParticleEmitterGraphCPUExecutor(ParticleEmitterGraphCPU& graph); /// /// Computes the local bounds of the particle emitter instance. /// /// The owning emitter. /// The instance effect. /// The instance data. /// The result bounding box. /// True if has valid bounds, otherwise false if failed to calculate it (eg. GPU emitter or not synced or no particles. bool ComputeBounds(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, BoundingBox& result); /// /// Draw the particles (eg. lights). /// /// The owning emitter. /// The instance effect. /// The instance data. /// The rendering context. /// The effect transform matrix. void Draw(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, RenderContext& renderContext, Matrix& transform); /// /// Updates the particles simulation (the CPU simulation). /// /// The owning emitter. /// The instance effect. /// The instance data. /// The delta time (in seconds). /// True if can spawn new particles, otherwise will just perform an update. void Update(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, float dt, bool canSpawn); /// /// Updates the particles spawning logic (the non-CPU simulation that needs to spawn particles) and returns the amount of particles to add to the simulation. /// /// The owning emitter. /// The instance effect. /// The instance data. /// The simulation delta time (in seconds). /// The particles to spawn count int32 UpdateSpawn(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, float dt); private: void Init(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, float dt = 0.0f); Value eatBox(Node* caller, Box* box) override; Graph* GetCurrentGraph() const override; void ProcessGroupParameters(Box* box, Node* node, Value& value); void ProcessGroupTextures(Box* box, Node* node, Value& value); void ProcessGroupTools(Box* box, Node* node, Value& value); void ProcessGroupParticles(Box* box, Node* nodeBase, Value& value); void ProcessGroupFunction(Box* box, Node* node, Value& value); int32 ProcessSpawnModule(int32 index); void ProcessModule(ParticleEmitterGraphCPUNode* node, int32 particlesStart, int32 particlesEnd); FORCE_INLINE Value GetValue(Box* box, int32 defaultValueBoxIndex) { const auto parentNode = box->GetParent(); if (box->HasConnection()) return eatBox(parentNode, box->FirstConnection()); return parentNode->Values[defaultValueBoxIndex]; } FORCE_INLINE Value TryGetValue(Box* box, const Value& defaultValue) { return box && box->HasConnection() ? eatBox(box->GetParent(), box->FirstConnection()) : defaultValue; } };