载入中...
搜索中...
未找到
ParticleSystem.cpp
浏览该文件的文档.
6#include "graphics/Graphics.h"
7#include "graphics/Light.h"
9#include "graphics/Canvas.h"
11#include "common/Module.h"
12
13#include <algorithm>
14#include <bit>
15#include <chrono>
16#include <cmath>
17#include <functional>
18#include <string>
19#include <unordered_map>
20#include <vector>
21
22namespace eve::particles {
23
24namespace {
25
26constexpr float kRad2Deg = 180.f / 3.14159265358979323846f;
27
28void sampleColor(const ParticleEmitter::Config &cfg, float t, Color &out) {
29 if (!cfg.colorGradient.empty()) {
30 float r, g, b, a;
31 cfg.colorGradient.sample(t, r, g, b, a);
32 out = Color(r, g, b, a);
33 return;
34 }
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;
40}
41
42void sampleScale(const ParticleEmitter::Config &cfg, float t, float &scale) {
43 if (!cfg.sizeCurve.empty()) {
44 scale = cfg.sizeCurve.sample(t, 1.f);
45 return;
46 }
47 t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
48 scale = cfg.sizeStart + (cfg.sizeEnd - cfg.sizeStart) * t;
49}
50
51void flipbookUV(const ParticleEmitter::Config &cfg, float frame, float &u0, float &v0, float &u1,
52 float &v1) {
53 const int total = cfg.hframes * cfg.vframes;
54 if (total <= 1 || cfg.hframes <= 0 || cfg.vframes <= 0) {
55 u0 = 0.f;
56 v0 = 0.f;
57 u1 = 1.f;
58 v1 = 1.f;
59 return;
60 }
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);
67 u0 = float(col) * iw;
68 v0 = float(row) * ih;
69 u1 = u0 + iw;
70 v1 = v0 + ih;
71}
72
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;
76 Color c;
77 sampleColor(cfg, t, c);
78 float scale;
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;
83
84 graphics::DrawItem2D item;
85 item.x = p.x - w * 0.5f;
86 item.y = p.y - h * 0.5f;
87 item.w = w;
88 item.h = h;
89 if (cfg.renderMode == "stretched") {
90 // Elongate along the velocity direction (comet / streak style).
91 const float speed = std::sqrt(p.vx * p.vx + p.vy * p.vy);
92 const float len = std::max(w, speed * cfg.stretchFactor);
93 item.w = len;
94 item.rotation = std::atan2(p.vy, p.vx) * kRad2Deg;
95 } else if (cfg.renderMode == "axis") {
96 item.rotation = cfg.renderAxisDegrees;
97 } else {
98 item.rotation = p.rot * kRad2Deg;
99 if (!cfg.rotationCurve.empty()) item.rotation += cfg.rotationCurve.sample(t, 0.f);
100 }
101 item.order = order;
102 item.hasOrder = true;
103 item.color = c;
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);
117 item.hasUV = true;
118 }
119 out.push_back(item);
120}
121
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);
126 return;
127 }
128
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;
134 };
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;
142 auto distanceSquared = [&](int index) {
143 const Particle& p = sim.particles[std::size_t(index)];
144 const float dx = p.x - cameraX;
145 const float dy = p.y - cameraY;
146 return dx * dx + dy * dy;
147 };
148 std::stable_sort(indices.begin(), indices.end(),
149 [&](int a, int b) { return distanceSquared(a) > distanceSquared(b); });
150 }
151 for (int index : indices) appendParticleItem(cfg, draw, sim.particles[std::size_t(index)], order++, out);
152}
153
154int appendRibbonItems(const ParticleEmitter::Config& cfg, const ParticleEmitter::Sim& sim,
155 const ParticleEmitter::Draw& draw, int& order, std::vector<graphics::DrawItem2D>& out) {
156 int segments = 0;
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)];
160 const float dx = current.x - previous.x;
161 const float dy = current.y - previous.y;
162 const float length = std::sqrt(dx * dx + dy * dy);
163 if (length < cfg.ribbonMinSegmentLength) continue;
164
165 appendParticleItem(cfg, draw, current, order++, out);
166 auto& item = out.back();
167 const float h = item.h * cfg.ribbonWidth;
168 item.x = (previous.x + current.x - length) * 0.5f;
169 item.y = (previous.y + current.y - h) * 0.5f;
170 item.w = length;
171 item.h = h;
172 item.rotation = std::atan2(dy, dx) * kRad2Deg;
173 ++segments;
174 }
175 return segments;
176}
177
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;
186 }
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 +
198 1.f;
199 const float margin =
200 cfg.speedMax * cfg.lifeMax + std::max(cfg.areaX, cfg.areaY) + maxHalf;
201 return sx < -margin || sx > viewW + margin || sy < -margin || sy > viewH + margin;
202}
203
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;
209 return info.modtime;
210}
211
212int liveParticleCount(ParticleEmitter* emitter) {
213 auto gpu = emitter->gpuSim();
214 return gpu->residentActive ? gpu->estimatedAlive + emitter->sim()->alive : emitter->sim()->alive;
215}
216
217graphics::GpuParticleSpawn gpuSpawn(const Particle& particle, std::uint32_t birthSerial) {
218 graphics::GpuParticleSpawn out;
219 out.x = particle.x;
220 out.y = particle.y;
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);
235 return out;
236}
237
238void deactivateResidentGpu(graphics::Graphics* gfx, ParticleEmitter::GpuSim& gpu) {
239 if (gfx && gpu.residentHandle != graphics::kInvalidGpuParticleHandle)
240 gfx->releaseGpuParticleEmitter(gpu.residentHandle);
241 gpu.residentHandle = graphics::kInvalidGpuParticleHandle;
242 gpu.residentActive = false;
243 gpu.estimatedAlive = 0;
244 gpu.timeline = 0.0;
245 gpu.deathTimes.clear();
246 gpu.pendingWorldOffsetX = 0.f;
247 gpu.pendingWorldOffsetY = 0.f;
248 gpu.nextBirthSerial = 1;
249}
250
251eve::Result<bool> advanceResidentGpu(graphics::Graphics *gfx, ParticleEmitter *emitter, const eve::SimulationStep &step,
252 bool legacyControls) {
253 auto cfg = emitter->config();
254 auto sim = emitter->sim();
255 auto gpu = emitter->gpuSim();
256 if (!gfx || !gfx->canSubmitGpuParticles()) return eve::Result<bool>::success(false);
257
258 if (gpu->residentHandle == graphics::kInvalidGpuParticleHandle) {
259 gpu->residentHandle = gfx->createGpuParticleEmitter(std::uint32_t(sim->particles.size()));
260 if (gpu->residentHandle == graphics::kInvalidGpuParticleHandle) return eve::Result<bool>::success(false);
261 }
262
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;
270 else
271 sim->spawnQuota = std::min(sim->spawnQuota, conservativeRoom);
272
273 float simulatedDt = 0.f;
274 if (legacyControls) {
275 simulatedDt = advanceEmitterSim(*cfg, *sim, static_cast<float>(step.delta.seconds()));
276 } else {
277 auto simulation = advanceEmitterSim(*cfg, *sim, step);
278 if (!simulation) return eve::Result<bool>::failure(simulation.status());
279 simulatedDt = static_cast<float>(step.delta.seconds());
280 }
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();
286 }
287
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);
299 }
300
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;
317 }
318 update.localOffsetX += gpu->pendingWorldOffsetX;
319 update.localOffsetY += gpu->pendingWorldOffsetY;
320
321 if (!gfx->updateGpuParticleEmitter(gpu->residentHandle, update, spawns.data(), std::uint32_t(spawns.size()))) {
322 // Before activation the CPU state remains authoritative and already
323 // advanced. Once resident, a transient submit failure freezes state
324 // rather than losing it through an implicit CPU migration.
325 if (!gpu->residentActive) deactivateResidentGpu(gfx, *gpu);
326 gpu->failed = true;
327 return eve::Result<bool>::success(true);
328 }
329
330 for (int i = 0; i < sim->alive; ++i) sim->particles[std::size_t(i)].life = 0.f;
331 sim->alive = 0;
332 gpu->residentActive = true;
333 gpu->estimatedAlive = int(gpu->deathTimes.size());
334 gpu->pendingWorldOffsetX = 0.f;
335 gpu->pendingWorldOffsetY = 0.f;
336 return eve::Result<bool>::success(true);
337}
338
339} // namespace
340
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"));
349
350 // Build the complete frame record off to the side. Checked failures must
351 // not publish counters that describe only the prefix of a frame; the
352 // previous published record remains the observable snapshot until the
353 // simulation reaches its commit point below.
354 ParticleFrameStats nextStats;
355 const uint64_t nextFrame = particleFrameStats().frameIndex + 1;
356 nextStats.frameIndex = nextFrame;
357 nextStats.simulationTick = step.tick;
358 auto &stats = nextStats;
359 const auto begin = std::chrono::steady_clock::now();
360
361 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) {
362 mutableParticleFrameStats() = nextStats;
364 }
365
366 std::vector<ParticleEmitter *> allEmitters;
367 std::vector<ParticleEmitter *> emitters;
368 {
369 auto view = ecs::View<ParticleEmitter, ParticleEmitter::Config>();
370 for (auto it = view.begin(); it != view.end(); ++it) {
371 auto [cfg] = *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);
380 }
381 }
382 std::stable_sort(emitters.begin(), emitters.end(), [](ParticleEmitter *a, ParticleEmitter *b) {
383 return a->config()->priority > b->config()->priority;
384 });
385
386 const auto &budget = particleBudgetConfig();
387 stats.emittersTotal = int(emitters.size());
388 for (auto *emitter : allEmitters) {
389 auto sim = emitter->sim();
390 stats.particlesBefore += liveParticleCount(emitter);
391 sim->spawnedThisFrame = 0;
392 sim->droppedSpawnsThisFrame = 0;
393 sim->spawnQuota = budget.maxParticles > 0 ? 0 : -1;
394 }
395 int reservedAlive = 0;
396 for (auto* emitter : emitters) reservedAlive += liveParticleCount(emitter);
397
398 auto* gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
399
400 int processedAlive = 0;
401 for (auto *emitter : emitters) {
402 auto cfg = emitter->config();
403 auto sim = emitter->sim();
404 auto draw = emitter->draw();
405 const int aliveBefore = liveParticleCount(emitter);
406 reservedAlive -= aliveBefore;
407
408 if (budget.qualityLevel < cfg->minimumQuality) {
409 ++stats.emittersQualitySkipped;
410 processedAlive += aliveBefore;
411 continue;
412 }
413
414 const bool offscreen = emitterOffscreen(*cfg, *draw);
415 const bool culled = cfg->cullingMode == "pause" ? offscreen
416 : cfg->cullingMode == "always" ? false
417 : (aliveBefore <= 0 && offscreen);
418 if (culled) {
419 ++stats.emittersCulled;
420 processedAlive += aliveBefore;
421 continue;
422 }
423
424 if (budget.maxSimulatedEmitters > 0 &&
425 stats.emittersSimulated >= budget.maxSimulatedEmitters) {
426 ++stats.emittersBudgetSkipped;
427 processedAlive += aliveBefore;
428 continue;
429 }
430
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);
436 }
437 sim->spawnQuota = spawnQuota;
438
439 auto attach = emitter->attach();
440 auto skinSrc = emitter->skinSource();
441 auto gpuSim = emitter->gpuSim();
442 syncEmitterSources(*cfg, *sim, *attach, *skinSrc);
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;
447 if (wantsResident) {
448 auto gpuAdvance = advanceResidentGpu(gfx, emitter, step, legacyControls);
449 if (!gpuAdvance) return eve::Result<void>::failure(gpuAdvance.status());
450 handled = std::move(gpuAdvance).takeValue();
451 }
452 if (!handled) {
453 if (legacyControls)
454 advanceEmitterSim(*cfg, *sim, dt);
455 else {
456 auto cpuAdvance = advanceEmitterSim(*cfg, *sim, step);
457 if (!cpuAdvance) return eve::Result<void>::failure(cpuAdvance.status());
458 }
459 }
460 ++stats.emittersSimulated;
461 processedAlive += liveParticleCount(emitter);
462 if (budget.maxParticles > 0 || cfg->maxSpawnPerFrame > 0) sim->spawnQuota = 0;
463 }
464
465 stats.particlesAfter = 0;
466 stats.particlesSpawned = 0;
467 stats.droppedSpawns = 0;
468 for (auto *emitter : allEmitters) {
469 auto sim = emitter->sim();
470 auto gpu = emitter->gpuSim();
471 stats.particlesAfter += liveParticleCount(emitter);
472 stats.particlesSpawned += sim->spawnedThisFrame;
473 stats.droppedSpawns += sim->droppedSpawnsThisFrame;
474 if (gpu->residentActive) {
475 ++stats.gpuResidentEmitters;
476 stats.gpuResidentParticles += gpu->estimatedAlive;
477 }
478 sim->spawnQuota = -1;
479 }
480 stats.particlesKilled =
481 std::max(0, stats.particlesBefore + stats.particlesSpawned - stats.particlesAfter);
482 stats.simulationMs =
483 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - begin)
484 .count();
485
486 if (!legacyControls) {
487 for (auto *emitter : allEmitters) {
488 auto sim = emitter->sim();
489 sim->simulationTick = step.tick;
490 sim->hasSimulationTick = true;
491 }
492 }
493 mutableParticleFrameStats() = nextStats;
495}
496
500
503 if (!duration) {
504 duration.ignore("legacy particle update received an invalid duration");
505 return;
506 }
507 runParticleSimulation({eve::SimulationTick(1), std::move(duration).takeValue()}, true)
508 .ignore("legacy particle update facade");
509}
510
511namespace {
512int appendOneEmitter(graphics::Graphics& gfx, ParticleEmitter& emitter,
513 std::vector<graphics::DrawItem2D>& items, int& order,
514 bool respectSceneCulling, bool& usedCanvas, bool& culled) {
515 auto cfg = emitter.config(); auto sim = emitter.sim(); auto draw = emitter.draw();
516 auto gpu = emitter.gpuSim(); const int alive = liveParticleCount(&emitter);
517 if (!draw->visible || alive <= 0) return 0;
518 const auto& budget = particleBudgetConfig();
519 if (respectSceneCulling &&
520 (budget.qualityLevel < cfg->minimumQuality || emitterOffscreen(*cfg, *draw))) {
521 culled = true; return 0;
522 }
523 if (gpu->residentActive) {
525 gpuDraw.texture = draw->texture; gpuDraw.sceneDepth = gfx.getSceneLinearDepthTexture();
526 gpuDraw.blend = draw->blend; gpuDraw.viewportWidth = float(gfx.getWidth());
527 gpuDraw.viewportHeight = float(gfx.getHeight());
528 if (draw->camera) { gpuDraw.cameraEnabled = true; gpuDraw.cameraX = draw->camera->data()->x;
529 gpuDraw.cameraY = draw->camera->data()->y; gpuDraw.cameraZoom = draw->camera->data()->zoom; }
530 gpuDraw.particleWidth = cfg->particleW; gpuDraw.particleHeight = cfg->particleH;
531 gpuDraw.sizeStart = cfg->sizeStart; gpuDraw.sizeEnd = cfg->sizeEnd;
532 gpuDraw.stretchFactor = cfg->stretchFactor;
533 gpuDraw.facing = cfg->renderMode == "stretched" ? graphics::GpuParticleFacingMode::Velocity
534 : cfg->renderMode == "axis" ? graphics::GpuParticleFacingMode::Axis
535 : cfg->renderMode == "ribbon" ? graphics::GpuParticleFacingMode::Ribbon
537 gpuDraw.axisRotationRadians = cfg->renderAxisDegrees / kRad2Deg;
538 gpuDraw.ribbonWidth = cfg->ribbonWidth;
539 gpuDraw.ribbonMinSegmentLength = cfg->ribbonMinSegmentLength;
540 gpuDraw.softParticles = cfg->softParticles; gpuDraw.particleDepth = cfg->softParticleDepth;
541 gpuDraw.softFadeDistance = cfg->softFadeDistance;
542 gpuDraw.colorStart[0]=cfg->colorStart.r; gpuDraw.colorStart[1]=cfg->colorStart.g;
543 gpuDraw.colorStart[2]=cfg->colorStart.b; gpuDraw.colorStart[3]=cfg->colorStart.a;
544 gpuDraw.colorEnd[0]=cfg->colorEnd.r; gpuDraw.colorEnd[1]=cfg->colorEnd.g;
545 gpuDraw.colorEnd[2]=cfg->colorEnd.b; gpuDraw.colorEnd[3]=cfg->colorEnd.a;
546 gpuDraw.hframes=cfg->hframes; gpuDraw.vframes=cfg->vframes;
547 gpuDraw.sortMode = cfg->renderMode == "ribbon" ? graphics::GpuParticleSortMode::Birth
548 : cfg->sortMode == "oldest" ? graphics::GpuParticleSortMode::Oldest
549 : cfg->sortMode == "youngest" ? graphics::GpuParticleSortMode::Youngest
550 : cfg->sortMode == "distance" ? graphics::GpuParticleSortMode::Distance
552 if (gfx.drawGpuParticleEmitter(gpu->residentHandle, gpuDraw)) return gpu->estimatedAlive;
553 culled = true; return 0;
554 }
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;
558}
559}
560
562 if (!gfx || !emitter) return 0;
563 std::vector<graphics::DrawItem2D> items; bool usedCanvas=false, culled=false;
564 int order=0;
565 const int rendered=appendOneEmitter(*gfx,*emitter,items,order,false,usedCanvas,culled);
566 if(!items.empty()) graphics::RenderSystem::drawItems(*gfx,items,false);
567 if (usedCanvas) gfx->setCanvas();
568 return rendered;
569}
570
573 stats.renderedParticles = 0;
574 stats.renderCulledEmitters = 0;
575 stats.renderMs = 0.0;
576 const auto begin = std::chrono::steady_clock::now();
577 if (!gfx) return;
578 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) return;
579
580 std::vector<graphics::DrawItem2D> items;
583 bool anyCanvas = false;
584 int order = 0;
585 for (auto it = view.begin(); it != view.end(); ++it) {
586 auto [cfg, sim, draw] = *it; (void)sim; (void)draw;
587 auto* emitter = cfg->entity;
588 if (!emitter) continue;
589 bool culled=false;
590 stats.renderedParticles += appendOneEmitter(*gfx,*emitter,items,order,true,anyCanvas,culled);
591 if(culled) ++stats.renderCulledEmitters;
592 }
593
594 // Unified 2D sprite path: rotation / flipbook UV / blend / layer sorting
595 // and camera handling all come from RenderSystem::drawItems.
596 if (!items.empty()) graphics::RenderSystem::drawItems(*gfx, items, false);
597 if (anyCanvas) gfx->setCanvas();
598 stats.renderMs =
599 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - begin)
600 .count();
601}
602
604 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) return;
605
606 // Pass 1: collect emitters. Creating Light2D entities inside a deferred
607 // View would stage them and invalidate stored raw pointers on publish.
608 std::vector<ParticleEmitter *> emitters;
609 {
610 auto view = ecs::View<ParticleEmitter, ParticleEmitter::Config>();
611 for (auto it = view.begin(); it != view.end(); ++it) {
612 auto [cfg] = *it;
613 if (cfg->entity) emitters.push_back(cfg->entity);
614 }
615 }
616
617 // Pass 2: create/sync lights with no View active (stable entity pointers).
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);
626 continue;
627 }
628 const int maxL = cfg->lights.max > 0 ? (cfg->lights.max > 8 ? 8 : cfg->lights.max) : 0;
629 while (int(lights->pool.size()) < maxL)
630 lights->pool.push_back(graphics::Light2D::createLight());
631 const int n = sim->alive < maxL ? sim->alive : maxL;
632 for (int i = 0; i < maxL; ++i) {
633 graphics::Light2D *l = lights->pool[size_t(i)];
634 if (i < n) {
635 const Particle &p = sim->particles[size_t(i)];
636 l->setPosition(p.x, p.y);
637 l->setRadius(cfg->lights.radius);
638 l->setColor(cfg->lights.r, cfg->lights.g, cfg->lights.b, cfg->lights.intensity);
639 l->setCanvas(draw->canvas);
640 l->setEnabled(true);
641 } else {
642 l->setEnabled(false);
643 }
644 }
645 }
646}
647
649 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) return 0;
650
651 // Watch events are drained by load.nut / HotReload. This polling path is a
652 // deliberate, explicit file-state observation for hosts that do not
653 // deliver a watch event; its result is recorded on each Resource.
654 int reloaded = 0;
655 auto view =
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()) {
662 continue;
663 }
664 if (!res->autoReload) {
666 continue;
667 }
668
669 const int64_t mt = fileModtime(res->path);
670 if (mt < 0) {
672 continue;
673 }
674 if (mt == res->modtime) {
676 continue;
677 }
678 if (reloadConfigFile(cfg->entity, nullptr)) {
680 ++reloaded;
681 } else {
683 }
684 }
685 return reloaded;
686}
687
688} // namespace eve::particles
float w
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float duration
float length
Definition CaveMesh.cpp:94
glm::vec4 p[6]
tensor::Graph g
Definition GpuGraph.cpp:7
std::vector< eve::ProcgenProbeDesc > lights
std::uint32_t alive
std::uint32_t capacity
glm::vec3 n
Definition Grass.cpp:63
double r
std::vector< std::uint32_t > indices
std::int32_t c
int h
std::array< float, 3 > scale
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
MeshVfxBatchedDraw draw
graphics::Canvas * previous
OnnxTransferStats stats
Definition OnnxGpgpu.cpp:56
std::string path
Definition PlayHost.cpp:110
float begin
float t
glm::mat4 view
double current
float dy
float dx
std::unique_ptr< TrailEmitter > emitter
int margin
float step
Definition TreeMesh.cpp:314
uint32_t index
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.
Definition Diagnostic.h:125
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
virtual Texture * getSceneLinearDepthTexture()
Sampleable scene linear depth in [0,1], or nullptr when unavailable.
Definition Graphics.h:1049
int getWidth() const
Returns the width.
Definition Graphics.h:502
virtual bool drawGpuParticleEmitter(GpuParticleHandle handle, const GpuParticleDraw &draw)
Queue an indirect draw at the current 2D overlay position.
Definition Graphics.h:347
virtual void setCanvas(Canvas *canvas)=0
nullptr or this → screen. Switching flushes pending draws to the previous target.
int getHeight() const
Returns the height.
Definition Graphics.h:504
Declarative 2D light. Collected by RenderSystem (max 8 per canvas/frame). type: "point" | "dir" | "sp...
Definition Light.h:64
void setPosition(float x, float y)
Definition Light.cpp:37
void setColor(float r, float g, float b, float intensity=1.f)
Definition Light.cpp:53
void setCanvas(Canvas *canvas)
Definition Light.cpp:88
void setEnabled(bool enabled)
Definition Light.cpp:74
static Light2D * createLight(const std::string &type="point")
Definition Light.cpp:7
void setRadius(float radius)
Definition Light.cpp:61
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 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.
Definition Widget.cpp:679
glm::vec4 Color
Render-neutral RGBA color shared by graphics-facing modules.
Definition RenderTypes.h:8
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
Parameters for rendering a resident GPU particle emitter.
GpuParticleSortMode sortMode
GpuParticleFacingMode facing
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).
glm::uvec4 info