Merge remote-tracking branch 'origin/master' into 1.11

# Conflicts:
#	Source/Engine/Particles/Particles.cpp
This commit is contained in:
Wojtek Figat
2025-07-14 18:35:53 +02:00
17 changed files with 394 additions and 119 deletions

View File

@@ -374,14 +374,7 @@ namespace FlaxEditor.Viewport
// Draw selected objects debug shapes and visuals
if (DrawDebugDraw && (renderContext.View.Flags & ViewFlags.DebugDraw) == ViewFlags.DebugDraw)
{
unsafe
{
fixed (IntPtr* actors = _debugDrawData.ActorsPtrs)
{
DebugDraw.DrawActors(new IntPtr(actors), _debugDrawData.ActorsCount, true);
}
}
_debugDrawData.DrawActors();
DebugDraw.Draw(ref renderContext, target.View(), targetDepth.View(), true);
}
}

View File

@@ -643,14 +643,7 @@ namespace FlaxEditor.Viewport
if (selectedParents[i].IsActiveInHierarchy)
selectedParents[i].OnDebugDraw(_debugDrawData);
}
unsafe
{
fixed (IntPtr* actors = _debugDrawData.ActorsPtrs)
{
DebugDraw.DrawActors(new IntPtr(actors), _debugDrawData.ActorsCount, false);
}
}
_debugDrawData.DrawActors();
// Debug draw all actors in prefab and collect actors
var view = Task.View;

View File

@@ -246,6 +246,14 @@ namespace FlaxEditor.Viewport.Previews
}
}
/// <inheritdoc />
protected override void OnDebugDraw(GPUContext context, ref RenderContext renderContext)
{
base.OnDebugDraw(context, ref renderContext);
_previewEffect.OnDebugDraw();
}
/// <inheritdoc />
public override void Draw()
{
@@ -295,7 +303,8 @@ namespace FlaxEditor.Viewport.Previews
/// <inheritdoc />
public override void OnDestroy()
{
// Cleanup objects
if (IsDisposing)
return;
_previewEffect.ParticleSystem = null;
Object.Destroy(ref _previewEffect);
Object.Destroy(ref _boundsModel);

View File

@@ -88,6 +88,18 @@ namespace FlaxEditor
}
}
/// <summary>
/// Draws the collected actors via <see cref="DebugDraw"/>.
/// </summary>
/// <param name="drawScenes">True if draw all loaded scenes too, otherwise will draw only provided actors.</param>
public unsafe void DrawActors(bool drawScenes = false)
{
fixed (IntPtr* actors = ActorsPtrs)
{
DebugDraw.DrawActors(new IntPtr(actors), _actors.Count, drawScenes);
}
}
/// <summary>
/// Called when task calls <see cref="SceneRenderTask.CollectDrawCalls" /> event.
/// </summary>

View File

@@ -113,8 +113,55 @@ namespace FlaxEditor.Windows.Assets
}
}
private sealed class LayoutTabProxy
{
[EditorDisplay("Layout"), CustomEditor(typeof(Editor)), NoSerialize]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public ParticleEmitterWindow Window;
private class Editor : CustomEditor
{
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
public override void Initialize(LayoutElementsContainer layout)
{
var window = (ParticleEmitterWindow)Values[0];
var emitter = window.Preview.Emitter;
if (emitter == null || !emitter.IsLoaded)
return;
var attributes = emitter.Layout;
var size = 0;
var height = 14;
foreach (var attribute in attributes)
{
layout.Label($" - {GetAttributeType(attribute.Format)} {attribute.Name}").Label.Height = height;
size += PixelFormatExtensions.SizeInBytes(attribute.Format);
}
var capacity = 0;
if (window.Surface != null && window.Surface.RootNode != null && window.Surface.RootNode.Values.Length > 0)
capacity = (int)window.Surface.RootNode.Values[0];
layout.Space(10);
layout.Label($"Particle size: {size} bytes\nParticle buffer size: {Utilities.Utils.FormatBytesCount((ulong)(size * capacity))}").Label.Height = height * 2;
}
private static string GetAttributeType(PixelFormat format)
{
switch (format)
{
case PixelFormat.R32_Float: return "float";
case PixelFormat.R32G32_Float: return "Float2";
case PixelFormat.R32G32B32_Float: return "Float3";
case PixelFormat.R32G32B32A32_Float: return "Float4";
case PixelFormat.R32_SInt: return "int";
case PixelFormat.R32_UInt: return "uint";
default: return format.ToString();
}
}
}
}
private readonly PropertiesProxy _properties;
private Tab _previewTab;
private Tab _previewTab, _layoutTab;
private ToolStripButton _showSourceCodeButton;
/// <inheritdoc />
@@ -127,18 +174,22 @@ namespace FlaxEditor.Windows.Assets
PlaySimulation = true,
Parent = _split2.Panel1
};
_preview.PreviewActor.ShowDebugDraw = true;
_preview.ShowDebugDraw = true;
// Asset properties proxy
_properties = new PropertiesProxy();
// Preview properties editor
_previewTab = new Tab("Preview");
_previewTab.Presenter.Select(new PreviewProxy
{
Window = this,
});
_previewTab.Presenter.Select(new PreviewProxy { Window = this });
_tabs.AddTab(_previewTab);
// Particle data layout
_layoutTab = new Tab("Layout");
_layoutTab.Presenter.Select(new LayoutTabProxy { Window = this });
_tabs.AddTab(_layoutTab);
// Surface
_surface = new ParticleEmitterSurface(this, Save, _undo)
{
@@ -237,6 +288,7 @@ namespace FlaxEditor.Windows.Assets
_asset.WaitForLoaded();
_preview.PreviewActor.ResetSimulation();
_previewTab.Presenter.BuildLayoutOnUpdate();
_layoutTab.Presenter.BuildLayoutOnUpdate();
}
}
@@ -253,6 +305,7 @@ namespace FlaxEditor.Windows.Assets
// Init asset properties and parameters proxy
_properties.OnLoad(this);
_previewTab.Presenter.BuildLayoutOnUpdate();
_layoutTab.Presenter.BuildLayoutOnUpdate();
return false;
}

