19#include <unordered_map>
26constexpr float kRad2Deg = 180.f / 3.14159265358979323846f;
28void sampleColor(
const ParticleEmitter::Config &cfg,
float t, Color &out) {
29 if (!cfg.colorGradient.empty()) {
31 cfg.colorGradient.sample(
t,
r,
g,
b,
a);
35 t =
t < 0.f ? 0.f : (
t > 1.f ? 1.f :
t);
36 out.r = cfg.colorStart.r + (cfg.colorEnd.r - cfg.colorStart.r) *
t;
37 out.g = cfg.colorStart.g + (cfg.colorEnd.g - cfg.colorStart.g) *
t;
38 out.b = cfg.colorStart.b + (cfg.colorEnd.b - cfg.colorStart.b) *
t;
39 out.a = cfg.colorStart.a + (cfg.colorEnd.a - cfg.colorStart.a) *
t;
42void sampleScale(
const ParticleEmitter::Config &cfg,
float t,
float &
scale) {
43 if (!cfg.sizeCurve.empty()) {
44 scale = cfg.sizeCurve.sample(
t, 1.f);
47 t =
t < 0.f ? 0.f : (
t > 1.f ? 1.f :
t);
48 scale = cfg.sizeStart + (cfg.sizeEnd - cfg.sizeStart) *
t;
51void flipbookUV(
const ParticleEmitter::Config &cfg,
float frame,
float &u0,
float &v0,
float &u1,
53 const int total = cfg.hframes * cfg.vframes;
54 if (total <= 1 || cfg.hframes <= 0 || cfg.vframes <= 0) {
61 int fi = int(std::floor(frame));
62 fi = ((fi % total) + total) % total;
63 const int col = fi % cfg.hframes;
64 const int row = fi / cfg.hframes;
65 const float iw = 1.f / float(cfg.hframes);
66 const float ih = 1.f / float(cfg.vframes);
73void appendParticleItem(
const ParticleEmitter::Config &cfg,
const ParticleEmitter::Draw &
draw,
74 const Particle &
p,
int order, std::vector<graphics::DrawItem2D> &out) {
75 const float t =
p.lifetime > 0.f ? 1.f - (
p.life /
p.lifetime) : 1.f;
77 sampleColor(cfg,
t,
c);
79 sampleScale(cfg,
t,
scale);
80 scale *=
p.size > 0.f ?
p.size : 1.f;
81 const float w = cfg.particleW *
scale;
82 const float h = cfg.particleH *
scale;
84 graphics::DrawItem2D item;
85 item.x =
p.x -
w * 0.5f;
86 item.y =
p.y -
h * 0.5f;
89 if (cfg.renderMode ==
"stretched") {
91 const float speed = std::sqrt(
p.vx *
p.vx +
p.vy *
p.vy);
92 const float len = std::max(
w, speed * cfg.stretchFactor);
94 item.rotation = std::atan2(
p.vy,
p.vx) * kRad2Deg;
95 }
else if (cfg.renderMode ==
"axis") {
96 item.rotation = cfg.renderAxisDegrees;
98 item.rotation =
p.rot * kRad2Deg;
99 if (!cfg.rotationCurve.empty()) item.rotation += cfg.rotationCurve.sample(
t, 0.f);
102 item.hasOrder =
true;
104 item.layer =
draw.layer;
105 item.blend =
draw.blend;
106 item.texture =
draw.texture;
107 item.normal = cfg.materialMode ==
"lit" ?
draw.normalTexture :
nullptr;
108 item.shader =
draw.shader;
109 item.canvas =
draw.canvas;
110 item.camera =
draw.camera;
111 item.receiveLight = cfg.materialMode ==
"lit";
112 item.litPath = item.receiveLight && item.texture !=
nullptr && item.normal !=
nullptr && item.shader ==
nullptr;
113 item.sceneColorDistortion = cfg.materialMode ==
"distortion";
114 item.distortionStrength = cfg.distortionStrength;
115 if (
draw.texture && (cfg.hframes > 1 || cfg.vframes > 1)) {
116 flipbookUV(cfg,
p.frame, item.u0, item.v0, item.u1, item.v1);
122void appendEmitterItems(
const ParticleEmitter::Config& cfg,
const ParticleEmitter::Sim& sim,
123 const ParticleEmitter::Draw&
draw,
int&
order, std::vector<graphics::DrawItem2D>& out) {
124 if (cfg.sortMode ==
"none") {
125 for (
int i = 0; i < sim.alive; ++i) appendParticleItem(cfg,
draw, sim.particles[std::size_t(i)],
order++, out);
129 std::vector<int>
indices(std::size_t(sim.alive));
130 for (
int i = 0; i < sim.alive; ++i)
indices[std::size_t(i)] = i;
131 auto age = [&](
int index) {
132 const Particle&
p = sim.particles[std::size_t(
index)];
133 return p.lifetime > 0.f ? 1.f -
p.life /
p.lifetime : 1.f;
135 if (cfg.sortMode ==
"oldest") {
136 std::stable_sort(
indices.begin(),
indices.end(), [&](
int a,
int b) { return age(a) > age(b); });
137 }
else if (cfg.sortMode ==
"youngest") {
138 std::stable_sort(
indices.begin(),
indices.end(), [&](
int a,
int b) { return age(a) < age(b); });
139 }
else if (cfg.sortMode ==
"distance" &&
draw.camera) {
140 const float cameraX =
draw.camera->data()->x;
141 const float cameraY =
draw.camera->data()->y;
143 const Particle&
p = sim.particles[std::size_t(
index)];
144 const float dx =
p.x - cameraX;
145 const float dy =
p.y - cameraY;
149 [&](
int a,
int b) { return distanceSquared(a) > distanceSquared(b); });
154int appendRibbonItems(
const ParticleEmitter::Config& cfg,
const ParticleEmitter::Sim& sim,
155 const ParticleEmitter::Draw&
draw,
int&
order, std::vector<graphics::DrawItem2D>& out) {
157 for (
int i = 1; i < sim.alive; ++i) {
158 const Particle&
previous = sim.particles[std::size_t(i - 1)];
159 const Particle&
current = sim.particles[std::size_t(i)];
163 if (
length < cfg.ribbonMinSegmentLength)
continue;
166 auto& item = out.back();
167 const float h = item.h * cfg.ribbonWidth;
172 item.rotation = std::atan2(
dy,
dx) * kRad2Deg;
179bool emitterOffscreen(
const ParticleEmitter::Config &cfg,
const ParticleEmitter::Draw &
draw) {
180 auto *cam =
draw.camera;
181 if (!cam)
return false;
182 if (cfg.cullDistance > 0.f) {
183 const float dx = cfg.x - cam->data()->x;
184 const float dy = cfg.y - cam->data()->y;
185 if (
dx *
dx +
dy *
dy > cfg.cullDistance * cfg.cullDistance)
return true;
187 auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>(
"Graphics");
188 if (!gfx)
return false;
189 const float viewW =
draw.canvas ? float(
draw.canvas->getWidth()) : float(gfx->getWidth());
190 const float viewH =
draw.canvas ? float(
draw.canvas->getHeight()) : float(gfx->getHeight());
191 if (viewW <= 0.f || viewH <= 0.f)
return false;
192 const float z = cam->data()->zoom > 0.f ? cam->data()->zoom : 1e-4f;
193 const float sx = (cfg.x - cam->data()->x) *
z + viewW * 0.5f;
194 const float sy = (cfg.y - cam->data()->y) *
z + viewH * 0.5f;
195 const float maxHalf =
196 std::max(cfg.particleW, cfg.particleH) *
197 std::max(std::abs(cfg.sizeStart), std::abs(cfg.sizeEnd)) * 0.5f +
200 cfg.speedMax * cfg.lifeMax + std::max(cfg.areaX, cfg.areaY) + maxHalf;
204int64_t fileModtime(
const std::string &
path) {
205 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>(
"Filesystem");
206 if (!fs) fs = eve::filesystem::Filesystem::create();
208 if (!fs->getInfo(
path,
info))
return -1;
212int liveParticleCount(ParticleEmitter*
emitter) {
214 return gpu->residentActive ? gpu->estimatedAlive +
emitter->sim()->alive :
emitter->sim()->alive;
217graphics::GpuParticleSpawn gpuSpawn(
const Particle& particle, std::uint32_t birthSerial) {
218 graphics::GpuParticleSpawn out;
221 out.vx = particle.vx;
222 out.vy = particle.vy;
223 out.life = particle.life;
224 out.lifetime = particle.lifetime;
225 out.size = particle.size;
226 out.rotation = particle.rot;
227 out.spin = particle.spin;
228 out.frame = particle.frame;
229 out.radial = particle.radial;
230 out.tangential = particle.tangential;
231 out.ax = particle.ax;
232 out.ay = particle.ay;
233 out.noisePhase = particle.noisePhase;
234 out.birthSerial = std::bit_cast<float>(birthSerial);
238void deactivateResidentGpu(graphics::Graphics* gfx, ParticleEmitter::GpuSim& gpu) {
240 gfx->releaseGpuParticleEmitter(gpu.residentHandle);
242 gpu.residentActive =
false;
243 gpu.estimatedAlive = 0;
245 gpu.deathTimes.clear();
246 gpu.pendingWorldOffsetX = 0.f;
247 gpu.pendingWorldOffsetY = 0.f;
248 gpu.nextBirthSerial = 1;
252 bool legacyControls) {
259 gpu->residentHandle = gfx->createGpuParticleEmitter(std::uint32_t(sim->particles.size()));
263 const bool hadLastPosition = sim->hasLastPos;
264 const float previousX = sim->lastX;
265 const float previousY = sim->lastY;
266 const int capacity = int(sim->particles.size());
267 const int conservativeRoom = std::max(0,
capacity - gpu->estimatedAlive - sim->alive);
268 if (sim->spawnQuota < 0)
269 sim->spawnQuota = conservativeRoom;
271 sim->spawnQuota = std::min(sim->spawnQuota, conservativeRoom);
273 float simulatedDt = 0.f;
274 if (legacyControls) {
279 simulatedDt =
static_cast<float>(
step.delta.seconds());
281 gpu->timeline += simulatedDt;
282 auto minHeap = std::greater<float>{};
283 while (!gpu->deathTimes.empty() && gpu->deathTimes.front() <= float(gpu->timeline) + 1e-6f) {
284 std::pop_heap(gpu->deathTimes.begin(), gpu->deathTimes.end(), minHeap);
285 gpu->deathTimes.pop_back();
288 const int room = std::max(0,
capacity -
int(gpu->deathTimes.size()));
289 const int accepted = std::min(sim->alive, room);
290 if (sim->alive > accepted) sim->droppedSpawnsThisFrame += sim->alive - accepted;
291 std::vector<graphics::GpuParticleSpawn> spawns;
292 spawns.reserve(std::size_t(accepted));
293 for (
int i = 0; i < accepted; ++i) {
294 const Particle& particle = sim->particles[std::size_t(i)];
295 if (particle.life <= 0.f)
continue;
296 spawns.push_back(gpuSpawn(particle, gpu->nextBirthSerial++));
297 gpu->deathTimes.push_back(
float(gpu->timeline) + particle.life);
298 std::push_heap(gpu->deathTimes.begin(), gpu->deathTimes.end(), minHeap);
301 graphics::GpuParticleUpdate update;
302 update.dt = simulatedDt;
303 update.emitterX = cfg->x;
304 update.emitterY = cfg->y;
305 update.gravityX = cfg->gravityX;
306 update.gravityY = cfg->gravityY;
307 update.damping = cfg->damping;
308 update.velocityLimit = cfg->limitVelocity;
309 update.noiseStrength = cfg->noiseStrength;
310 update.noiseFrequency = cfg->noiseFrequency;
311 update.noiseSpeed = cfg->noiseSpeed;
312 update.time = sim->emitterAge;
313 update.frameRate = cfg->frameRate;
314 if (cfg->simSpace ==
"local" && hadLastPosition) {
315 update.localOffsetX = cfg->x - previousX;
316 update.localOffsetY = cfg->y - previousY;
318 update.localOffsetX += gpu->pendingWorldOffsetX;
319 update.localOffsetY += gpu->pendingWorldOffsetY;
321 if (!gfx->updateGpuParticleEmitter(gpu->residentHandle, update, spawns.data(), std::uint32_t(spawns.size()))) {
325 if (!gpu->residentActive) deactivateResidentGpu(gfx, *gpu);
330 for (
int i = 0; i < sim->alive; ++i) sim->particles[std::size_t(i)].life = 0.f;
332 gpu->residentActive =
true;
333 gpu->estimatedAlive = int(gpu->deathTimes.size());
334 gpu->pendingWorldOffsetX = 0.f;
335 gpu->pendingWorldOffsetY = 0.f;
342 if (
step.delta.nanoseconds() < 0)
344 "particle simulation delta must be non-negative"));
345 const float dt =
static_cast<float>(
step.delta.seconds());
346 if (!std::isfinite(dt))
348 "particle simulation delta is outside float range"));
358 auto &
stats = nextStats;
359 const auto begin = std::chrono::steady_clock::now();
361 if (ecs::current()->getManager<ParticleEmitter>() ==
nullptr) {
366 std::vector<ParticleEmitter *> allEmitters;
367 std::vector<ParticleEmitter *> emitters;
369 auto view = ecs::View<ParticleEmitter, ParticleEmitter::Config>();
370 for (
auto it =
view.begin(); it !=
view.end(); ++it) {
372 if (!cfg->entity)
continue;
373 allEmitters.push_back(cfg->entity);
374 auto sim = cfg->entity->sim();
375 if (!legacyControls && sim->hasSimulationTick &&
step.tick <= sim->simulationTick)
378 "particle simulation tick must advance monotonically for every emitter"));
379 if (sim->active || liveParticleCount(cfg->entity) > 0) emitters.push_back(cfg->entity);
383 return a->config()->priority > b->config()->priority;
387 stats.emittersTotal = int(emitters.size());
388 for (
auto *
emitter : allEmitters) {
391 sim->spawnedThisFrame = 0;
392 sim->droppedSpawnsThisFrame = 0;
393 sim->spawnQuota = budget.maxParticles > 0 ? 0 : -1;
395 int reservedAlive = 0;
396 for (
auto*
emitter : emitters) reservedAlive += liveParticleCount(
emitter);
398 auto* gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>(
"Graphics");
400 int processedAlive = 0;
401 for (
auto *
emitter : emitters) {
405 const int aliveBefore = liveParticleCount(
emitter);
406 reservedAlive -= aliveBefore;
408 if (budget.qualityLevel < cfg->minimumQuality) {
409 ++
stats.emittersQualitySkipped;
410 processedAlive += aliveBefore;
414 const bool offscreen = emitterOffscreen(*cfg, *
draw);
415 const bool culled = cfg->cullingMode ==
"pause" ? offscreen
416 : cfg->cullingMode ==
"always" ? false
417 : (aliveBefore <= 0 && offscreen);
419 ++
stats.emittersCulled;
420 processedAlive += aliveBefore;
424 if (budget.maxSimulatedEmitters > 0 &&
425 stats.emittersSimulated >= budget.maxSimulatedEmitters) {
426 ++
stats.emittersBudgetSkipped;
427 processedAlive += aliveBefore;
431 int spawnQuota = cfg->maxSpawnPerFrame > 0 ? cfg->maxSpawnPerFrame : -1;
432 if (budget.maxParticles > 0) {
433 const int existingTotal = processedAlive + aliveBefore + reservedAlive;
434 const int globalRoom = std::max(0, budget.maxParticles - existingTotal);
435 spawnQuota = spawnQuota < 0 ? globalRoom : std::min(spawnQuota, globalRoom);
437 sim->spawnQuota = spawnQuota;
439 auto attach =
emitter->attach();
440 auto skinSrc =
emitter->skinSource();
441 auto gpuSim =
emitter->gpuSim();
443 const bool gpuEligible =
emitter->isGpuFeatureSetSupported();
444 if (gpuSim->residentActive && (!cfg->gpuSimulation || !gpuEligible)) deactivateResidentGpu(gfx, *gpuSim);
445 const bool wantsResident = cfg->gpuSimulation && gpuEligible;
446 bool handled =
false;
448 auto gpuAdvance = advanceResidentGpu(gfx,
emitter,
step, legacyControls);
450 handled = std::move(gpuAdvance).takeValue();
460 ++
stats.emittersSimulated;
461 processedAlive += liveParticleCount(
emitter);
462 if (budget.maxParticles > 0 || cfg->maxSpawnPerFrame > 0) sim->spawnQuota = 0;
465 stats.particlesAfter = 0;
466 stats.particlesSpawned = 0;
467 stats.droppedSpawns = 0;
468 for (
auto *
emitter : allEmitters) {
472 stats.particlesSpawned += sim->spawnedThisFrame;
473 stats.droppedSpawns += sim->droppedSpawnsThisFrame;
474 if (gpu->residentActive) {
475 ++
stats.gpuResidentEmitters;
476 stats.gpuResidentParticles += gpu->estimatedAlive;
478 sim->spawnQuota = -1;
480 stats.particlesKilled =
481 std::max(0,
stats.particlesBefore +
stats.particlesSpawned -
stats.particlesAfter);
483 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
begin)
486 if (!legacyControls) {
487 for (
auto *
emitter : allEmitters) {
489 sim->simulationTick =
step.tick;
490 sim->hasSimulationTick =
true;
504 duration.ignore(
"legacy particle update received an invalid duration");
508 .ignore(
"legacy particle update facade");
513 std::vector<graphics::DrawItem2D>& items,
int&
order,
514 bool respectSceneCulling,
bool& usedCanvas,
bool& culled) {
517 if (!
draw->visible ||
alive <= 0)
return 0;
519 if (respectSceneCulling &&
520 (budget.qualityLevel < cfg->minimumQuality || emitterOffscreen(*cfg, *
draw))) {
521 culled =
true;
return 0;
523 if (gpu->residentActive) {
553 culled =
true;
return 0;
555 if (
draw->canvas) usedCanvas =
true;
556 if (cfg->renderMode ==
"ribbon")
return appendRibbonItems(*cfg, *sim, *
draw,
order, items);
557 appendEmitterItems(*cfg, *sim, *
draw,
order, items);
return sim->alive;
562 if (!gfx || !
emitter)
return 0;
563 std::vector<graphics::DrawItem2D> items;
bool usedCanvas=
false, culled=
false;
565 const int rendered=appendOneEmitter(*gfx,*
emitter,items,
order,
false,usedCanvas,culled);
573 stats.renderedParticles = 0;
574 stats.renderCulledEmitters = 0;
575 stats.renderMs = 0.0;
576 const auto begin = std::chrono::steady_clock::now();
578 if (ecs::current()->getManager<ParticleEmitter>() ==
nullptr)
return;
580 std::vector<graphics::DrawItem2D> items;
583 bool anyCanvas =
false;
585 for (
auto it =
view.begin(); it !=
view.end(); ++it) {
586 auto [cfg, sim,
draw] = *it; (void)sim; (void)
draw;
590 stats.renderedParticles += appendOneEmitter(*gfx,*
emitter,items,
order,
true,anyCanvas,culled);
591 if(culled) ++
stats.renderCulledEmitters;
599 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
begin)
604 if (ecs::current()->getManager<ParticleEmitter>() ==
nullptr)
return;
608 std::vector<ParticleEmitter *> emitters;
610 auto view = ecs::View<ParticleEmitter, ParticleEmitter::Config>();
611 for (
auto it =
view.begin(); it !=
view.end(); ++it) {
613 if (cfg->entity) emitters.push_back(cfg->entity);
618 for (
auto *em : emitters) {
619 auto cfg = em->config();
620 auto sim = em->sim();
621 auto draw = em->draw();
622 auto lights = em->lights();
623 if (!cfg->lights.enabled) {
624 for (
auto *l :
lights->pool)
625 if (l) l->setEnabled(
false);
628 const int maxL = cfg->lights.max > 0 ? (cfg->lights.max > 8 ? 8 : cfg->lights.max) : 0;
629 while (
int(
lights->pool.size()) < maxL)
631 const int n = sim->alive < maxL ? sim->alive : maxL;
632 for (
int i = 0; i < maxL; ++i) {
635 const Particle &
p = sim->particles[size_t(i)];
638 l->
setColor(cfg->lights.r, cfg->lights.g, cfg->lights.b, cfg->lights.intensity);
649 if (ecs::current()->getManager<ParticleEmitter>() ==
nullptr)
return 0;
656 ecs::View<ParticleEmitter, ParticleEmitter::Config, ParticleEmitter::Resource>();
657 for (
auto it =
view.begin(); it !=
view.end(); ++it) {
658 auto [cfg, res] = *it;
659 if (!cfg->entity)
continue;
660 if (res->path.empty()) {
664 if (!res->autoReload) {
669 const int64_t mt = fileModtime(res->path);
674 if (mt == res->modtime) {
std::vector< eve::ProcgenProbeDesc > lights
std::vector< std::uint32_t > indices
std::array< float, 3 > scale
graphics::Canvas * previous
std::unique_ptr< TrailEmitter > emitter
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
virtual Texture * getSceneLinearDepthTexture()
Sampleable scene linear depth in [0,1], or nullptr when unavailable.
int getWidth() const
Returns the width.
virtual bool drawGpuParticleEmitter(GpuParticleHandle handle, const GpuParticleDraw &draw)
Queue an indirect draw at the current 2D overlay position.
virtual void setCanvas(Canvas *canvas)=0
nullptr or this → screen. Switching flushes pending draws to the previous target.
int getHeight() const
Returns the height.
Declarative 2D light. Collected by RenderSystem (max 8 per canvas/frame). type: "point" | "dir" | "sp...
void setPosition(float x, float y)
void setColor(float r, float g, float b, float intensity=1.f)
void setCanvas(Canvas *canvas)
void setEnabled(bool enabled)
static Light2D * createLight(const std::string &type="point")
void setRadius(float radius)
static void drawItems(Graphics &gfx, std::vector< DrawItem2D > &items, bool present)
Sort and draw items. If present=true, calls gfx.present() at the end. Map::render uses present=false ...
ECS emitter entity. Script configures components; ParticleSimSystem / ParticleRenderSystem drive per-...
static void update()
Updates .
static void render(graphics::Graphics *gfx)
Renders .
static int renderEmitter(graphics::Graphics *gfx, ParticleEmitter *emitter)
Draw exactly one emitter without traversing the global ECS registry.
static eve::Result< void > advance(const eve::SimulationStep &step)
Advances emitters using one scheduler-owned deterministic step.
static void update(float dt)
Legacy seconds facade; conversion and Result consumption are explicit.
constexpr GpuParticleHandle kInvalidGpuParticleHandle
Invalid GPU particle handle returned when the backend cannot allocate the resource.
@ Ribbon
Connect birth-ordered neighbors into ribbon segments.
ParticleBudgetConfig & particleBudgetConfig()
Mutable process-wide scalability configuration.
EVENGINE_API_DOMAINS float advanceEmitterSim(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, float dt)
Apply playback speed and optional bounded fixed stepping before simulation.
bool reloadConfigFile(ParticleEmitter *emitter, std::string *error)
Re-read Resource.path if set; updates modtime.
eve::Result< void > runParticleSimulation(const eve::SimulationStep &step, bool legacyControls)
const ParticleFrameStats & particleFrameStats()
Statistics for the most recently processed particle frame.
ParticleFrameStats & mutableParticleFrameStats()
Internal mutable statistics used by particle systems.
void syncEmitterSources(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &, ParticleEmitter::Attach &attach, ParticleEmitter::SkinSource &skinSrc)
Sync bone attach + refresh skin cache; call before stepEmitterSim when using Attach/SkinSource.
float distanceSquared(float ax, float ay, float bx, float by)
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
glm::vec4 Color
Render-neutral RGBA color shared by graphics-facing modules.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
One deterministic fixed-step emitted by SimulationClock.
Parameters for rendering a resident GPU particle emitter.
GpuParticleSortMode sortMode
GpuParticleFacingMode facing
float ribbonMinSegmentLength
float axisRotationRadians
@ MtimePollingReloadFailed
Counters from the most recent particle update and render pass.
eve::SimulationTick simulationTick
Simulation tick that produced the latest simulation counters.
Single live particle (CPU simulation).