Files
FlaxEngine/Source/Engine/Particles/Graph/ParticleEmitterGraph.h
2024-04-19 12:22:04 +02:00

672 lines
23 KiB
C++

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Visject/Graph.h"
#include "Engine/Content/Content.h"
#include "Engine/Animations/Curve.h"
#include "Engine/Particles/Types.h"
#include "Engine/Particles/ParticlesSimulation.h"
#include "Engine/Particles/ParticlesData.h"
#include "Engine/Core/Math/Vector4.h"
#include "Engine/Core/Types/BaseTypes.h"
class ParticleEffect;
// 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
template<class Base>
class ParticleEmitterGraphNode : public Base
{
public:
/// <summary>
/// True if node is used by the particles graph (is connected to the any module input or its a enabled module).
/// </summary>
bool Used = false;
/// <summary>
/// Flag valid for used particle nodes that need per-particle data to evaluate its value (including dependant nodes linked to input boxes). Used to skip per-particle graph evaluation if graph uses the same value for all particles (eg. is not using per-particle seed or position node).
/// </summary>
bool UsesParticleData = false;
/// <summary>
/// Flag valid for used particle nodes that result in constant data (nothing random nor particle data).
/// </summary>
bool IsConstant = true;
/// <summary>
/// The cached particle attribute indices used by the simulation graph to access particle properties.
/// </summary>
int32 Attributes[PARTICLE_EMITTER_MAX_ATTRIBUTES_REFS_PER_NODE];
};
void InitParticleEmitterFunctionCall(const Guid& assetId, AssetReference<Asset>& asset, bool& usesParticleData, ParticleLayout& layout);
/// <summary>
/// The Particle Emitter Graph used to simulate particles.
/// </summary>
template<class BaseType, class NodeType, class ValueType>
class ParticleEmitterGraph : public BaseType
{
public:
typedef ValueType Value;
enum class ModuleType
{
Spawn,
Initialize,
Update,
Render,
};
protected:
// Attributes cache
int32 _attrPosition;
int32 _attrVelocity;
int32 _attrRotation;
int32 _attrAngularVelocity;
int32 _attrAge;
int32 _attrLifetime;
int32 _attrSpriteSize;
int32 _attrScale;
int32 _attrMass;
int32 _attrRibbonWidth;
int32 _attrColor;
int32 _attrRadius;
public:
/// <summary>
/// The Particle Emitter Graph data version number. Used to sync the Particle Emitter Graph data with the instances state. Handles graph reloads to ensure data is valid.
/// </summary>
uint32 Version = 0;
/// <summary>
/// The cached root node.
/// </summary>
NodeType* Root = nullptr;
/// <summary>
/// The particle layout.
/// </summary>
ParticleLayout Layout;
/// <summary>
/// The particle emitter capacity (max particles limit for the simulation).
/// </summary>
int32 Capacity;
/// <summary>
/// The particles simulation space.
/// </summary>
ParticlesSimulationSpace SimulationSpace;
/// <summary>
/// The particle layout attributes default values.
/// </summary>
Array<Variant, FixedAllocation<PARTICLE_ATTRIBUTES_MAX_COUNT>> AttributesDefaults;
public:
/// <summary>
/// The particles modules for Spawn context.
/// </summary>
Array<NodeType*, FixedAllocation<PARTICLE_EMITTER_MAX_MODULES>> SpawnModules;
/// <summary>
/// The particles modules for Initialize context.
/// </summary>
Array<NodeType*, FixedAllocation<PARTICLE_EMITTER_MAX_MODULES>> InitModules;
/// <summary>
/// The particles modules for Update context.
/// </summary>
Array<NodeType*, FixedAllocation<PARTICLE_EMITTER_MAX_MODULES>> UpdateModules;
/// <summary>
/// The particles modules for Render context.
/// </summary>
Array<NodeType*, FixedAllocation<PARTICLE_EMITTER_MAX_MODULES>> RenderModules;
/// <summary>
/// The particles modules for lights rendering.
/// </summary>
Array<NodeType*, FixedAllocation<PARTICLE_EMITTER_MAX_MODULES>> LightModules;
/// <summary>
/// The particles modules for sorting particles.
/// </summary>
Array<NodeType*, FixedAllocation<PARTICLE_EMITTER_MAX_MODULES>> SortModules;
/// <summary>
/// The particles modules for ribbon particles rendering.
/// </summary>
Array<NodeType*, FixedAllocation<PARTICLE_EMITTER_MAX_MODULES>> RibbonRenderingModules;
bool UsesVolumetricFogRendering = false;
virtual void InitializeNode(NodeType* node)
{
// Skip if already initialized
if (node->Used)
return;
node->Used = true;
#define USE_ATTRIBUTE(name, valueType, slot) \
{ \
const StringView name(TEXT(#name)); \
auto idx = Layout.FindAttribute(name, ParticleAttribute::ValueTypes::valueType); \
if (idx == -1) \
idx = Layout.AddAttribute(name, ParticleAttribute::ValueTypes::valueType); \
static_assert(slot < ARRAY_COUNT(NodeType::Attributes), "Invalid attribute index."); \
node->Attributes[slot] = idx; \
}
switch (node->Type)
{
// == Tools ==
// Get Gameplay Global
case GRAPH_NODE_MAKE_TYPE(7, 16):
{
node->Assets.Resize(1);
node->Assets[0] = Content::LoadAsync<Asset>((Guid)node->Values[0]);
break;
}
// === Particles ===
// Particle Attribute
case GRAPH_NODE_MAKE_TYPE(14, 100):
case GRAPH_NODE_MAKE_TYPE(14, 303):
{
node->UsesParticleData = true;
const StringView name(node->Values[0]);
const ParticleAttribute::ValueTypes valueType = (ParticleAttribute::ValueTypes)node->Values[1].AsUint64;
auto idx = Layout.FindAttribute(name, valueType);
if (idx == -1)
idx = Layout.AddAttribute(name, valueType);
node->Attributes[0] = idx;
node->Attributes[1] = (int32)valueType;
static_assert(PARTICLE_EMITTER_MAX_ATTRIBUTES_REFS_PER_NODE >= 2, "Invalid node attributes count. Need more space for some data here.");
break;
}
// Particle Position
case GRAPH_NODE_MAKE_TYPE(14, 101):
case GRAPH_NODE_MAKE_TYPE(14, 212):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Position, Float3, 0);
break;
}
// Particle Lifetime
case GRAPH_NODE_MAKE_TYPE(14, 102):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Lifetime, Float, 0);
break;
}
// Particle Age
case GRAPH_NODE_MAKE_TYPE(14, 103):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Age, Float, 0);
break;
}
// Particle Color
case GRAPH_NODE_MAKE_TYPE(14, 104):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Color, Float4, 0);
break;
}
// Particle Velocity
case GRAPH_NODE_MAKE_TYPE(14, 105):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Velocity, Float3, 0);
break;
}
// Particle Sprite Size
case GRAPH_NODE_MAKE_TYPE(14, 106):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(SpriteSize, Float2, 0);
break;
}
// Particle Mass
case GRAPH_NODE_MAKE_TYPE(14, 107):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Mass, Float, 0);
break;
}
// Particle Rotation
case GRAPH_NODE_MAKE_TYPE(14, 108):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Rotation, Float3, 0);
break;
}
// Particle Angular Velocity
case GRAPH_NODE_MAKE_TYPE(14, 109):
{
USE_ATTRIBUTE(AngularVelocity, Float3, 0);
break;
}
// Particle Normalized Age
case GRAPH_NODE_MAKE_TYPE(14, 110):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Age, Float, 0);
USE_ATTRIBUTE(Lifetime, Float, 1);
break;
}
// Particle Radius
case GRAPH_NODE_MAKE_TYPE(14, 111):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Radius, Float, 0);
break;
}
// Particle Scale
case GRAPH_NODE_MAKE_TYPE(14, 112):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Scale, Float3, 0);
break;
}
// Random
case GRAPH_NODE_MAKE_TYPE(14, 208):
case GRAPH_NODE_MAKE_TYPE(14, 209):
case GRAPH_NODE_MAKE_TYPE(14, 210):
case GRAPH_NODE_MAKE_TYPE(14, 211):
case GRAPH_NODE_MAKE_TYPE(14, 213):
case GRAPH_NODE_MAKE_TYPE(14, 214):
case GRAPH_NODE_MAKE_TYPE(14, 215):
case GRAPH_NODE_MAKE_TYPE(14, 216):
node->IsConstant = false;
break;
// Particle Emitter Function
case GRAPH_NODE_MAKE_TYPE(14, 300):
node->Assets.Resize(1);
InitParticleEmitterFunctionCall((Guid)node->Values[0], node->Assets[0], node->UsesParticleData, Layout);
break;
// Particle Index
case GRAPH_NODE_MAKE_TYPE(14, 301):
node->UsesParticleData = true;
break;
// === Particle Modules ===
// Orient Sprite
case GRAPH_NODE_MAKE_TYPE(15, 201):
case GRAPH_NODE_MAKE_TYPE(15, 303):
{
USE_ATTRIBUTE(SpriteFacingMode, Int, 0);
if (((ParticleSpriteFacingMode)node->Values[2].AsInt) == ParticleSpriteFacingMode::CustomFacingVector ||
((ParticleSpriteFacingMode)node->Values[2].AsInt) == ParticleSpriteFacingMode::FixedAxis)
{
USE_ATTRIBUTE(SpriteFacingVector, Float3, 1);
}
break;
}
// Orient Model
case GRAPH_NODE_MAKE_TYPE(15, 213):
case GRAPH_NODE_MAKE_TYPE(15, 309):
{
USE_ATTRIBUTE(ModelFacingMode, Int, 0);
break;
}
// Update Age
case GRAPH_NODE_MAKE_TYPE(15, 300):
{
USE_ATTRIBUTE(Age, Float, 0);
break;
}
// Gravity/Force
case GRAPH_NODE_MAKE_TYPE(15, 301):
case GRAPH_NODE_MAKE_TYPE(15, 304):
{
USE_ATTRIBUTE(Velocity, Float3, 0);
break;
}
// Linear Drag
case GRAPH_NODE_MAKE_TYPE(15, 310):
{
USE_ATTRIBUTE(Velocity, Float3, 0);
USE_ATTRIBUTE(Mass, Float, 1);
if (node->Values[3].AsBool)
{
USE_ATTRIBUTE(SpriteSize, Float2, 2);
}
break;
}
// Turbulence
case GRAPH_NODE_MAKE_TYPE(15, 311):
{
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Velocity, Float3, 1);
USE_ATTRIBUTE(Mass, Float, 2);
break;
}
// Position (plane/box surface/box volume/cylinder/line/sphere/circle/disc/torus/Global SDF)
case GRAPH_NODE_MAKE_TYPE(15, 202):
case GRAPH_NODE_MAKE_TYPE(15, 203):
case GRAPH_NODE_MAKE_TYPE(15, 204):
case GRAPH_NODE_MAKE_TYPE(15, 205):
case GRAPH_NODE_MAKE_TYPE(15, 206):
case GRAPH_NODE_MAKE_TYPE(15, 207):
case GRAPH_NODE_MAKE_TYPE(15, 208):
case GRAPH_NODE_MAKE_TYPE(15, 209):
case GRAPH_NODE_MAKE_TYPE(15, 210):
case GRAPH_NODE_MAKE_TYPE(15, 211):
case GRAPH_NODE_MAKE_TYPE(15, 215):
{
USE_ATTRIBUTE(Position, Float3, 0);
break;
}
// Position (depth)
case GRAPH_NODE_MAKE_TYPE(15, 212):
{
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Lifetime, Float, 1);
break;
}
// Position (spiral)
case GRAPH_NODE_MAKE_TYPE(15, 214):
{
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Velocity, Float3, 1);
break;
}
// Set Attribute
case GRAPH_NODE_MAKE_TYPE(15, 200):
case GRAPH_NODE_MAKE_TYPE(15, 302):
{
const StringView name(node->Values[2]);
const ParticleAttribute::ValueTypes valueType = (ParticleAttribute::ValueTypes)node->Values[3].AsInt;
auto idx = Layout.FindAttribute(name, valueType);
if (idx == -1)
idx = Layout.AddAttribute(name, valueType);
node->Attributes[0] = idx;
break;
}
// Set Position/Lifetime/Age/..
#define CASE_SET_PARTICLE_ATTRIBUTE(id0, id1, name, type) case GRAPH_NODE_MAKE_TYPE(15, id0): case GRAPH_NODE_MAKE_TYPE(15, id1): USE_ATTRIBUTE(name, type, 0); break
CASE_SET_PARTICLE_ATTRIBUTE(250, 350, Position, Float3);
CASE_SET_PARTICLE_ATTRIBUTE(251, 351, Lifetime, Float);
CASE_SET_PARTICLE_ATTRIBUTE(252, 352, Age, Float);
CASE_SET_PARTICLE_ATTRIBUTE(253, 353, Color, Float4);
CASE_SET_PARTICLE_ATTRIBUTE(254, 354, Velocity, Float3);
CASE_SET_PARTICLE_ATTRIBUTE(255, 355, SpriteSize, Float2);
CASE_SET_PARTICLE_ATTRIBUTE(256, 356, Mass, Float);
CASE_SET_PARTICLE_ATTRIBUTE(257, 357, Rotation, Float3);
CASE_SET_PARTICLE_ATTRIBUTE(258, 358, AngularVelocity, Float3);
CASE_SET_PARTICLE_ATTRIBUTE(259, 359, Scale, Float3);
CASE_SET_PARTICLE_ATTRIBUTE(260, 360, RibbonWidth, Float);
CASE_SET_PARTICLE_ATTRIBUTE(261, 361, RibbonTwist, Float);
CASE_SET_PARTICLE_ATTRIBUTE(262, 362, RibbonFacingVector, Float3);
CASE_SET_PARTICLE_ATTRIBUTE(263, 363, Radius, Float);
#undef CASE_SET_PARTICLE_ATTRIBUTE
// Conform to Sphere
case GRAPH_NODE_MAKE_TYPE(15, 305):
case GRAPH_NODE_MAKE_TYPE(15, 335): // Conform to Global SDF
{
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Velocity, Float3, 1);
USE_ATTRIBUTE(Mass, Float, 2);
break;
}
// Kill (sphere/box)
case GRAPH_NODE_MAKE_TYPE(15, 306):
case GRAPH_NODE_MAKE_TYPE(15, 307):
{
USE_ATTRIBUTE(Position, Float3, 0);
break;
}
// Collision (plane/sphere/box/cylinder/depth/Global SDF)
case GRAPH_NODE_MAKE_TYPE(15, 330):
case GRAPH_NODE_MAKE_TYPE(15, 331):
case GRAPH_NODE_MAKE_TYPE(15, 332):
case GRAPH_NODE_MAKE_TYPE(15, 333):
case GRAPH_NODE_MAKE_TYPE(15, 334):
case GRAPH_NODE_MAKE_TYPE(15, 336):
{
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Velocity, Float3, 1);
USE_ATTRIBUTE(Age, Float, 2);
break;
}
// Sprite Rendering
case GRAPH_NODE_MAKE_TYPE(15, 400):
{
node->Assets.Resize(1);
node->Assets[0] = Content::LoadAsync<Asset>((Guid)node->Values[2]);
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Rotation, Float3, 1);
USE_ATTRIBUTE(SpriteSize, Float2, 2);
break;
}
// Sort
case GRAPH_NODE_MAKE_TYPE(15, 402):
{
const auto sortMode = static_cast<ParticleSortMode>(node->Values[2].AsInt);
if (sortMode == ParticleSortMode::CustomAscending || sortMode == ParticleSortMode::CustomDescending)
{
const StringView name(node->Values[3]);
auto idx = Layout.FindAttribute(name);
if (idx == -1)
{
LOG(Warning, "Particles sort module uses missing particle attribute {0}.", name.Get());
}
else
{
switch (Layout.Attributes[idx].ValueType)
{
case ParticleAttribute::ValueTypes::Float:
case ParticleAttribute::ValueTypes::Int:
case ParticleAttribute::ValueTypes::Uint:
break;
default:
LOG(Warning, "Particles sort module uses invalid particle attribute {0} of type {1}. It has to be a scalar value.", name.Get(), (int32)Layout.Attributes[idx].ValueType);
}
}
node->Attributes[0] = idx;
}
break;
}
// Model Rendering
case GRAPH_NODE_MAKE_TYPE(15, 403):
{
node->Assets.Resize(2);
node->Assets[0] = Content::LoadAsync<Asset>((Guid)node->Values[2]);
node->Assets[1] = Content::LoadAsync<Asset>((Guid)node->Values[3]);
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Rotation, Float3, 1);
USE_ATTRIBUTE(Scale, Float3, 2);
break;
}
// Ribbon Rendering
case GRAPH_NODE_MAKE_TYPE(15, 404):
{
node->Assets.Resize(1);
node->Assets[0] = Content::LoadAsync<Asset>((Guid)node->Values[2]);
USE_ATTRIBUTE(Position, Float3, 0);
// TODO: add support for custom sorting key - not only by age
USE_ATTRIBUTE(Age, Float, 1);
break;
}
// Volumetric Fog Rendering
case GRAPH_NODE_MAKE_TYPE(15, 405):
{
node->Assets.Resize(1);
node->Assets[0] = Content::LoadAsync<Asset>((Guid)node->Values[2]);
USE_ATTRIBUTE(Position, Float3, 0);
USE_ATTRIBUTE(Radius, Float, 1);
break;
}
}
// Check all input boxes
for (int32 i = 0; i < node->Boxes.Count(); i++)
{
auto box = node->Boxes[i];
for (int32 j = 0; j < box.Connections.Count(); j++)
{
const auto other = box.Connections[j]->template GetParent<NodeType>();
InitializeNode(other);
// Propagate UsesParticleData from the input boxes
if (other->Used)
{
node->UsesParticleData |= other->UsesParticleData;
node->IsConstant &= other->IsConstant;
}
}
}
#undef USE_ATTRIBUTE
}
public:
// [Graph]
void Clear() override
{
// Clear cached data
Root = nullptr;
Layout.Clear();
SpawnModules.Clear();
InitModules.Clear();
UpdateModules.Clear();
RenderModules.Clear();
LightModules.Clear();
SortModules.Clear();
RibbonRenderingModules.Clear();
UsesVolumetricFogRendering = false;
// Base
BaseType::Clear();
}
bool Load(ReadStream* stream, bool loadMeta) override
{
// Bump up version on reload
Version++;
// Base
if (BaseType::Load(stream, loadMeta))
return true;
// Compute particle data layout and initialize used nodes (for only used nodes, start depth searching rom the modules)
//Layout.AddAttribute(TEXT("Position"), ParticleAttribute::ValueTypes::Float3);
#define PROCESS_MODULES(modules) for (int32 i = 0; i < modules.Count(); i++) { modules[i]->Used = false; InitializeNode(modules[i]); }
PROCESS_MODULES(SpawnModules);
PROCESS_MODULES(InitModules);
PROCESS_MODULES(UpdateModules);
PROCESS_MODULES(RenderModules);
#undef PROCESS_MODULES
Layout.UpdateLayout();
AttributesDefaults.Resize(Layout.Attributes.Count());
// Ensure that spawn modules are not using particle data (not supported)
for (int32 i = SpawnModules.Count() - 1; i >= 0; --i)
{
if (SpawnModules[i]->UsesParticleData)
{
LOG(Warning, "Particle spawn module uses particle data as an input which is invalid. Disabling spawn module.");
SpawnModules.RemoveAtKeepOrder(i);
}
}
// Peek the root node options
Capacity = 0;
if (Root && Root->Values.Count() > 3)
{
Capacity = Root->Values[0].AsInt;
SimulationSpace = (ParticlesSimulationSpace)Root->Values[2].AsInt;
}
// Cache common attributes and initialize defaults
for (int32 i = 0; i < AttributesDefaults.Count(); i++)
AttributesDefaults[i] = Variant::Zero;
int32 idx;
#define SETUP_ATTRIBUTE(name, type, defaultValue) \
idx = Layout.FindAttribute(TEXT(MACRO_TO_STR(name)), ParticleAttribute::ValueTypes::type); \
_attr##name = idx; \
if (idx != -1) \
AttributesDefaults[idx] = defaultValue
SETUP_ATTRIBUTE(Position, Float3, Variant(Float3::Zero));
SETUP_ATTRIBUTE(Velocity, Float3, Variant(Float3::Zero));
SETUP_ATTRIBUTE(Rotation, Float3, Variant(Float3::Zero));
SETUP_ATTRIBUTE(AngularVelocity, Float3, Variant(Float3::Zero));
SETUP_ATTRIBUTE(Age, Float, Variant::Zero);
SETUP_ATTRIBUTE(Lifetime, Float, Variant(5.0f));
SETUP_ATTRIBUTE(SpriteSize, Float2, Variant(Float2(50.0f)));
SETUP_ATTRIBUTE(Scale, Float3, Variant(Float3::One));
SETUP_ATTRIBUTE(Mass, Float, Variant(1.0f));
SETUP_ATTRIBUTE(RibbonWidth, Float, Variant(10.0f));
SETUP_ATTRIBUTE(Color, Float4, Variant(Float4(0.0f, 0.0f, 0.0f, 1.0f)));
SETUP_ATTRIBUTE(Radius, Float, Variant(100.0f));
#undef SETUP_ATTRIBUTE
return false;
}
bool onNodeLoaded(NodeType* n) override
{
// Root node
if (n->Type == PARTICLE_EMITTER_ROOT_NODE_TYPE)
{
ASSERT(!Root);
Root = n;
}
// Particle Modules (only if module is enabled)
else if (n->GroupID == 15 && n->Values[0].AsBool)
{
const auto moduleType = static_cast<ModuleType>(n->Values[1].AsInt);
switch (moduleType)
{
case ModuleType::Spawn:
SpawnModules.Add(n);
break;
case ModuleType::Initialize:
InitModules.Add(n);
break;
case ModuleType::Update:
UpdateModules.Add(n);
break;
case ModuleType::Render:
RenderModules.Add(n);
switch (n->TypeID)
{
case 401:
LightModules.Add(n);
break;
case 402:
if (static_cast<ParticleSortMode>(n->Values[2].AsInt) != ParticleSortMode::None)
SortModules.Add(n);
break;
case 404:
RibbonRenderingModules.Add(n);
break;
case 405:
UsesVolumetricFogRendering = true;
break;
}
break;
}
}
return BaseType::onNodeLoaded(n);
}
};