View File

@@ -510,13 +510,7 @@ namespace FlaxEditor.Windows
selectedParents[i].OnDebugDraw(drawDebugData);
}
}
unsafe
{
fixed (IntPtr* actors = drawDebugData.ActorsPtrs)
{
DebugDraw.DrawActors(new IntPtr(actors), drawDebugData.ActorsCount, true);
}
}
drawDebugData.DrawActors(true);
}
DebugDraw.Draw(ref renderContext, task.OutputView);

View File

@@ -2,8 +2,16 @@
#include "ParticleEmitterGraph.CPU.h"
#include "Engine/Core/Random.h"
#include "Engine/Core/Math/Vector2.h"
#include "Engine/Core/Math/Vector3.h"
#include "Engine/Core/Math/Vector4.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Math/Quaternion.h"
#include "Engine/Core/Math/BoundingBox.h"
#include "Engine/Core/Math/BoundingSphere.h"
#include "Engine/Core/Math/OrientedBoundingBox.h"
#include "Engine/Utilities/Noise.h"
#include "Engine/Core/Types/CommonValue.h"
#include "Engine/Debug/DebugDraw.h"
// ReSharper disable CppCStyleCast
// ReSharper disable CppClangTidyClangDiagnosticCastAlign
@@ -1468,3 +1476,89 @@ void ParticleEmitterGraphCPUExecutor::ProcessModule(ParticleEmitterGraphCPUNode*
#undef COLLISION_LOGIC
}
}
#if USE_EDITOR
void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNode* node, const Transform& transform)
{
// Skip modules that rely on particle data
if (node->UsePerParticleDataResolve())
return;
const Color color = Color::White;
switch (node->TypeID)
{
case 202: // Position (sphere surface)
case 211: // Position (sphere volume)
{
const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2));
const float radius = (float)GetValue(node->GetBox(1), 3);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius), color, 0.0f, true);
break;
}
case 203: // Position (plane)
{
const Float3 center = (Float3)GetValue(node->GetBox(0), 2);
const Float2 size = (Float2)GetValue(node->GetBox(1), 3);
const Float3 halfExtent = Float3(size.X * 0.5f, 0.0f, size.Y * 0.5f);
OrientedBoundingBox box(halfExtent, Transform(center));
box.Transform(transform);
DEBUG_DRAW_WIRE_BOX(box, color, 0.0f, true);
break;
}
case 204: // Position (circle)
case 205: // Position (disc)
{
const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2));
const float radius = (float)GetValue(node->GetBox(1), 3);
DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, 0.0f, color, 0.0f, true);
break;
}
case 206: // Position (box surface)
case 207: // Position (box volume)
{
const Float3 center = (Float3)GetValue(node->GetBox(0), 2);
const Float3 size = (Float3)GetValue(node->GetBox(1), 3);
OrientedBoundingBox box(size * 0.5f, Transform(center));
box.Transform(transform);
DEBUG_DRAW_WIRE_BOX(box, color, 0.0f, true);
break;
}
// Position (cylinder)
case 208:
{
const float height = (float)GetValue(node->GetBox(2), 4);
const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2) + Float3(0, 0, height * 0.5f));
const float radius = (float)GetValue(node->GetBox(1), 3);
DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, height, color, 0.0f, true);
break;
}
// Position (line)
case 209:
{
const Float3 start = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2));
const Float3 end = transform.LocalToWorld((Float3)GetValue(node->GetBox(1), 3));
DEBUG_DRAW_LINE(start, end, color, 0.0f, true);
break;
}
// Position (torus)
case 210:
{
const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2));
const float radius = Math::Max((float)GetValue(node->GetBox(1), 3), ZeroTolerance);
const float thickness = (float)GetValue(node->GetBox(2), 4);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius + thickness), color, 0.0f, true);
break;
}
// Position (spiral)
case 214:
{
const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2));
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, 5.0f), color, 0.0f, true);
break;
}
}
}
#endif

