系统性全面的讲解o3de如何架构粒子特效系统,给出代码和插件代码和架构结构,以及列出详细的步骤和分析和包含内容和要点和案例和规格
下面是一份系统性、全面的 O3DE 粒子特效系统架构讲解与实战指南,包含整体架构、模块关系、组件与数据流、代码与插件骨架、资源规范、开发步骤、调试方法、性能优化要点、案例与扩展示例。内容兼顾引擎内置体系(Atom + EMotionFX/Animation 相关上下文)、Gems 插件化方式、以及如何自研一个可扩展的粒子系统 Gem 与编辑器工具。
说明:
- 默认你已安装 O3DE(>= 23.x/24.x)并了解 CMake、C++17、Qt 编辑器扩展。
- 代码示例以 C++ 为主,配合少量 Script Canvas/Lua 触发。
- 由于篇幅所限,示例代码聚焦框架与关键路径,便于你扩展。
一、总体架构与模块关系
- 引擎层级
- Core/EBus/Reflection:基于 AZCore/AZ::Component/AZ::Rtti/AZ::Serialize 提供组件化、反射、事件总线。
- RHI/RPI/Atom:渲染抽象与管线(RHI 平台层、RPI 渲染管线接口、RP 具体 Pass/Material/Shader)。
- Physics/Animation:与粒子碰撞、风场、骨骼绑定等交互。
- Scripting:Script Canvas、Lua 驱动行为与触发。
- Editor:Qt/Prefab/Asset 系统、Property Editor、Viewport Manipulator。
- 粒子系统核心子模块
- ParticleSystem Gem:
- Runtime
- Emitters(发射器)组件:管理粒子生成、生命周期、池化、运行时更新。
- Updaters(更新器)模块:力场、加速度、噪声、碰撞、属性插值。
- Renderers(渲染器)模块:Billboard/Stretch Billboard/Mesh/Trail/Volume 渲染。
- GPU 粒子路径(可选):Compute/Draw 结合,借助 Atom Pass。
- Data/Assets
- Particle Effect Asset(.particle.json/.asset):系统顶层资源,包含多个 Emitter 配置与共享资源。
- Particle Material/Shader:材质与着色器,含粒子属性读入、软粒子深度衰减、灯光/阴影选项。
- Curves/Gradients:属性曲线与渐变贴图。
- Integration
- Triggers:基于 EBus 或 Script Canvas 触发粒子事件(开始、停止、Burst)。
- Physics/Collision:与 PhysX/CollisionRequest 交互计算粒子碰撞。
- Wind/Volume Fields:体积力场。
- Editor
- ParticleEffectEditor:粒子效果资源编辑器(独立 DockWidget 或 O3DE Editor 内工具)。
- Property Handlers:属性面板(发射率、初速、寿命曲线等)。
- Viewport Preview:内嵌预览、可录制。
- Tooling
- Asset Builders:粒子资源编译器(Source -> Product Asset)。
- Serialization/Versioning:资产版本升级脚本与结构。
- Runtime
- 数据流
- 关卡启动 -> EmitterComponent 激活 -> 注册到 ParticleWorld(系统单例/子系统管理器)
- 每帧:
- Emit:根据发射率/触发器生成粒子,初始化属性(位置、速度、随机种子)。
- Update:按顺序执行 Updaters(力、阻尼、碰撞、曲线采样、子发射器等)。
- Culling/Sorting:视锥裁剪、按相机距离排序(透明)。
- Render:CPU 路径构建实例缓冲 -> 提交到 Atom;或 GPU 路径由 Compute 更新粒子缓冲 -> 渲染 Pass 采样。
- 生命周期结束:回收到池中。
二、粒子系统关键设计要点
- 组件化:
- ParticleEmitterComponent:可放置在实体上,暴露属性。
- ParticleSystemComponent(可选):管理场景中所有粒子世界状态、全局参数。
- 资产驱动:
- 粒子系统/发射器/材质/曲线均为资产,可复用与版本化。
- 性能:
- 池化、SoA(Structure of Arrays)存储、SIMD。
- 批次合并、实例化渲染、GPU 计算。
- 可扩展:
- Updater 插件化注册表。
- 自定义 Renderer 与 Pass。
- EBus 扩展触发器、事件回调。
三、Gem 插件架构结构建议
目录结构示例(Gem 名称:ParticleSystemGem):
- Gems/ParticleSystemGem/
- gem.json
- Code/
- Include/ParticleSystemGem/
- ParticleFeatureBus.h
- ParticleEmitterComponentBus.h
- ParticleTypes.h
- ParticleWorldInterface.h
- ParticleEmitterComponent.h
- ParticleSystemComponent.h
- Updaters/
- IParticleUpdater.h
- GravityUpdater.h
- ColorOverLifeUpdater.h
- CollisionUpdater.h
- Rendering/
- IParticleRenderer.h
- BillboardRenderer.h
- MeshRenderer.h
- Assets/
- ParticleEffectAsset.h
- ParticleEmitterAsset.h
- Curves.h
- Source/
- Module.cpp
- SystemComponent.cpp/h
- ParticleWorld.cpp/h
- ParticleEmitterComponent.cpp
- ParticleSystemComponent.cpp
- Updaters/...
- Rendering/...
- Assets/
- ParticleEffectAsset.cpp
- ParticleAssetBuilder.cpp
- Editor/
- EditorParticleEmitterComponent.cpp/h
- EditorSystemComponent.cpp/h
- ParticleEffectEditorWindow.cpp/h
- PropertyHandlers.cpp/h
- Include/ParticleSystemGem/
- Assets/
- Shaders/
- ParticleBillboard.azsl
- ParticleCommon.azsli
- ParticleCompute.azsl
- Materials/
- ParticleDefault.azmaterial
- Textures/
- Gradients/...
- Shaders/
- Registry/
- assetcatalog.xml
- gem.json
四、核心接口与代码骨架
- Module 与 SystemComponent
- Module.cpp
cpp
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Module/Module.h>
#include <AzCore/RTTI/RTTI.h>
#include "SystemComponent.h"
#include "ParticleEmitterComponent.h"
#include "ParticleSystemComponent.h"
#include "Editor/EditorSystemComponent.h"
#include "Assets/ParticleEffectAsset.h"
namespace ParticleSystemGem
{
class ParticleSystemModule
: public AZ::Module
{
public:
AZ_RTTI(ParticleSystemModule, "{A1B6A0C2-9E07-4C26-9F53-98C3E7B2D9A1}", AZ::Module);
AZ_CLASS_ALLOCATOR(ParticleSystemModule, AZ::SystemAllocator);
ParticleSystemModule()
{
m_descriptors.insert(m_descriptors.end(), {
SystemComponent::CreateDescriptor(),
ParticleEmitterComponent::CreateDescriptor(),
ParticleSystemComponent::CreateDescriptor(),
#if defined(AZ_EDITOR)
EditorSystemComponent::CreateDescriptor(),
#endif
ParticleEffectAsset::CreateDescriptor()
});
}
AZ::ComponentTypeList GetRequiredSystemComponents() const override
{
return AZ::ComponentTypeList{
azrtti_typeid<SystemComponent>()
};
}
};
}
AZ_DECLARE_MODULE_CLASS(Gem_ParticleSystemGem, ParticleSystemGem::ParticleSystemModule)
- SystemComponent.h/cpp(注册 EBus、反射、全局 ParticleWorld)
cpp
// SystemComponent.h
#pragma once
#include <AzCore/Component/Component.h>
#include "ParticleWorldInterface.h"
namespace ParticleSystemGem
{
class SystemComponent
: public AZ::Component
{
public:
AZ_COMPONENT(SystemComponent, "{8C8F4A5B-7098-4D6E-A1B0-44B5A8E2C912}");
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
void Activate() override;
void Deactivate() override;
ParticleWorldInterface* GetWorld() { return m_world.get(); }
private:
AZStd::unique_ptr<ParticleWorldInterface> m_world;
};
}
cpp
// SystemComponent.cpp
#include "SystemComponent.h"
#include <AzCore/Serialization/SerializeContext.h>
#include "ParticleWorld.h"
namespace ParticleSystemGem
{
void SystemComponent::Reflect(AZ::ReflectContext* context)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<SystemComponent, AZ::Component>()
->Version(1);
}
}
void SystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC_CE("ParticleSystemService"));
}
void SystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC_CE("ParticleSystemService"));
}
void SystemComponent::Activate()
{
m_world = AZStd::make_unique<ParticleWorld>();
m_world->Initialize();
}
void SystemComponent::Deactivate()
{
if (m_world)
{
m_world->Shutdown();
m_world.reset();
}
}
}
- 粒子世界与接口
cpp
// ParticleWorldInterface.h
#pragma once
#include <AzCore/base.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/std/containers/vector.h>
#include "ParticleTypes.h"
namespace ParticleSystemGem
{
class ParticleWorldInterface
{
public:
virtual ~ParticleWorldInterface() = default;
virtual void Initialize() = 0;
virtual void Shutdown() = 0;
virtual void RegisterEmitter(class ParticleEmitterComponent* emitter) = 0;
virtual void UnregisterEmitter(class ParticleEmitterComponent* emitter) = 0;
virtual void Tick(float deltaTime) = 0;
};
}
cpp
// ParticleWorld.h
#pragma once
#include "ParticleWorldInterface.h"
#include <AzCore/std/containers/vector.h>
namespace ParticleSystemGem
{
class ParticleWorld final : public ParticleWorldInterface
{
public:
void Initialize() override;
void Shutdown() override;
void RegisterEmitter(class ParticleEmitterComponent* emitter) override;
void UnregisterEmitter(class ParticleEmitterComponent* emitter) override;
void Tick(float deltaTime) override;
private:
AZStd::vector<class ParticleEmitterComponent*> m_emitters;
};
}
cpp
// ParticleWorld.cpp
#include "ParticleWorld.h"
#include "ParticleEmitterComponent.h"
namespace ParticleSystemGem
{
void ParticleWorld::Initialize() {}
void ParticleWorld::Shutdown() { m_emitters.clear(); }
void ParticleWorld::RegisterEmitter(ParticleEmitterComponent* emitter)
{
m_emitters.push_back(emitter);
}
void ParticleWorld::UnregisterEmitter(ParticleEmitterComponent* emitter)
{
m_emitters.erase(AZStd::remove(m_emitters.begin(), m_emitters.end(), emitter), m_emitters.end());
}
void ParticleWorld::Tick(float dt)
{
for (auto* emitter : m_emitters)
{
emitter->OnTick(dt);
}
}
}
- 粒子数据与发射器组件
cpp
// ParticleTypes.h
#pragma once
#include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Color.h>
#include <AzCore/Math/Quaternion.h>
#include <AzCore/std/containers/vector.h>
namespace ParticleSystemGem
{
struct Particle
{
AZ::Vector3 position;
AZ::Vector3 velocity;
float age = 0.f;
float lifetime = 1.f;
AZ::Color color = AZ::Colors::White;
float size = 1.f;
float rotation = 0.f;
float angularVelocity = 0.f;
uint32_t alive = 0;
};
struct EmitterRuntimeData
{
AZStd::vector<Particle> particles;
size_t aliveCount = 0;
float emitAccumulator = 0.f;
};
}
cpp
// ParticleEmitterComponent.h
#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/Component/TickBus.h>
#include "ParticleTypes.h"
#include "Assets/ParticleEmitterAsset.h"
namespace ParticleSystemGem
{
class ParticleEmitterComponent
: public AZ::Component
, public AZ::TickBus::Handler
{
public:
AZ_COMPONENT(ParticleEmitterComponent, "{3B4D5456-28E2-4C2F-9C7C-45E48F6A4F1E}");
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
void Activate() override;
void Deactivate() override;
// TickBus
void OnTick(float deltaTime, AZ::ScriptTimePoint) override;
// Called by world
void OnTick(float dt);
// Control
void Start();
void Stop();
void Burst(uint32_t count);
private:
EmitterRuntimeData m_runtime;
bool m_running = true;
AZ::Data::Asset<ParticleEmitterAsset> m_emitterAsset;
};
}
cpp
// ParticleEmitterComponent.cpp
#include "ParticleEmitterComponent.h"
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Component/Entity.h>
#include "ParticleWorldInterface.h"
#include "SystemComponent.h"
namespace ParticleSystemGem
{
void ParticleEmitterComponent::Reflect(AZ::ReflectContext* context)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<ParticleEmitterComponent, AZ::Component>()
->Version(1)
->Field("EmitterAsset", &ParticleEmitterComponent::m_emitterAsset);
}
}
void ParticleEmitterComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC_CE("ParticleEmitterService"));
}
void ParticleEmitterComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(AZ_CRC_CE("TransformService"));
}
void ParticleEmitterComponent::Activate()
{
AZ::TickBus::Handler::BusConnect();
// register to world
// locate SystemComponent on the application
// Simplified: could use a ServiceLocator or EBus
// ...
m_runtime.particles.resize(10000); // pool
}
void ParticleEmitterComponent::Deactivate()
{
AZ::TickBus::Handler::BusDisconnect();
// unregister from world
}
void ParticleEmitterComponent::OnTick(float deltaTime, AZ::ScriptTimePoint)
{
OnTick(deltaTime);
}
void ParticleEmitterComponent::OnTick(float dt)
{
if (!m_running) return;
// Emit
if (m_emitterAsset.IsReady())
{
float rate = m_emitterAsset->m_emitRate; // particles per second
m_runtime.emitAccumulator += rate * dt;
uint32_t toEmit = static_cast<uint32_t>(m_runtime.emitAccumulator);
m_runtime.emitAccumulator -= toEmit;
Burst(toEmit);
}
// Update
size_t alive = m_runtime.aliveCount;
for (size_t i = 0; i < alive; )
{
Particle& p = m_runtime.particles[i];
p.age += dt;
if (p.age >= p.lifetime)
{
// swap with last alive
--alive;
AZStd::swap(m_runtime.particles[i], m_runtime.particles[alive]);
continue;
}
// simple Euler
p.velocity += AZ::Vector3(0.f, 0.f, -9.8f) * dt; // gravity
p.position += p.velocity * dt;
++i;
}
m_runtime.aliveCount = alive;
// TODO: push to renderer instance buffer or notify renderer subsystem
}
void ParticleEmitterComponent::Start() { m_running = true; }
void ParticleEmitterComponent::Stop() { m_running = false; }
void ParticleEmitterComponent::Burst(uint32_t count)
{
while (count-- && m_runtime.aliveCount < m_runtime.particles.size())
{
Particle& p = m_runtime.particles[m_runtime.aliveCount++];
p.alive = 1;
p.age = 0.f;
p.lifetime = m_emitterAsset.IsReady() ? m_emitterAsset->m_lifetime : 1.f;
p.size = m_emitterAsset.IsReady() ? m_emitterAsset->m_startSize : 1.f;
// init pos/vel with some randomization
p.position = AZ::Vector3::CreateZero();
p.velocity = AZ::Vector3(0.f, 0.f, 5.f);
p.color = AZ::Colors::White;
}
}
}
- 资产类型与 Builder
cpp
// Assets/ParticleEmitterAsset.h
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Serialization/SerializeContext.h>
namespace ParticleSystemGem
{
class ParticleEmitterAsset final
: public AZ::Data::AssetData
{
public:
AZ_RTTI(ParticleEmitterAsset, "{5700C9CC-88C2-44A5-81F6-63D5B1FEA9F5}", AZ::Data::AssetData);
AZ_CLASS_ALLOCATOR(ParticleEmitterAsset, AZ::SystemAllocator);
static void Reflect(AZ::ReflectContext* context)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<ParticleEmitterAsset, AZ::Data::AssetData>()
->Version(1)
->Field("EmitRate", &ParticleEmitterAsset::m_emitRate)
->Field("Lifetime", &ParticleEmitterAsset::m_lifetime)
->Field("StartSize", &ParticleEmitterAsset::m_startSize);
}
}
float m_emitRate = 100.f;
float m_lifetime = 1.5f;
float m_startSize = 0.1f;
};
class ParticleEffectAsset final
: public AZ::Data::AssetData
{
public:
AZ_RTTI(ParticleEffectAsset, "{A6B5570A-3714-4AA6-8B24-54B6B18A1E05}", AZ::Data::AssetData);
AZ_CLASS_ALLOCATOR(ParticleEffectAsset, AZ::SystemAllocator);
static AZ::ComponentDescriptor* CreateDescriptor(); // for Module registration if needed
static void Reflect(AZ::ReflectContext* context) { /* similar */ }
};
}
- Asset Builder 简要骨架(注册到 Asset Processor)
cpp
// Assets/ParticleAssetBuilder.cpp
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
namespace ParticleSystemGem
{
class ParticleAssetBuilder : public AssetBuilderSDK::AssetBuilderCommandBus::Handler
{
public:
void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) override
{
// scan .particle.json and .emitter.json, set fingerprint, add one job per platform
}
void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) override
{
// read JSON -> serialize to runtime asset (.particle.asset)
// response.m_outputProducts.emplace_back(productPath, productTypeUuid, ...);
}
};
void RegisterBuilder()
{
AssetBuilderSDK::AssetBuilderDesc desc;
desc.m_name = "ParticleAssetBuilder";
desc.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.particle.json", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
desc.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.emitter.json", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
// Hook EBus
AssetBuilderSDK::AssetBuilderBus::QueueFunction([desc]() {
AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, desc);
});
}
}
- Updater 扩展点
cpp
// Updaters/IParticleUpdater.h
#pragma once
#include "ParticleTypes.h"
namespace ParticleSystemGem
{
struct UpdateContext
{
float dt;
// world params, wind, noise, etc.
};
class IParticleUpdater
{
public:
virtual ~IParticleUpdater() = default;
virtual void Update(EmitterRuntimeData& runtime, const UpdateContext& ctx) = 0;
};
}
cpp
// Updaters/GravityUpdater.h
#pragma once
#include "IParticleUpdater.h"
#include <AzCore/Math/Vector3.h>
namespace ParticleSystemGem
{
class GravityUpdater : public IParticleUpdater
{
public:
AZ::Vector3 m_gravity = AZ::Vector3(0,0,-9.8f);
void Update(EmitterRuntimeData& runtime, const UpdateContext& ctx) override
{
const size_t n = runtime.aliveCount;
for (size_t i=0;i<n;++i)
{
runtime.particles[i].velocity += m_gravity * ctx.dt;
}
}
};
}
- 渲染器骨架(Billboard)
cpp
// Rendering/IParticleRenderer.h
#pragma once
#include "ParticleTypes.h"
#include <RHI/Buffer.h> // simplified includes
namespace ParticleSystemGem
{
class IParticleRenderer
{
public:
virtual ~IParticleRenderer() = default;
virtual void Initialize() = 0;
virtual void Shutdown() = 0;
virtual void Submit(const EmitterRuntimeData& runtime) = 0; // fill instance buffer
};
}
cpp
// Rendering/BillboardRenderer.h
#pragma once
#include "IParticleRenderer.h"
namespace ParticleSystemGem
{
class BillboardRenderer final : public IParticleRenderer
{
public:
void Initialize() override;
void Shutdown() override;
void Submit(const EmitterRuntimeData& runtime) override;
private:
// Atom resources: buffer views, stream buffer, SRG, material, draw packet
// AZ::RPI::Ptr<AZ::RPI::Buffer> m_instanceBuffer;
};
}
- Shader 片段思路(Atom .azsl)
- 顶点:实例数据包含 position/size/color/rotation;相机对齐生成四边形;可支持软粒子(读取深度)。
- 像素:采样 texture/gradient;应用透明/加色/乘色混合。
五、编辑器与资产 UI
- EditorSystemComponent:注册自定义资产类型、属性编辑器、菜单项。
- EditorParticleEmitterComponent:在属性面板暴露发射率、寿命、大小、初速分布、随机种子、曲线(SizeOverLife/ColorOverLife)、渲染器选择、材质绑定。
- 粒子效果编辑器窗口:
- 左侧树形结构:Effect -> Emitters -> Modules
- 中间参数面板:模块参数与曲线编辑
- 右侧预览视口:可播放/暂停/重置、相机控制
- 底部时间轴:关键帧或曲线可视化
- 资产编译:保存为 .particle.json;Asset Processor 生成运行时 .asset。
六、详细开发步骤
- 创建 Gem
- 使用 o3de.exe create-gem 或手动复制骨架,填写 gem.json(依赖 Atom)。
- 在项目 project.json 启用该 Gem。
- 定义资产与 Builder
- 设计 JSON 架构:Effect -> Emitters -> Modules -> Renderer -> Materials。
- 实现反射与 AssetData 类;实现 AssetBuilder 注册、CreateJobs/ProcessJob。
- 运行时系统
- SystemComponent 提供 ParticleWorld,集中 Tick 或由 TickBus 分发。
- EmitterComponent 加载资产,初始化池化数组,Expose Start/Stop/Burst。
- Updaters 管线:根据资产配置生成更新序列,按帧运行。
- 渲染集成 Atom
- 为 Billboard/Mesh 粒子准备实例缓冲布局(position/size/color/rotation)。
- 创建 SRG、Material、DrawPacket;在每帧 Submit 时更新 Stream Buffer。
- 配置混合、深度写开关、排序关键字;支持软粒子深度淡化。
- 编辑器工具
- 资产编辑窗口:使用 Qt DockWidget;通过 AZ::SerializeContext 反射属性。
- 预览视口:创建独立 RPI::Scene 和 RPI::View 或复用 Editor 视口嵌入层。
- 触发与脚本
- 暴露 EBus:Start/Stop/Burst/SetParameter。
- Script Canvas 节点:发射一次、修改发射率、设置材质参数等。
- Lua:调用 EBus 控制特效。
- 碰撞与力场(可选)
- 接入 PhysX Scene 查询:Raycast/Sweep 粒子运动步长中检测。
- Volume Field Component:在区域内施加力(风场/噪声)。
- GPU 粒子(进阶)
- 设计 Compute Shader 更新粒子状态(SSBO/StructuredBuffer)。
- CPU 仅负责发射与少量管理,或全部转 GPU。
- Pass 模板:RPI Pass + SRG 绑定,Compute -> Draw。
七、性能优化与规格
- 数据布局:SoA 提升 Cache;必要时使用 AZSimd。
- 池化:避免频繁分配,最大粒子数静态配置或动态增长(上限)。
- 多线程:发射器并行 Update;渲染合批(同材质/同 Renderer)并行填充缓冲。
- 透明排序:基于发射器批次排序 + 近似排序;或使用 OIT(可选,成本高)。
- LOD:远距离降低发射率/更新频率/使用贴图替代。
- 质量阶(Quality Level):粒子上限、GPU/CPU 开关、软粒子开关。
- 资源规范:
- 纹理:推荐 256–1024 方形,压缩 BC 格式。
- 材质:尽量共享同一 Material/Shader,减少切换。
- 曲线:离线烘焙为小纹理或查表以减少 CPU 开销。
- 事件与调试:
- 可视化 Gizmo:发射形状、速度向量。
- 统计:活跃粒子数、填充带宽、发射器数、排序时间。
- 验证:越界检查、NaN 清理、资产版本迁移测试。
八、完整案例:火花喷射器
- 需求:受击时喷射 0.25 秒火花,重力下落、初速锥形分布、颜色从黄到红到透明,软粒子、加色混合。
- 资产配置:
- EmitRate: 0(只用 Burst)
- Lifetime: 0.6–1.2 随机
- StartSpeed: 8–16 m/s,锥半角 25°
- StartSize: 0.03–0.06 m
- ColorOverLife: Gradient [0: (1,0.95,0.5)] -> [0.5: (1,0.3,0.1)] -> [1: alpha=0]
- Renderer: Billboard, Additive, SoftParticles=On, Texture=spark.dds
- 脚本触发:
- OnHit -> EBus Burst(count=80)
- 代码片段(反映初始化随机):
cpp
// in Burst
p.lifetime = AZ::GetRandomFloat(0.6f, 1.2f);
p.size = AZ::GetRandomFloat(0.03f, 0.06f);
AZ::Vector3 dir = RandomConeDirection(forward, AZ::DegToRad(25.f));
float speed = AZ::GetRandomFloat(8.f, 16.f);
p.velocity = dir * speed;
九、编辑器 Script Canvas 节点示例接口
- Nodes:
- ParticleEmitter::Start(EntityId)
- ParticleEmitter::Stop(EntityId)
- ParticleEmitter::Burst(EntityId, int count)
- ParticleEmitter::SetRate(EntityId, float rate)
- 注册方式:基于 BehaviorContext 反射 EBus/Component 方法。
十、规格清单与要点汇总
- 运行时
- 支持 CPU 粒子、可选 GPU 路径
- 支持形状发射:点/球/圆盘/盒/网格表面
- Updaters:重力、阻尼、拖拽、噪声、曲线插值、颜色/尺寸随生命周期、碰撞/反弹、光照探针系数(可选)
- Renderer:Billboard、Stretched Billboard、Mesh、Trail
- 资源:材质/贴图/曲线/渐变;资产引用与依赖追踪
- 工具链
- AssetBuilder:.particle.json / .emitter.json -> .asset
- 编辑器窗口:所见即所得预览、热重载
- Script Canvas/Lua 集成
- 性能
- 批处理:同材质批
- GPU 计算:大规模粒子
- LOD/质量级别与软粒子开关
- 测试与验证
- 单元测试:Updater 逻辑、资产反射、Builder
- 性能基线:10k/50k/200k 粒子帧率
- 兼容性:PC/DX12、Vulkan;可扩展到移动需简化
十一、与 O3DE 内置系统的结合点
- Atom 渲染:使用 AZ::RPI::Scene、FeatureProcessor 或自定义 Pass。可实现 ParticleFeatureProcessor,注册到场景,在每帧收集发射器数据,统一渲染。
- 物理:PhysXGem 接入世界查询;对地面碰撞反弹。
- 资源系统:AssetCatalog 与 Hot-Reload。
- 组件依赖:TransformComponent 获取世界变换用作发射源。
十二、进阶:FeatureProcessor 轮廓
如果你希望完全融入 Atom 的渲染特性,建议实现 FeatureProcessor:
- 结构
- Rendering/ParticleFeatureProcessor.h/.cpp
- 在 SystemComponent Activate 时向 AZ::RPI::Scene::GetFeatureProcessorRegistry 注册。
- 职责
- 维护统一实例缓冲与 DrawPacket 池
- 提供 API:RegisterEmitterHandle/SubmitEmitterData
- 参与场景 Cull/Render Pipeline
示例句柄接口:
cpp
class EmitterHandle
{
public:
uint32_t m_id = InvalidId;
};
class ParticleFeatureProcessor : public AZ::RPI::FeatureProcessor
{
public:
EmitterHandle RegisterEmitter();
void RemoveEmitter(EmitterHandle);
void UpdateEmitter(EmitterHandle, const EmitterRuntimeData&);
// ...
};
结语
上述架构与代码骨架覆盖了从 Gem 插件、资产系统、运行时更新、渲染到编辑器工具的完整路径。你可以先实现 CPU Billboard 渲染与基础 Updaters,打通资产链与编辑器,再扩展到 GPU 粒子、碰撞、Trail、Mesh 粒子和高级材质效果。若你提供具体目标(例如“10 万粒子 @ 120 FPS”或“需要网格粒子+碰撞”),我可以给出更聚焦的渲染与数据结构优化方案以及更完整的 Atom 集成代码。
2万+

被折叠的 条评论
为什么被折叠?