View File

@@ -7,6 +7,7 @@
#include "Engine/Particles/ParticleEffect.h"
#include "Engine/Engine/Time.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Debug/DebugDraw.h"
ThreadLocal<ParticleEmitterGraphCPUContext*> ParticleEmitterGraphCPUExecutor::Context;
@@ -423,6 +424,23 @@ void ParticleEmitterGraphCPUExecutor::Draw(ParticleEmitter* emitter, ParticleEff
}
}
#if USE_EDITOR
void ParticleEmitterGraphCPUExecutor::DrawDebug(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data)
{
// Prepare graph data
Init(emitter, effect, data);
Transform transform = emitter->SimulationSpace == ParticlesSimulationSpace::Local ? effect->GetTransform() : Transform::Identity;
// Draw modules
for (auto module : emitter->Graph.SpawnModules)
DebugDrawModule(module, transform);
for (auto module : emitter->Graph.InitModules)
DebugDrawModule(module, transform);
}
#endif
void ParticleEmitterGraphCPUExecutor::Update(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, float dt, bool canSpawn)
{
// Prepare data

View File

@@ -162,6 +162,16 @@ public:
/// <param name="transform">The effect transform matrix.</param>
void Draw(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, RenderContext& renderContext, Matrix& transform);
#if USE_EDITOR
/// <summary>
/// Draws the particles debug shapes.
/// </summary>
/// <param name="emitter">The owning emitter.</param>
/// <param name="effect">The instance effect.</param>
/// <param name="data">The instance data.</param>
void DrawDebug(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data);
#endif
/// <summary>
/// Updates the particles simulation (the CPU simulation).
/// </summary>
@@ -195,6 +205,9 @@ private:
int32 ProcessSpawnModule(int32 index);
void ProcessModule(ParticleEmitterGraphCPUNode* node, int32 particlesStart, int32 particlesEnd);
#if USE_EDITOR
void DebugDrawModule(ParticleEmitterGraphCPUNode* node, const Transform& transform);
#endif
FORCE_INLINE Value GetValue(Box* box, int32 defaultValueBoxIndex)
{

View File

@@ -587,10 +587,19 @@ void ParticleEffect::OnDebugDrawSelected()
{
DEBUG_DRAW_WIRE_BOX(_box, Color::Violet * 0.7f, 0, true);
// Base
Actor::OnDebugDrawSelected();
}
void ParticleEffect::OnDebugDraw()
{
if (ShowDebugDraw)
{
Particles::DebugDraw(this);
}
Actor::OnDebugDraw();
}
#endif
void ParticleEffect::OnLayerChanged()

View File

@@ -244,6 +244,13 @@ public:
API_FIELD(Attributes="EditorDisplay(\"Particle Effect\"), EditorOrder(80), DefaultValue(0)")
int8 SortOrder = 0;
#if USE_EDITOR
/// <summary>
/// If checked, the particle emitter debug shapes will be shawn during debug drawing. This includes particle spawn location shapes display.
/// </summary>
API_FIELD(Attributes = "EditorDisplay(\"Particle Effect\"), EditorOrder(200)") bool ShowDebugDraw = false;
#endif
public:
/// <summary>
/// Gets the effect parameters collection. Those parameters are instanced from the <see cref="ParticleSystem"/> that contains a linear list of emitters and every emitter has a list of own parameters.
@@ -399,6 +406,7 @@ public:
void Draw(RenderContext& renderContext) override;
#if USE_EDITOR
void OnDebugDrawSelected() override;
void OnDebugDraw() override;
#endif
void OnLayerChanged() override;
void Serialize(SerializeStream& stream, const void* otherObj) override;

View File

@@ -522,4 +522,39 @@ bool ParticleEmitter::HasShaderCode() const
return false;
}
Array<ParticleEmitter::Attribute> ParticleEmitter::GetLayout() const
{
Array<Attribute> result;
ScopeLock lock(Locker);
result.Resize(Graph.Layout.Attributes.Count());
for (int32 i = 0; i < result.Count(); i++)
{
auto& dst = result[i];
const auto& src = Graph.Layout.Attributes[i];
dst.Name = src.Name;
switch (src.ValueType)
{
case ParticleAttribute::ValueTypes::Float:
dst.Format = PixelFormat::R32_Float;
break;
case ParticleAttribute::ValueTypes::Float2:
dst.Format = PixelFormat::R32G32_Float;
break;
case ParticleAttribute::ValueTypes::Float3:
dst.Format = PixelFormat::R32G32B32_Float;
break;
case ParticleAttribute::ValueTypes::Float4:
dst.Format = PixelFormat::R32G32B32A32_Float;
break;
case ParticleAttribute::ValueTypes::Int:
dst.Format = PixelFormat::R32_SInt;
break;
case ParticleAttribute::ValueTypes::Uint:
dst.Format = PixelFormat::R32_UInt;
break;
}
}
return result;
}
#endif

View File

@@ -177,10 +177,16 @@ public:
void GetReferences(Array<Guid>& assets, Array<String>& files) const override;
bool Save(const StringView& path = StringView::Empty) override;
/// <summary>
/// Checks if the particle emitter has valid shader code present.
/// </summary>
API_PROPERTY() bool HasShaderCode() const;
API_STRUCT(Internal) struct Attribute
{
DECLARE_SCRIPTING_TYPE_MINIMAL(Attribute);
API_FIELD() PixelFormat Format;
API_FIELD() String Name;
};
private:
API_PROPERTY(Internal) bool HasShaderCode() const;
API_PROPERTY(Internal) Array<Attribute> GetLayout() const;
#endif
protected:

View File

@@ -936,6 +936,7 @@ void Particles::DrawParticles(RenderContext& renderContext, ParticleEffect* effe
if (drawModes == DrawPass::None || SpriteRenderer.Init())
return;
PROFILE_MEM(Particles);
ConcurrentSystemLocker::ReadScope systemScope(SystemLocker);
Matrix worlds[2];
Matrix::Translation(-renderContext.View.Origin, worlds[0]); // World
renderContext.View.GetWorldMatrix(effect->GetTransform(), worlds[1]); // Local
@@ -1068,6 +1069,28 @@ void Particles::DrawParticles(RenderContext& renderContext, ParticleEffect* effe
}
}
#if USE_EDITOR
void Particles::DebugDraw(ParticleEffect* effect)
{
PROFILE_CPU_NAMED("Particles.DrawDebug");
ConcurrentSystemLocker::ReadScope systemScope(SystemLocker);
// Draw all emitters
for (auto& emitterData : effect->Instance.Emitters)
{
const auto buffer = emitterData.Buffer;
if (!buffer)
continue;
auto emitter = buffer->Emitter;
if (!emitter || !emitter->IsLoaded())
continue;
emitter->GraphExecutorCPU.DrawDebug(emitter, effect, emitterData);
}
}
#endif
#if COMPILE_WITH_GPU_PARTICLES
void UpdateGPU(RenderTask* task, GPUContext* context)

View File

@@ -52,6 +52,14 @@ public:
/// <param name="effect">The owning actor.</param>
static void DrawParticles(RenderContext& renderContext, ParticleEffect* effect);
#if USE_EDITOR
/// <summary>
/// Draws the particles debug shapes.
/// </summary>
/// <param name="effect">The owning actor.</param>
static void DebugDraw(ParticleEffect* effect);
#endif
public:
/// <summary>
/// Enables or disables particle buffer pooling.

View File

@@ -7,6 +7,89 @@
#include "Engine/Graphics/DynamicBuffer.h"
#include "Engine/Profiler/ProfilerMemory.h"
int32 ParticleAttribute::GetSize() const
{
switch (ValueType)
{
case ValueTypes::Float2:
return 8;
case ValueTypes::Float3:
return 12;
case ValueTypes::Float4:
return 16;
case ValueTypes::Float:
case ValueTypes::Int:
case ValueTypes::Uint:
return 4;
default:
return 0;
}
}
void ParticleLayout::Clear()
{
Size = 0;
Attributes.Clear();
}
void ParticleLayout::UpdateLayout()
{
Size = 0;
for (int32 i = 0; i < Attributes.Count(); i++)
{
Attributes[i].Offset = Size;
Size += Attributes[i].GetSize();
}
}
int32 ParticleLayout::FindAttribute(const StringView& name) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (name == Attributes[i].Name)
return i;
}
return -1;
}
int32 ParticleLayout::FindAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (Attributes[i].ValueType == valueType && name == Attributes[i].Name)
return i;
}
return -1;
}
int32 ParticleLayout::FindAttributeOffset(const StringView& name, int32 fallbackValue) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (name == Attributes[i].Name)
return Attributes[i].Offset;
}
return fallbackValue;
}
int32 ParticleLayout::FindAttributeOffset(const StringView& name, ParticleAttribute::ValueTypes valueType, int32 fallbackValue) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (Attributes[i].ValueType == valueType && name == Attributes[i].Name)
return Attributes[i].Offset;
}
return fallbackValue;
}
int32 ParticleLayout::AddAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType)
{
auto& a = Attributes.AddOne();
a.Name = String(*name, name.Length());
a.ValueType = valueType;
return Attributes.Count() - 1;
}
ParticleBuffer::ParticleBuffer()
{
}

View File

@@ -52,25 +52,7 @@ struct ParticleAttribute
/// <summary>
/// Gets the size of the attribute (in bytes).
/// </summary>
/// <returns>The size (in bytes).</returns>
int32 GetSize() const
{
switch (ValueType)
{
case ValueTypes::Float2:
return 8;
case ValueTypes::Float3:
return 12;
case ValueTypes::Float4:
return 16;
case ValueTypes::Float:
case ValueTypes::Int:
case ValueTypes::Uint:
return 4;
default:
return 0;
}
}
int32 GetSize() const;
};
/// <summary>
@@ -93,41 +75,19 @@ public:
/// <summary>
/// Clears the layout data.
/// </summary>
void Clear()
{
Size = 0;
Attributes.Clear();
}
void Clear();
/// <summary>
/// Updates the attributes layout (calculates offset) and updates the total size of the layout.
/// </summary>
void UpdateLayout()
{
Size = 0;
for (int32 i = 0; i < Attributes.Count(); i++)
{
Attributes[i].Offset = Size;
Size += Attributes[i].GetSize();
}
}
void UpdateLayout();
/// <summary>
/// Finds the attribute by the name.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>The attribute index or -1 if cannot find it.</returns>
int32 FindAttribute(const StringView& name) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (name == Attributes[i].Name)
{
return i;
}
}
return -1;
}
int32 FindAttribute(const StringView& name) const;
/// <summary>
/// Finds the attribute by the name and type.
@@ -135,17 +95,7 @@ public:
/// <param name="name">The name.</param>
/// <param name="valueType">The type.</param>
/// <returns>The attribute index or -1 if cannot find it.</returns>
int32 FindAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (Attributes[i].ValueType == valueType && name == Attributes[i].Name)
{
return i;
}
}
return -1;
}
int32 FindAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) const;
/// <summary>
/// Finds the attribute offset by the name.
@@ -153,17 +103,7 @@ public:
/// <param name="name">The name.</param>
/// <param name="fallbackValue">The fallback value to return if attribute is missing.</param>
/// <returns>The attribute offset or fallback value if cannot find it.</returns>
int32 FindAttributeOffset(const StringView& name, int32 fallbackValue = 0) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (name == Attributes[i].Name)
{
return Attributes[i].Offset;
}
}
return fallbackValue;
}
int32 FindAttributeOffset(const StringView& name, int32 fallbackValue = 0) const;
/// <summary>
/// Finds the attribute offset by the name.
@@ -172,17 +112,7 @@ public:
/// <param name="valueType">The type.</param>
/// <param name="fallbackValue">The fallback value to return if attribute is missing.</param>
/// <returns>The attribute offset or fallback value if cannot find it.</returns>
int32 FindAttributeOffset(const StringView& name, ParticleAttribute::ValueTypes valueType, int32 fallbackValue = 0) const
{
for (int32 i = 0; i < Attributes.Count(); i++)
{
if (Attributes[i].ValueType == valueType && name == Attributes[i].Name)
{
return Attributes[i].Offset;
}
}
return fallbackValue;
}
int32 FindAttributeOffset(const StringView& name, ParticleAttribute::ValueTypes valueType, int32 fallbackValue = 0) const;
/// <summary>
/// Gets the attribute offset by the attribute index.
@@ -201,13 +131,7 @@ public:
/// <param name="name">The name.</param>
/// <param name="valueType">The value type.</param>
/// <returns>The attribute index or -1 if cannot find it.</returns>
int32 AddAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType)
{
auto& a = Attributes.AddOne();
a.Name = String(*name, name.Length());
a.ValueType = valueType;
return Attributes.Count() - 1;
}
int32 AddAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType);
};
/// <summary>