载入中...
搜索中...
未找到
ParticleEmitter.cpp
浏览该文件的文档.
3
10#include "gpgpu/ComputeShader.h"
11#include "gpgpu/Gpgpu.h"
12#include "gpgpu/GpuBuffer.h"
13#include "graphics/Canvas.h"
14#include "graphics/Texture.h"
15#include "ik/Skeleton2D.h"
16#include "ik/Skeleton3D.h"
18
19#include <cmath>
20#include <cstdio>
21#include <random>
22#include <string>
23
24namespace eve::particles {
25
26namespace {
27constexpr float kPi = 3.14159265358979323846f;
28constexpr float kEps = 1e-6f;
29
30float randRange(std::mt19937 &rng, float a, float b) {
31 if (a == b) return a;
32 std::uniform_real_distribution<float> dist(a, b);
33 return dist(rng);
34}
35
36int randIndex(std::mt19937 &rng, int n) {
37 if (n <= 1) return 0;
38 std::uniform_int_distribution<int> dist(0, n - 1);
39 return dist(rng);
40}
41
42float hashNoise2(int ix, int iy) {
43 unsigned h = unsigned(ix) * 374761393u + unsigned(iy) * 668265263u;
44 h = (h ^ (h >> 13)) * 1274126177u;
45 return (float((h ^ (h >> 16)) & 0xFFFFFFu) / float(0x1000000u)) * 2.f - 1.f;
46}
47
49float smoothNoise2(float x, float y) {
50 const int ix = int(std::floor(x));
51 const int iy = int(std::floor(y));
52 const float fx = x - float(ix);
53 const float fy = y - float(iy);
54 const float sx = fx * fx * (3.f - 2.f * fx);
55 const float sy = fy * fy * (3.f - 2.f * fy);
56 const float n00 = hashNoise2(ix, iy);
57 const float n10 = hashNoise2(ix + 1, iy);
58 const float n01 = hashNoise2(ix, iy + 1);
59 const float n11 = hashNoise2(ix + 1, iy + 1);
60 const float a = n00 + (n10 - n00) * sx;
61 const float b = n01 + (n11 - n01) * sx;
62 return a + (b - a) * sy;
63}
64
65std::string normalizePlane(const std::string &plane) {
66 if (plane == "xz" || plane == "yz") return plane;
67 return "xy";
68}
69
70void projectToPlane(float x, float y, float z, const std::string &plane, float scale, float &ox,
71 float &oy) {
72 if (plane == "xz") {
73 ox = x * scale;
74 oy = z * scale;
75 } else if (plane == "yz") {
76 ox = y * scale;
77 oy = z * scale;
78 } else {
79 ox = x * scale;
80 oy = y * scale;
81 }
82}
83
84void quatRotateVec(float qx, float qy, float qz, float qw, float vx, float vy, float vz, float &ox,
85 float &oy, float &oz) {
86 const float ix = qw * vx + qy * vz - qz * vy;
87 const float iy = qw * vy + qz * vx - qx * vz;
88 const float iz = qw * vz + qx * vy - qy * vx;
89 const float iw = -qx * vx - qy * vy - qz * vz;
90 ox = ix * qw + iw * -qx + iy * -qz - iz * -qy;
91 oy = iy * qw + iw * -qy + iz * -qx - ix * -qz;
92 oz = iz * qw + iw * -qz + ix * -qy - iy * -qx;
93}
94
95void sampleEmissionOffset(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, float &ox,
96 float &oy) {
97 ox = 0.f;
98 oy = 0.f;
99 if (cfg.areaX <= 0.f && cfg.areaY <= 0.f) return;
100 if (cfg.areaType == "ellipse") {
101 const float a = randRange(sim.rng, 0.f, kPi * 2.f);
102 const float r = std::sqrt(randRange(sim.rng, 0.f, 1.f));
103 ox = std::cos(a) * cfg.areaX * r;
104 oy = std::sin(a) * cfg.areaY * r;
105 } else if (cfg.areaType == "rect") {
106 ox = randRange(sim.rng, -cfg.areaX, cfg.areaX);
107 oy = randRange(sim.rng, -cfg.areaY, cfg.areaY);
108 } else if (cfg.areaType == "line") {
109 ox = randRange(sim.rng, -cfg.areaX, cfg.areaX);
110 oy = 0.f;
111 } else if (cfg.areaType == "ring") {
112 const float a = randRange(sim.rng, 0.f, kPi * 2.f);
113 const float rx = cfg.areaX > 0.f ? cfg.areaX : 1.f;
114 const float ry = cfg.areaY > 0.f ? cfg.areaY : rx;
115 ox = std::cos(a) * rx;
116 oy = std::sin(a) * ry;
117 }
118}
119
120void rebuildSkinCandidates(ParticleEmitter::SkinSource &src) {
121 src.candidates.clear();
122 src.candidatesDirty = false;
123 if (!src.skin || src.skin->getVertexCount() <= 0) return;
124
125 const int n = src.skin->getVertexCount();
126 const int influences = src.skin->getInfluenceCount();
127 if (src.filterBone < 0) {
128 src.candidates.reserve(static_cast<size_t>(n));
129 for (int v = 0; v < n; ++v) src.candidates.push_back(v);
130 return;
131 }
132
133 src.candidates.reserve(static_cast<size_t>(n / 4 + 1));
134 for (int v = 0; v < n; ++v) {
135 float w = 0.f;
136 for (int i = 0; i < influences; ++i) {
137 if (src.skin->getVertexBone(v, i) == src.filterBone) {
138 w += src.skin->getVertexWeight(v, i);
139 }
140 }
141 if (w >= src.minWeight) src.candidates.push_back(v);
142 }
143}
144
145bool ensureSkinCache(ParticleEmitter::SkinSource &src) {
146 if (!src.enabled || !src.skin || !src.pose) return false;
147 if (src.candidatesDirty) rebuildSkinCandidates(src);
148 if (src.candidates.empty()) return false;
149 // Always refresh skinned positions from the live pose (caller must have
150 // computeWorld()'d). Cheap relative to VFX; keeps surface following animation.
151 return src.skin->updateSkinnedPositions(src.pose);
152}
153
154void fillParticleMotion(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, Particle &p) {
155 p.lifetime = randRange(sim.rng, cfg.lifeMin, cfg.lifeMax);
156 if (p.lifetime <= 0.f) p.lifetime = 1e-4f;
157 p.life = p.lifetime;
158 p.ax = randRange(sim.rng, cfg.accelXMin, cfg.accelXMax);
159 p.ay = randRange(sim.rng, cfg.accelYMin, cfg.accelYMax);
160 p.radial = randRange(sim.rng, cfg.radialMin, cfg.radialMax);
161 p.tangential = randRange(sim.rng, cfg.tangentialMin, cfg.tangentialMax);
162 p.spin = randRange(sim.rng, cfg.spinMin, cfg.spinMax);
163 p.rot = randRange(sim.rng, cfg.startRotMin, cfg.startRotMax) * (kPi / 180.f);
164
165 const int totalFrames = cfg.hframes > 0 && cfg.vframes > 0 ? cfg.hframes * cfg.vframes : 1;
166 if (cfg.frameRandomStart > 0.f && totalFrames > 1) {
167 const float frac = cfg.frameRandomStart > 1.f ? 1.f : cfg.frameRandomStart;
168 p.frame = randRange(sim.rng, 0.f, frac) * float(totalFrames);
169 } else {
170 p.frame = 0.f;
171 }
172 p.noisePhase = randRange(sim.rng, 0.f, 1000.f);
173
174 float sizeMul = 1.f;
175 if (cfg.sizeVariation > 0.f) {
176 const float v = cfg.sizeVariation > 1.f ? 1.f : cfg.sizeVariation;
177 sizeMul = 1.f + randRange(sim.rng, -v, v);
178 if (sizeMul < 0.01f) sizeMul = 0.01f;
179 }
180 p.size = sizeMul;
181
182 const float half = cfg.spread * 0.5f;
183 const float angle = cfg.direction + randRange(sim.rng, -half, half);
184 const float speed = randRange(sim.rng, cfg.speedMin, cfg.speedMax);
185 p.vx = std::cos(angle) * speed;
186 p.vy = std::sin(angle) * speed;
187}
188
189void clearAttachSources(ParticleEmitter::Attach &a) {
191 a.pose = nullptr;
192 a.skeleton = nullptr;
193 a.spine = nullptr;
194 a.ik2d = nullptr;
195 a.ik3d = nullptr;
196 a.boneIndex = -1;
197 a.enabled = false;
198}
199
200void syncAnimPoseAttach(ParticleEmitter::Config &cfg, ParticleEmitter::Attach &attach) {
201 if (!attach.pose || attach.boneIndex < 0 || attach.boneIndex >= attach.pose->getBoneCount())
202 return;
203 const auto &w = attach.pose->world(attach.boneIndex);
204 float ox = attach.offsetX, oy = attach.offsetY, oz = attach.offsetZ;
205 float wx, wy, wz;
206 animation::Mat4::fromTRS(w).transformPoint(ox, oy, oz, wx, wy, wz);
207 projectToPlane(wx, wy, wz, attach.plane, attach.scale, cfg.x, cfg.y);
208
209 if (attach.followRotation) {
210 float fx, fy, fz;
211 quatRotateVec(w.qx, w.qy, w.qz, w.qw, 1.f, 0.f, 0.f, fx, fy, fz);
212 float ax, ay;
213 projectToPlane(fx, fy, fz, attach.plane, 1.f, ax, ay);
214 if (ax * ax + ay * ay > kEps) cfg.direction = std::atan2(ay, ax);
215 }
216}
217
218void syncSpineAttach(ParticleEmitter::Config &cfg, ParticleEmitter::Attach &attach) {
219 if (!attach.spine || attach.boneIndex < 0 || attach.boneIndex >= attach.spine->getBoneCount())
220 return;
221 float a, b, c, d;
222 attach.spine->getBoneWorldMatrix(attach.boneIndex, a, b, c, d);
223 const float wx =
224 attach.spine->getBoneWorldX(attach.boneIndex) + a * attach.offsetX + b * attach.offsetY;
225 const float wy =
226 attach.spine->getBoneWorldY(attach.boneIndex) + c * attach.offsetX + d * attach.offsetY;
227 // Spine is already 2D pixel space; scale still applies, plane ignored (xy).
228 cfg.x = wx * attach.scale;
229 cfg.y = wy * attach.scale;
230
231 if (attach.followRotation) {
232 // World rotation is degrees → particle direction radians.
233 cfg.direction = attach.spine->getBoneWorldRotation(attach.boneIndex) * (kPi / 180.f);
234 }
235}
236
237void syncIk2DAttach(ParticleEmitter::Config &cfg, ParticleEmitter::Attach &attach) {
238 if (!attach.ik2d || attach.boneIndex < 0 || attach.boneIndex >= attach.ik2d->getBoneCount())
239 return;
240 float ox = attach.offsetX;
241 float oy = attach.offsetY;
242 if (attach.followRotation || (ox != 0.f || oy != 0.f)) {
243 const float fx = attach.ik2d->getOrientationX(attach.boneIndex);
244 const float fy = attach.ik2d->getOrientationY(attach.boneIndex);
245 const float len2 = fx * fx + fy * fy;
246 if (len2 > kEps) {
247 const float inv = 1.f / std::sqrt(len2);
248 const float ux = fx * inv;
249 const float uy = fy * inv;
250 // Local +X along bone forward, +Y perpendicular.
251 const float rx = ox * ux - oy * uy;
252 const float ry = ox * uy + oy * ux;
253 ox = rx;
254 oy = ry;
255 if (attach.followRotation) cfg.direction = std::atan2(uy, ux);
256 }
257 }
258 cfg.x = (attach.ik2d->getX(attach.boneIndex) + ox) * attach.scale;
259 cfg.y = (attach.ik2d->getY(attach.boneIndex) + oy) * attach.scale;
260}
261
262void syncIk3DAttach(ParticleEmitter::Config &cfg, ParticleEmitter::Attach &attach) {
263 if (!attach.ik3d || attach.boneIndex < 0 || attach.boneIndex >= attach.ik3d->getBoneCount())
264 return;
265 float ox = attach.offsetX;
266 float oy = attach.offsetY;
267 float oz = attach.offsetZ;
268 const float fx = attach.ik3d->getOrientationX(attach.boneIndex);
269 const float fy = attach.ik3d->getOrientationY(attach.boneIndex);
270 const float fz = attach.ik3d->getOrientationZ(attach.boneIndex);
271 const float len2 = fx * fx + fy * fy + fz * fz;
272 if (len2 > kEps && (ox != 0.f || oy != 0.f || oz != 0.f || attach.followRotation)) {
273 const float inv = 1.f / std::sqrt(len2);
274 const float ux = fx * inv, uy = fy * inv, uz = fz * inv;
275 // Offset: along-bone (ox) + world remainder (oy/oz as translation extras).
276 const float wx = attach.ik3d->getX(attach.boneIndex) + ux * ox + oy;
277 const float wy = attach.ik3d->getY(attach.boneIndex) + uy * ox + oz;
278 const float wz = attach.ik3d->getZ(attach.boneIndex) + uz * ox;
279 projectToPlane(wx, wy, wz, attach.plane, attach.scale, cfg.x, cfg.y);
280 if (attach.followRotation) {
281 float ax, ay;
282 projectToPlane(ux, uy, uz, attach.plane, 1.f, ax, ay);
283 if (ax * ax + ay * ay > kEps) cfg.direction = std::atan2(ay, ax);
284 }
285 return;
286 }
287 projectToPlane(attach.ik3d->getX(attach.boneIndex) + ox, attach.ik3d->getY(attach.boneIndex) + oy,
288 attach.ik3d->getZ(attach.boneIndex) + oz, attach.plane, attach.scale, cfg.x,
289 cfg.y);
290}
291
292void fireSubEmitter(ParticleEmitter::Config &cfg, const std::string &trigger, float x, float y,
293 float vx, float vy) {
294 static int g_subDepth = 0;
295 if (g_subDepth >= 8) return; // cycle guard (A→B→A chains)
296 for (const auto &se : cfg.subEmitters) {
297 if (!se.target || se.trigger != trigger) continue;
298 // Self-referencing sub-emitters would corrupt the compaction loop.
299 if (se.target == cfg.entity) continue;
300 auto tc = se.target->config();
301 auto ts = se.target->sim();
302 if (ts->alive >= int(ts->particles.size())) continue;
303 ++g_subDepth;
304 spawnParticleAt(*tc, *ts, x, y);
305 --g_subDepth;
306 if (se.inheritVelocity > 0.f) {
307 Particle &p = ts->particles[size_t(ts->alive - 1)];
308 p.vx += vx * se.inheritVelocity;
309 p.vy += vy * se.inheritVelocity;
310 }
311 }
312}
313
314} // namespace
315
316namespace {
317WorldCollisionFn g_worldCollision = nullptr;
318} // namespace
319
320void setWorldCollisionResolver(WorldCollisionFn fn) { g_worldCollision = fn; }
321
322WorldCollisionFn getWorldCollisionResolver() { return g_worldCollision; }
323
325 if (sim.alive >= int(sim.particles.size())) return;
326
327 Particle &p = sim.particles[size_t(sim.alive++)];
328 float ox = 0.f, oy = 0.f;
329 sampleEmissionOffset(cfg, sim, ox, oy);
330 p.x = x + ox;
331 p.y = y + oy;
332 fillParticleMotion(cfg, sim, p);
333 fireSubEmitter(cfg, "birth", p.x, p.y, p.vx, p.vy);
334}
335
337 // Prefer skin surface when configured on the owning entity.
338 if (cfg.entity) {
339 auto skinComp = cfg.entity->skinSource();
340 if (skinComp->enabled) {
341 float sx = cfg.x, sy = cfg.y;
342 if (sampleSkinSpawn(*skinComp, sim, sx, sy)) {
343 spawnParticleAt(cfg, sim, sx, sy);
344 return;
345 }
346 }
347 }
348 spawnParticleAt(cfg, sim, cfg.x, cfg.y);
349}
350
352 float &outY) {
353 if (!ensureSkinCache(skinSrc)) return false;
354 const int vi = skinSrc.candidates[static_cast<size_t>(randIndex(sim.rng, int(skinSrc.candidates.size())))];
355 const float wx = skinSrc.skin->getSkinnedPositionX(vi);
356 const float wy = skinSrc.skin->getSkinnedPositionY(vi);
357 const float wz = skinSrc.skin->getSkinnedPositionZ(vi);
358 projectToPlane(wx, wy, wz, skinSrc.plane, skinSrc.scale, outX, outY);
359 return true;
360}
361
364 if (attach.enabled) {
365 switch (attach.kind) {
367 syncAnimPoseAttach(cfg, attach);
368 break;
370 syncSpineAttach(cfg, attach);
371 break;
373 syncIk2DAttach(cfg, attach);
374 break;
376 syncIk3DAttach(cfg, attach);
377 break;
379 default:
380 break;
381 }
382 }
383
384 if (skinSrc.enabled && skinSrc.skin && skinSrc.pose) {
385 ensureSkinCache(skinSrc);
386 }
387}
388
390 if (sim.paused || dt <= 0.f) return;
391 if (cfg.maxDeltaTime > 0.f && dt > cfg.maxDeltaTime) dt = cfg.maxDeltaTime;
392
393 // Emitter velocity for inheritVelocity (before lastX/lastY refresh).
394 float emitVx = 0.f, emitVy = 0.f;
395 if (cfg.inheritVelocity > 0.f && sim.hasLastPos) {
396 emitVx = (cfg.x - sim.lastX) / dt;
397 emitVy = (cfg.y - sim.lastY) / dt;
398 }
399 const bool localSpace = cfg.simSpace == "local";
400 const float localDx = localSpace && sim.hasLastPos ? cfg.x - sim.lastX : 0.f;
401 const float localDy = localSpace && sim.hasLastPos ? cfg.y - sim.lastY : 0.f;
402
403 const float damp = cfg.damping > 0.f ? (cfg.damping > 1.f ? 1.f : cfg.damping) : 0.f;
404 const float dampFactor = damp > 0.f ? std::max(0.f, 1.f - damp * dt) : 1.f;
405 const float noiseFreq = cfg.noiseFrequency > 0.f ? cfg.noiseFrequency : 1.f;
406 const bool hasCollision =
407 cfg.collisionMode != "none" && (cfg.worldCollision || cfg.collisionBoundsEnabled);
408 const bool bounce = cfg.collisionMode == "bounce";
409 const bool killMode = cfg.collisionMode == "kill";
410 const bool stopMode = cfg.collisionMode == "stop";
411 const float cRadius = cfg.collisionRadius;
412 const float lifeLoss = cfg.collisionLifetimeLoss;
413
414 int write = 0;
415 for (int i = 0; i < sim.alive; ++i) {
416 Particle &p = sim.particles[size_t(i)];
417 p.life -= dt;
418 if (p.life <= 0.f) {
419 fireSubEmitter(cfg, "death", p.x, p.y, p.vx, p.vy);
420 continue;
421 }
422
423 if (localSpace && (localDx != 0.f || localDy != 0.f)) {
424 p.x += localDx;
425 p.y += localDy;
426 }
427
428 float ax = p.ax + cfg.gravityX;
429 float ay = p.ay + cfg.gravityY;
430 const float dx = p.x - cfg.x;
431 const float dy = p.y - cfg.y;
432 const float len2 = dx * dx + dy * dy;
433 if (len2 > kEps) {
434 const float inv = 1.f / std::sqrt(len2);
435 const float rdx = dx * inv;
436 const float rdy = dy * inv;
437 ax += rdx * p.radial - rdy * p.tangential;
438 ay += rdy * p.radial + rdx * p.tangential;
439 }
440
441 if (cfg.noiseStrength != 0.f) {
442 const float t = sim.emitterAge * cfg.noiseSpeed;
443 ax += smoothNoise2(p.x * noiseFreq + t, p.y * noiseFreq + p.noisePhase) *
444 cfg.noiseStrength;
445 ay += smoothNoise2(p.y * noiseFreq + t, p.x * noiseFreq - p.noisePhase) *
446 cfg.noiseStrength;
447 }
448
449 // Radial force fields (strength > 0 attract, < 0 repel).
450 for (const auto &f : cfg.forceFields) {
451 if (f.radius <= 0.f || f.strength == 0.f) continue;
452 const float fdx = f.x - p.x;
453 const float fdy = f.y - p.y;
454 const float dist = std::sqrt(fdx * fdx + fdy * fdy);
455 if (dist >= f.radius) continue;
456 const float fall = std::pow(1.f - dist / f.radius, f.falloff > 0.f ? f.falloff : 1.f);
457 const float inv = dist > kEps ? 1.f / dist : 0.f;
458 ax += fdx * inv * f.strength * fall;
459 ay += fdy * inv * f.strength * fall;
460 }
461
462 p.vx += ax * dt;
463 p.vy += ay * dt;
464
465 // Velocity-over-lifetime multiplier (applied as a smooth ratio).
466 if (!cfg.velocityCurve.empty() && p.lifetime > 0.f) {
467 const float tNew = 1.f - (p.life / p.lifetime);
468 const float tOld = tNew - dt / p.lifetime;
469 const float vOld = cfg.velocityCurve.sample(tOld, 1.f);
470 const float vNew = cfg.velocityCurve.sample(tNew, 1.f);
471 if (vOld > 1e-4f) {
472 const float ratio = vNew / vOld;
473 p.vx *= ratio;
474 p.vy *= ratio;
475 }
476 }
477
478 if (dampFactor != 1.f) {
479 p.vx *= dampFactor;
480 p.vy *= dampFactor;
481 }
482 if (cfg.limitVelocity > 0.f) {
483 const float sp2 = p.vx * p.vx + p.vy * p.vy;
484 const float max2 = cfg.limitVelocity * cfg.limitVelocity;
485 if (sp2 > max2) {
486 const float s = std::sqrt(max2 / sp2);
487 p.vx *= s;
488 p.vy *= s;
489 }
490 }
491
492 p.x += p.vx * dt;
493 p.y += p.vy * dt;
494 p.rot += p.spin * dt;
495 p.frame += cfg.frameRate * dt;
496
497 if (hasCollision) {
498 const float scale = p.size > 0.f ? p.size : 1.f;
499 const float rad =
500 cRadius > 0.f ? cRadius : std::max(cfg.particleW, cfg.particleH) * 0.5f * scale;
501 float nx = 0.f, ny = 0.f;
502 bool hit = false;
503 if (cfg.worldCollision) {
505 if (fn(p.x, p.y, rad, nx, ny)) hit = true;
506 }
507 if (!hit && cfg.collisionBoundsEnabled) {
508 if (p.x - rad < cfg.boundsMinX) {
509 nx += 1.f;
510 p.x = cfg.boundsMinX + rad;
511 hit = true;
512 } else if (p.x + rad > cfg.boundsMaxX) {
513 nx += -1.f;
514 p.x = cfg.boundsMaxX - rad;
515 hit = true;
516 }
517 if (p.y - rad < cfg.boundsMinY) {
518 ny += 1.f;
519 p.y = cfg.boundsMinY + rad;
520 hit = true;
521 } else if (p.y + rad > cfg.boundsMaxY) {
522 ny += -1.f;
523 p.y = cfg.boundsMaxY - rad;
524 hit = true;
525 }
526 const float nlen = std::sqrt(nx * nx + ny * ny);
527 if (nlen > kEps) {
528 nx /= nlen;
529 ny /= nlen;
530 }
531 }
532 if (hit) {
533 fireSubEmitter(cfg, "collision", p.x, p.y, p.vx, p.vy);
534 if (killMode) continue;
535 if (stopMode) {
536 p.vx = 0.f;
537 p.vy = 0.f;
538 } else if (bounce && (nx != 0.f || ny != 0.f)) {
539 const float dot = p.vx * nx + p.vy * ny;
540 p.vx = (p.vx - 2.f * dot * nx) * cfg.collisionRestitution;
541 p.vy = (p.vy - 2.f * dot * ny) * cfg.collisionRestitution;
542 }
543 if (lifeLoss > 0.f) {
544 p.life -= p.lifetime * lifeLoss;
545 if (p.life <= 0.f) continue;
546 }
547 }
548 }
549
550 if (write != i) sim.particles[size_t(write)] = p;
551 ++write;
552 }
553 sim.alive = write;
554
555 if (!sim.active) {
556 sim.lastX = cfg.x;
557 sim.lastY = cfg.y;
558 sim.hasLastPos = true;
559 return;
560 }
561
562 sim.emitterAge += dt;
563
564 // Timed bursts fire once while the emitter is active.
565 auto spawnWithInherit = [&]() {
566 spawnParticle(cfg, sim);
567 if (cfg.inheritVelocity > 0.f && sim.alive > 0) {
568 Particle &np = sim.particles[size_t(sim.alive - 1)];
569 np.vx += emitVx * cfg.inheritVelocity;
570 np.vy += emitVy * cfg.inheritVelocity;
571 }
572 };
573 for (auto &b : cfg.bursts) {
574 if (!b.emitted && b.count > 0 && sim.emitterAge >= b.time) {
575 b.emitted = true;
576 for (int k = 0; k < b.count; ++k) {
577 if (sim.alive >= int(sim.particles.size())) break;
578 spawnWithInherit();
579 }
580 }
581 }
582
583 if (cfg.emissionRate > 0.f) {
584 sim.emitAccum += cfg.emissionRate * dt;
585 while (sim.emitAccum >= 1.f) {
586 if (sim.alive >= int(sim.particles.size())) {
587 if (cfg.overflowMode == "pause") break; // keep accum; retry next frame
588 if (cfg.overflowMode == "warn" && !sim.overflowWarned) {
589 sim.overflowWarned = true;
590 std::fprintf(stderr,
591 "[particles] buffer overflow (emissionRate=%.1f, buffer=%d)\n",
592 cfg.emissionRate, int(sim.particles.size()));
593 }
594 sim.emitAccum = 0.f;
595 break;
596 }
597 spawnWithInherit();
598 sim.emitAccum -= 1.f;
599 }
600 }
601
602 // Expire AFTER this frame's emission so a short-lived emitter still
603 // releases the particles due during its final frame.
604 if (cfg.emitterLife >= 0.f && sim.emitterAge >= cfg.emitterLife) {
605 sim.active = false;
606 sim.emitAccum = 0.f;
607 }
608
609 sim.lastX = cfg.x;
610 sim.lastY = cfg.y;
611 sim.hasLastPos = true;
612}
613
614namespace {
615
616constexpr int kGpuStride = 16;
617
618eve::gpgpu::ComputeShader *sharedGpuParticleShader(eve::gpgpu::Gpgpu *gpgpu) {
619 static eve::gpgpu::ComputeShader *s_shader = nullptr;
620 static eve::gpgpu::Gpgpu *s_gpgpu = nullptr;
621 if (!s_shader || s_gpgpu != gpgpu) {
622 delete s_shader;
623 s_shader = nullptr;
624 try {
625 s_shader = gpgpu->newShader(kParticleGpuKernel);
626 } catch (...) {
627 s_shader = nullptr;
628 }
629 s_gpgpu = gpgpu;
630 }
631 return s_shader;
632}
633
634void packParticles(const ParticleEmitter::Sim &sim, std::vector<float> &mirror) {
635 const size_t n = sim.particles.size();
636 mirror.assign(n * size_t(kGpuStride), 0.f);
637 for (size_t i = 0; i < n && int(i) < sim.alive; ++i) {
638 const Particle &p = sim.particles[i];
639 float *m = mirror.data() + i * size_t(kGpuStride);
640 m[0] = p.x;
641 m[1] = p.y;
642 m[2] = p.vx;
643 m[3] = p.vy;
644 m[4] = p.life;
645 m[5] = p.lifetime;
646 m[6] = p.size;
647 m[7] = p.rot;
648 m[8] = p.spin;
649 m[9] = p.frame;
650 m[10] = p.radial;
651 m[11] = p.tangential;
652 m[12] = p.ax;
653 m[13] = p.ay;
654 m[14] = p.noisePhase;
655 }
656}
657
658void unpackParticles(const std::vector<float> &mirror, ParticleEmitter::Sim &sim) {
659 const size_t n = sim.particles.size();
660 sim.alive = 0;
661 for (size_t i = 0; i < n; ++i) {
662 const float *m = mirror.data() + i * size_t(kGpuStride);
663 Particle &p = sim.particles[i];
664 p.x = m[0];
665 p.y = m[1];
666 p.vx = m[2];
667 p.vy = m[3];
668 p.life = m[4];
669 p.lifetime = m[5];
670 p.size = m[6];
671 p.rot = m[7];
672 p.spin = m[8];
673 p.frame = m[9];
674 p.radial = m[10];
675 p.tangential = m[11];
676 p.ax = m[12];
677 p.ay = m[13];
678 p.noisePhase = m[14];
679 if (p.life > 0.f) ++sim.alive;
680 }
681}
682
683} // namespace
684
692 ParticleEmitter::GpuSim &gpu, float dt) {
693 if (sim.paused || dt <= 0.f) return true;
694 if (cfg.maxDeltaTime > 0.f && dt > cfg.maxDeltaTime) dt = cfg.maxDeltaTime;
695
696 auto *gpgpu = eve::gpgpu::Gpgpu::create();
697 if (!gpgpu || !gpgpu->isAvailable()) return false;
698
699 if (!gpu.initialized) {
700 gpu.initialized = true;
701 const size_t n = sim.particles.size();
702 gpu.mirror.assign(n * size_t(kGpuStride), 0.f);
703 try {
704 gpu.buffer.reset(
705 gpgpu->newBuffer(int(n * size_t(kGpuStride) * sizeof(float)), "storage"));
706 } catch (...) {
707 gpu.failed = true;
708 return false;
709 }
710 if (!gpu.buffer) {
711 gpu.failed = true;
712 return false;
713 }
714 }
715 if (gpu.failed) return false;
716
717 auto *shader = sharedGpuParticleShader(gpgpu);
718 if (!shader) {
719 gpu.failed = true;
720 return false;
721 }
722
723 // Emitter velocity for inheritVelocity (before lastX/lastY refresh).
724 float emitVx = 0.f, emitVy = 0.f;
725 if (cfg.inheritVelocity > 0.f && sim.hasLastPos) {
726 emitVx = (cfg.x - sim.lastX) / dt;
727 emitVy = (cfg.y - sim.lastY) / dt;
728 }
729 const bool localSpace = cfg.simSpace == "local";
730 const float localDx = localSpace && sim.hasLastPos ? cfg.x - sim.lastX : 0.f;
731 const float localDy = localSpace && sim.hasLastPos ? cfg.y - sim.lastY : 0.f;
732
733 // 1) Upload the current CPU state, 2) dispatch the integration kernel,
734 // 3) read back the integrated state.
735 packParticles(sim, gpu.mirror);
736 try {
737 gpu.buffer->writeFloat32s(gpu.mirror.data(), int(gpu.mirror.size()), 0);
738 const int alive = sim.alive;
739 if (alive > 0) {
740 shader->bindBuffer(0, gpu.buffer.get());
741 shader->setFloat(0, float(alive));
742 shader->setFloat(1, dt);
743 shader->setFloat(2, cfg.x);
744 shader->setFloat(3, cfg.y);
745 shader->setFloat(4, cfg.gravityX);
746 shader->setFloat(5, cfg.gravityY);
747 shader->setFloat(6, cfg.damping);
748 shader->setFloat(7, cfg.limitVelocity);
749 shader->setFloat(8, cfg.noiseStrength);
750 shader->setFloat(9, cfg.noiseFrequency > 0.f ? cfg.noiseFrequency : 1.f);
751 shader->setFloat(10, cfg.noiseSpeed);
752 shader->setFloat(11, sim.emitterAge);
753 shader->setFloat(12, cfg.frameRate);
754 const int groups = (alive + 63) / 64;
755 gpgpu->dispatch(shader, groups, 1, 1);
756 }
757 gpu.buffer->readFloat32s(gpu.mirror.data(), int(gpu.mirror.size()), 0);
758 } catch (...) {
759 gpu.failed = true;
760 return false;
761 }
762 unpackParticles(gpu.mirror, sim);
763 const int gpuAlive = sim.alive;
764
765 // 4) CPU-side post-integration: local-space tracking, collision, death
766 // compaction (fire death sub-emitters), emitter age, bursts, emission.
767 const bool hasCollision =
768 cfg.collisionMode != "none" && (cfg.worldCollision || cfg.collisionBoundsEnabled);
769 const bool bounce = cfg.collisionMode == "bounce";
770 const bool killMode = cfg.collisionMode == "kill";
771 const bool stopMode = cfg.collisionMode == "stop";
772 const float cRadius = cfg.collisionRadius;
773 const float lifeLoss = cfg.collisionLifetimeLoss;
774
775 int write = 0;
776 for (int i = 0; i < int(sim.particles.size()); ++i) {
777 Particle &p = sim.particles[size_t(i)];
778 if (p.life <= 0.f) {
779 if (i < gpuAlive) fireSubEmitter(cfg, "death", p.x, p.y, p.vx, p.vy);
780 continue;
781 }
782 if (localSpace && (localDx != 0.f || localDy != 0.f)) {
783 p.x += localDx;
784 p.y += localDy;
785 }
786 if (hasCollision) {
787 const float scale = p.size > 0.f ? p.size : 1.f;
788 const float rad =
789 cRadius > 0.f ? cRadius : std::max(cfg.particleW, cfg.particleH) * 0.5f * scale;
790 float nx = 0.f, ny = 0.f;
791 bool hit = false;
792 if (cfg.worldCollision) {
794 if (fn(p.x, p.y, rad, nx, ny)) hit = true;
795 }
796 if (!hit && cfg.collisionBoundsEnabled) {
797 if (p.x - rad < cfg.boundsMinX) {
798 nx += 1.f;
799 p.x = cfg.boundsMinX + rad;
800 hit = true;
801 } else if (p.x + rad > cfg.boundsMaxX) {
802 nx += -1.f;
803 p.x = cfg.boundsMaxX - rad;
804 hit = true;
805 }
806 if (p.y - rad < cfg.boundsMinY) {
807 ny += 1.f;
808 p.y = cfg.boundsMinY + rad;
809 hit = true;
810 } else if (p.y + rad > cfg.boundsMaxY) {
811 ny += -1.f;
812 p.y = cfg.boundsMaxY - rad;
813 hit = true;
814 }
815 const float nlen = std::sqrt(nx * nx + ny * ny);
816 if (nlen > kEps) {
817 nx /= nlen;
818 ny /= nlen;
819 }
820 }
821 if (hit) {
822 fireSubEmitter(cfg, "collision", p.x, p.y, p.vx, p.vy);
823 if (killMode) continue;
824 if (stopMode) {
825 p.vx = 0.f;
826 p.vy = 0.f;
827 } else if (bounce && (nx != 0.f || ny != 0.f)) {
828 const float dot = p.vx * nx + p.vy * ny;
829 p.vx = (p.vx - 2.f * dot * nx) * cfg.collisionRestitution;
830 p.vy = (p.vy - 2.f * dot * ny) * cfg.collisionRestitution;
831 }
832 if (lifeLoss > 0.f) {
833 p.life -= p.lifetime * lifeLoss;
834 if (p.life <= 0.f) continue;
835 }
836 }
837 }
838 if (write != i) sim.particles[size_t(write)] = p;
839 ++write;
840 }
841 sim.alive = write;
842 // Mark stale tail slots dead so the next readback never re-fires death subs.
843 for (int i = write; i < int(sim.particles.size()); ++i)
844 sim.particles[size_t(i)].life = -1.f;
845
846 if (!sim.active) {
847 sim.lastX = cfg.x;
848 sim.lastY = cfg.y;
849 sim.hasLastPos = true;
850 return true;
851 }
852
853 sim.emitterAge += dt;
854
855 auto spawnWithInherit = [&]() {
856 spawnParticle(cfg, sim);
857 if (cfg.inheritVelocity > 0.f && sim.alive > 0) {
858 Particle &np = sim.particles[size_t(sim.alive - 1)];
859 np.vx += emitVx * cfg.inheritVelocity;
860 np.vy += emitVy * cfg.inheritVelocity;
861 }
862 };
863 for (auto &b : cfg.bursts) {
864 if (!b.emitted && b.count > 0 && sim.emitterAge >= b.time) {
865 b.emitted = true;
866 for (int k = 0; k < b.count; ++k) {
867 if (sim.alive >= int(sim.particles.size())) break;
868 spawnWithInherit();
869 }
870 }
871 }
872 if (cfg.emissionRate > 0.f) {
873 sim.emitAccum += cfg.emissionRate * dt;
874 while (sim.emitAccum >= 1.f) {
875 if (sim.alive >= int(sim.particles.size())) {
876 if (cfg.overflowMode == "pause") break;
877 if (cfg.overflowMode == "warn" && !sim.overflowWarned) {
878 sim.overflowWarned = true;
879 std::fprintf(stderr,
880 "[particles] buffer overflow (emissionRate=%.1f, buffer=%d)\n",
881 cfg.emissionRate, int(sim.particles.size()));
882 }
883 sim.emitAccum = 0.f;
884 break;
885 }
886 spawnWithInherit();
887 sim.emitAccum -= 1.f;
888 }
889 }
890
891 // Expire AFTER this frame's emission (see stepEmitterSim).
892 if (cfg.emitterLife >= 0.f && sim.emitterAge >= cfg.emitterLife) {
893 sim.active = false;
894 sim.emitAccum = 0.f;
895 }
896
897 sim.lastX = cfg.x;
898 sim.lastY = cfg.y;
899 sim.hasLastPos = true;
900 return true;
901}
902
904 ParticleEmitter *e = ParticleEmitter::create();
905 e->config()->entity = e;
906 const int n = bufferSize > 0 ? bufferSize : 1;
907 e->sim()->particles.resize(size_t(n));
908 std::random_device rd;
909 e->sim()->rng.seed(rd());
910 // Touch Draw / Attach / SkinSource so system views see fully initialized emitters.
911 (void)e->draw();
912 (void)e->attach();
913 (void)e->skinSource();
914 (void)e->lights();
915 (void)e->gpuSim();
916 return e;
917}
918
919void ParticleEmitter::setPosition(float x, float y) {
920 config()->x = x;
921 config()->y = y;
922}
923
924void ParticleEmitter::moveTo(float x, float y) { setPosition(x, y); }
925
926float ParticleEmitter::getX() { return config()->x; }
927float ParticleEmitter::getY() { return config()->y; }
928
930 config()->emissionRate = rate < 0.f ? 0.f : rate;
931}
932
933float ParticleEmitter::getEmissionRate() { return config()->emissionRate; }
934
935void ParticleEmitter::setParticleLifetime(float minLife, float maxLife) {
936 auto c = config();
937 c->lifeMin = minLife < 0.f ? 0.f : minLife;
938 c->lifeMax = maxLife < c->lifeMin ? c->lifeMin : maxLife;
939}
940
941float ParticleEmitter::getParticleLifetimeMin() { return config()->lifeMin; }
942float ParticleEmitter::getParticleLifetimeMax() { return config()->lifeMax; }
943
944void ParticleEmitter::setEmitterLifetime(float seconds) { config()->emitterLife = seconds; }
945float ParticleEmitter::getEmitterLifetime() { return config()->emitterLife; }
946
947void ParticleEmitter::setDirection(float radians) { config()->direction = radians; }
948float ParticleEmitter::getDirection() { return config()->direction; }
949
950void ParticleEmitter::setSpread(float radians) {
951 config()->spread = radians < 0.f ? 0.f : radians;
952}
953float ParticleEmitter::getSpread() { return config()->spread; }
954
955void ParticleEmitter::setSpeed(float minSpeed, float maxSpeed) {
956 auto c = config();
957 c->speedMin = minSpeed;
958 c->speedMax = maxSpeed < minSpeed ? minSpeed : maxSpeed;
959}
960
961void ParticleEmitter::setLinearAcceleration(float xmin, float ymin, float xmax, float ymax) {
962 auto c = config();
963 c->accelXMin = xmin;
964 c->accelYMin = ymin;
965 c->accelXMax = xmax < xmin ? xmin : xmax;
966 c->accelYMax = ymax < ymin ? ymin : ymax;
967}
968
969void ParticleEmitter::setRadialAcceleration(float minA, float maxA) {
970 auto c = config();
971 c->radialMin = minA;
972 c->radialMax = maxA < minA ? minA : maxA;
973}
974
975void ParticleEmitter::setTangentialAcceleration(float minA, float maxA) {
976 auto c = config();
977 c->tangentialMin = minA;
978 c->tangentialMax = maxA < minA ? minA : maxA;
979}
980
981void ParticleEmitter::setEmissionArea(const std::string &type, float x, float y) {
982 auto c = config();
983 if (type == "ellipse" || type == "rect" || type == "line" || type == "ring")
984 c->areaType = type;
985 else
986 c->areaType = "none";
987 c->areaX = x < 0.f ? 0.f : x;
988 c->areaY = y < 0.f ? 0.f : y;
989}
990
991std::string ParticleEmitter::getEmissionAreaType() { return config()->areaType; }
992float ParticleEmitter::getEmissionAreaX() { return config()->areaX; }
993float ParticleEmitter::getEmissionAreaY() { return config()->areaY; }
994
996 auto c = config();
997 c->particleW = width > 0.f ? width : 1.f;
998 c->particleH = height > 0.f ? height : 1.f;
999}
1000
1001float ParticleEmitter::getParticleWidth() { return config()->particleW; }
1002float ParticleEmitter::getParticleHeight() { return config()->particleH; }
1003
1004void ParticleEmitter::setSizes(float startScale, float endScale) {
1005 config()->sizeStart = startScale;
1006 config()->sizeEnd = endScale;
1007}
1008
1010 config()->sizeVariation = variation < 0.f ? 0.f : (variation > 1.f ? 1.f : variation);
1011}
1012float ParticleEmitter::getSizeVariation() { return config()->sizeVariation; }
1013
1014void ParticleEmitter::setSpin(float minSpin, float maxSpin) {
1015 auto c = config();
1016 c->spinMin = minSpin;
1017 c->spinMax = maxSpin < minSpin ? minSpin : maxSpin;
1018}
1019
1020void ParticleEmitter::setStartRotation(float minDeg, float maxDeg) {
1021 auto c = config();
1022 c->startRotMin = minDeg;
1023 c->startRotMax = maxDeg < minDeg ? minDeg : maxDeg;
1024}
1025
1026void ParticleEmitter::addBurst(float time, int count) {
1027 if (count <= 0) return;
1028 config()->bursts.push_back(Config::Burst{time < 0.f ? 0.f : time, count, false});
1029}
1030
1031void ParticleEmitter::clearBursts() { config()->bursts.clear(); }
1032
1033void ParticleEmitter::setPrewarm(float seconds) {
1034 config()->prewarmSeconds = seconds < 0.f ? 0.f : seconds;
1035}
1036
1037float ParticleEmitter::getPrewarmSeconds() { return config()->prewarmSeconds; }
1038
1039void ParticleEmitter::setGravity(float x, float y) {
1040 auto c = config();
1041 c->gravityX = x;
1042 c->gravityY = y;
1043}
1044
1045void ParticleEmitter::setDamping(float perSecond) {
1046 config()->damping = perSecond < 0.f ? 0.f : perSecond;
1047}
1048
1050 config()->limitVelocity = maxSpeed < 0.f ? 0.f : maxSpeed;
1051}
1052
1053void ParticleEmitter::clearVelocityCurve() { config()->velocityCurve.clear(); }
1054
1056 config()->velocityCurve.add(t, v);
1057}
1058
1060 config()->inheritVelocity =
1061 fraction < 0.f ? 0.f : (fraction > 1.f ? 1.f : fraction);
1062}
1063
1064void ParticleEmitter::setSimulationSpace(const std::string &space) {
1065 config()->simSpace = space == "local" ? "local" : "world";
1066}
1067
1068void ParticleEmitter::setNoise(float strength, float frequency, float speed) {
1069 auto c = config();
1070 c->noiseStrength = strength;
1071 c->noiseFrequency = frequency > 0.f ? frequency : 1.f;
1072 c->noiseSpeed = speed;
1073}
1074
1076 config()->gpuSimulation = enable;
1077 gpuSim()->enabled = enable;
1078}
1079
1080bool ParticleEmitter::getGpuSimulation() { return config()->gpuSimulation; }
1081
1082void ParticleEmitter::setCollision(const std::string &mode, float radius, float restitution,
1083 float lifetimeLoss) {
1084 auto c = config();
1085 c->collisionMode =
1086 (mode == "kill" || mode == "bounce" || mode == "stop") ? mode : "none";
1087 c->collisionRadius = radius < 0.f ? 0.f : radius;
1088 c->collisionRestitution = restitution < 0.f ? 0.f : restitution;
1089 c->collisionLifetimeLoss =
1090 lifetimeLoss < 0.f ? 0.f : (lifetimeLoss > 1.f ? 1.f : lifetimeLoss);
1091}
1092
1093void ParticleEmitter::setCollisionBounds(bool enabled, float minX, float minY, float maxX,
1094 float maxY) {
1095 auto c = config();
1096 c->collisionBoundsEnabled = enabled;
1097 c->boundsMinX = minX;
1098 c->boundsMinY = minY;
1099 c->boundsMaxX = maxX;
1100 c->boundsMaxY = maxY;
1101}
1102
1103void ParticleEmitter::setWorldCollision(bool enabled) { config()->worldCollision = enabled; }
1104
1105void ParticleEmitter::setRenderMode(const std::string &mode, float stretchFactor) {
1106 auto c = config();
1107 c->renderMode = mode == "stretched" ? "stretched" : "billboard";
1108 c->stretchFactor = stretchFactor < 0.f ? 0.f : stretchFactor;
1109}
1110
1111void ParticleEmitter::setOverflowMode(const std::string &mode) {
1112 config()->overflowMode =
1113 (mode == "pause" || mode == "warn") ? mode : "drop";
1114}
1115
1117 config()->maxDeltaTime = seconds < 0.f ? 0.f : seconds;
1118}
1119
1120void ParticleEmitter::addSubEmitter(ParticleEmitter *target, const std::string &trigger,
1121 float inheritVelocity) {
1122 if (!target || target == this) return;
1124 se.target = target;
1125 se.trigger = (trigger == "death" || trigger == "collision") ? trigger : "birth";
1126 se.inheritVelocity = inheritVelocity < 0.f ? 0.f : (inheritVelocity > 1.f ? 1.f : inheritVelocity);
1127 config()->subEmitters.push_back(se);
1128}
1129
1130void ParticleEmitter::clearSubEmitters() { config()->subEmitters.clear(); }
1131
1132void ParticleEmitter::addForceField(float x, float y, float radius, float strength,
1133 float falloff) {
1134 if (radius <= 0.f || strength == 0.f) return;
1136 f.x = x;
1137 f.y = y;
1138 f.radius = radius;
1139 f.strength = strength;
1140 f.falloff = falloff > 0.f ? falloff : 1.f;
1141 config()->forceFields.push_back(f);
1142}
1143
1144void ParticleEmitter::clearForceFields() { config()->forceFields.clear(); }
1145
1148
1149void ParticleEmitter::setLights(bool enabled, float radius, float intensity, float r, float g,
1150 float b, int maxLights) {
1151 auto c = config();
1152 c->lights.enabled = enabled;
1153 c->lights.radius = radius > 0.f ? radius : 0.f;
1154 c->lights.intensity = intensity;
1155 c->lights.r = r;
1156 c->lights.g = g;
1157 c->lights.b = b;
1158 c->lights.max = maxLights > 0 ? maxLights : 0;
1159}
1160
1161bool ParticleEmitter::getLightsEnabled() { return config()->lights.enabled; }
1162
1163void ParticleEmitter::setBlendMode(const std::string &mode) {
1164 if (mode == "additive")
1165 draw()->blend = graphics::BlendMode::Additive;
1166 else if (mode == "opaque")
1167 draw()->blend = graphics::BlendMode::Opaque;
1168 else
1169 draw()->blend = graphics::BlendMode::Alpha;
1170}
1171
1173 switch (draw()->blend) {
1175 return "additive";
1177 return "opaque";
1179 default:
1180 return "alpha";
1181 }
1182}
1183
1184void ParticleEmitter::setFlipbook(int h, int v, float framesPerSecond, float randomStart) {
1185 auto c = config();
1186 c->hframes = h > 0 ? h : 1;
1187 c->vframes = v > 0 ? v : 1;
1188 c->frameRate = framesPerSecond;
1189 c->frameRandomStart = randomStart < 0.f ? 0.f : (randomStart > 1.f ? 1.f : randomStart);
1190}
1191
1192void ParticleEmitter::clearColorGradient() { config()->colorGradient.clear(); }
1193
1194void ParticleEmitter::addColorStop(float t, float r, float g, float b, float a) {
1195 config()->colorGradient.add(t, r, g, b, a);
1196}
1197
1198void ParticleEmitter::clearSizeCurve() { config()->sizeCurve.clear(); }
1199
1200void ParticleEmitter::addSizeCurvePoint(float t, float v) { config()->sizeCurve.add(t, v); }
1201
1202void ParticleEmitter::clearRotationCurve() { config()->rotationCurve.clear(); }
1203
1205 config()->rotationCurve.add(t, v);
1206}
1207
1208void ParticleEmitter::setColorStart(float r, float g, float b, float a) {
1209 config()->colorStart = Color(r, g, b, a);
1210}
1211
1212void ParticleEmitter::setColorEnd(float r, float g, float b, float a) {
1213 config()->colorEnd = Color(r, g, b, a);
1214}
1215
1216void ParticleEmitter::setTexture(graphics::Texture *texture) { draw()->texture = texture; }
1218
1219void ParticleEmitter::setCanvas(graphics::Canvas *canvas) { draw()->canvas = canvas; }
1220void ParticleEmitter::setCamera(graphics::Camera2D *camera) { draw()->camera = camera; }
1221
1222void ParticleEmitter::setLayer(int layer) { draw()->layer = layer; }
1223int ParticleEmitter::getLayer() { return draw()->layer; }
1224
1225void ParticleEmitter::setVisible(bool visible) { draw()->visible = visible; }
1226bool ParticleEmitter::isVisible() { return draw()->visible; }
1227
1229 auto s = sim();
1230 s->active = true;
1231 s->paused = false;
1232 s->emitterAge = 0.f;
1233 for (auto &b : config()->bursts) b.emitted = false;
1234 const float prewarm = config()->prewarmSeconds;
1235 if (prewarm > 0.f) {
1236 constexpr float kPrewarmDt = 1.f / 60.f;
1237 const int steps = int(std::ceil(prewarm / kPrewarmDt));
1238 for (int i = 0; i < steps; ++i) stepEmitterSim(*config(), *s, kPrewarmDt);
1239 }
1240}
1241
1243 auto s = sim();
1244 s->active = false;
1245 s->paused = false;
1246 s->emitAccum = 0.f;
1247 s->emitterAge = 0.f;
1248 s->lastX = config()->x;
1249 s->lastY = config()->y;
1250 s->hasLastPos = true;
1251}
1252
1254 auto s = sim();
1255 if (s->active) s->paused = true;
1256}
1257
1259 auto s = sim();
1260 s->alive = 0;
1261 s->emitAccum = 0.f;
1262 s->emitterAge = 0.f;
1263 s->hasLastPos = false;
1264 s->overflowWarned = false;
1265 for (auto &b : config()->bursts) b.emitted = false;
1266 for (auto &p : s->particles) p.life = 0.f;
1267 auto g = gpuSim();
1268 if (g->buffer) {
1269 try {
1270 g->buffer->fillFloat32(0.f);
1271 } catch (...) {
1272 }
1273 }
1274 if (!g->mirror.empty()) std::fill(g->mirror.begin(), g->mirror.end(), 0.f);
1275}
1276
1277void ParticleEmitter::emit(int count) {
1278 if (count <= 0) return;
1279 auto c = config();
1280 auto s = sim();
1281 // Spawn at the CURRENT attached position: refresh bone/skin sync so a
1282 // script that moves the pose then emits gets the new origin (no one-frame
1283 // lag). No-op for unattached emitters.
1284 syncAttach();
1285 for (int i = 0; i < count; ++i) spawnParticle(*c, *s);
1286}
1287
1289 auto s = sim();
1290 return s->active && !s->paused;
1291}
1292bool ParticleEmitter::isPaused() { return sim()->paused; }
1293bool ParticleEmitter::isStopped() { return !sim()->active; }
1294
1295int ParticleEmitter::getCount() { return sim()->alive; }
1296int ParticleEmitter::getBufferSize() { return int(sim()->particles.size()); }
1297
1298void ParticleEmitter::applyPreset(const std::string &name) {
1299 if (name == "spark") {
1300 setEmissionRate(80.f);
1301 setParticleLifetime(0.2f, 0.6f);
1302 setEmitterLifetime(-1.f);
1303 setDirection(-kPi * 0.5f);
1304 setSpread(kPi * 0.6f);
1305 setSpeed(60.f, 180.f);
1306 setLinearAcceleration(-20.f, 40.f, 20.f, 120.f);
1307 setRadialAcceleration(0.f, 0.f);
1308 setTangentialAcceleration(0.f, 0.f);
1309 setEmissionArea("none", 0.f, 0.f);
1310 setParticleSize(4.f, 4.f);
1311 setSizes(1.f, 0.2f);
1312 setSizeVariation(0.3f);
1313 setSpin(-8.f, 8.f);
1314 setColorStart(1.f, 0.9f, 0.3f, 1.f);
1315 setColorEnd(1.f, 0.2f, 0.f, 0.f);
1316 } else if (name == "smoke") {
1317 setEmissionRate(25.f);
1318 setParticleLifetime(1.5f, 3.f);
1319 setEmitterLifetime(-1.f);
1320 setDirection(-kPi * 0.5f);
1321 setSpread(0.4f);
1322 setSpeed(10.f, 40.f);
1323 setLinearAcceleration(-5.f, -30.f, 5.f, -10.f);
1324 setRadialAcceleration(-5.f, 5.f);
1325 setTangentialAcceleration(-10.f, 10.f);
1326 setEmissionArea("ellipse", 12.f, 4.f);
1327 setParticleSize(16.f, 16.f);
1328 setSizes(0.5f, 2.f);
1329 setSizeVariation(0.4f);
1330 setSpin(-1.f, 1.f);
1331 setColorStart(0.5f, 0.5f, 0.5f, 0.5f);
1332 setColorEnd(0.3f, 0.3f, 0.3f, 0.f);
1333 } else if (name == "fire") {
1334 setEmissionRate(60.f);
1335 setParticleLifetime(0.4f, 1.0f);
1336 setEmitterLifetime(-1.f);
1337 setDirection(-kPi * 0.5f);
1338 setSpread(0.5f);
1339 setSpeed(20.f, 80.f);
1340 setLinearAcceleration(-15.f, -80.f, 15.f, -20.f);
1341 setRadialAcceleration(-20.f, 10.f);
1342 setTangentialAcceleration(-30.f, 30.f);
1343 setEmissionArea("ellipse", 20.f, 8.f);
1344 setParticleSize(10.f, 10.f);
1345 setSizes(1.2f, 0.3f);
1346 setSizeVariation(0.25f);
1347 setSpin(-2.f, 2.f);
1348 setColorStart(1.f, 0.7f, 0.1f, 1.f);
1349 setColorEnd(1.f, 0.1f, 0.f, 0.f);
1350 }
1351}
1352
1353bool ParticleEmitter::applyConfig(const std::string &json) {
1354 return applyConfigText(this, json, nullptr);
1355}
1356
1357bool ParticleEmitter::loadConfig(const std::string &path) {
1358 return loadConfigFile(this, path, nullptr);
1359}
1360
1361bool ParticleEmitter::reloadConfig() { return reloadConfigFile(this, nullptr); }
1362
1363void ParticleEmitter::setAutoReload(bool enable) { resource()->autoReload = enable; }
1364bool ParticleEmitter::getAutoReload() { return resource()->autoReload; }
1365std::string ParticleEmitter::getConfigPath() { return resource()->path; }
1366
1368 auto a = attach();
1369 clearAttachSources(*a);
1370 a->kind = Attach::Kind::AnimPose;
1371 a->pose = pose;
1372 a->boneIndex = boneIndex;
1373 a->enabled = pose != nullptr && boneIndex >= 0;
1374 if (!a->enabled) a->kind = Attach::Kind::None;
1375 if (a->enabled) syncAttach();
1376}
1377
1379 animation::AnimSkeleton *skeleton,
1380 const std::string &boneName) {
1381 auto a = attach();
1382 a->skeleton = skeleton;
1383 int idx = -1;
1384 if (skeleton) idx = skeleton->findBone(boneName);
1385 attachToBone(pose, idx);
1386 // Preserve skeleton pointer for name lookups after attachToBone cleared sources.
1387 attach()->skeleton = skeleton;
1388}
1389
1391 auto a = attach();
1392 clearAttachSources(*a);
1393 a->kind = Attach::Kind::Spine;
1394 a->spine = spine;
1395 a->boneIndex = boneIndex;
1396 a->enabled = spine != nullptr && boneIndex >= 0 && boneIndex < spine->getBoneCount();
1397 if (!a->enabled) a->kind = Attach::Kind::None;
1398 if (a->enabled) syncAttach();
1399}
1400
1402 const std::string &boneName) {
1403 int idx = -1;
1404 if (spine && spine->getData()) idx = spine->getData()->findBone(boneName);
1405 attachToSpineBone(spine, idx);
1406}
1407
1409 auto a = attach();
1410 clearAttachSources(*a);
1411 a->kind = Attach::Kind::Ik2D;
1412 a->ik2d = skeleton;
1413 a->boneIndex = boneId;
1414 a->enabled = skeleton != nullptr && boneId >= 0 && boneId < skeleton->getBoneCount();
1415 if (!a->enabled) a->kind = Attach::Kind::None;
1416 if (a->enabled) syncAttach();
1417}
1418
1420 auto a = attach();
1421 clearAttachSources(*a);
1422 a->kind = Attach::Kind::Ik3D;
1423 a->ik3d = skeleton;
1424 a->boneIndex = boneId;
1425 a->enabled = skeleton != nullptr && boneId >= 0 && boneId < skeleton->getBoneCount();
1426 if (!a->enabled) a->kind = Attach::Kind::None;
1427 if (a->enabled) syncAttach();
1428}
1429
1430void ParticleEmitter::setAttachOffset(float x, float y, float z) {
1431 auto a = attach();
1432 a->offsetX = x;
1433 a->offsetY = y;
1434 a->offsetZ = z;
1435 if (a->enabled) syncAttach();
1436}
1437
1438void ParticleEmitter::setAttachPlane(const std::string &plane) {
1439 attach()->plane = normalizePlane(plane);
1440 if (attach()->enabled) syncAttach();
1441}
1442
1444 attach()->scale = scale;
1445 if (attach()->enabled) syncAttach();
1446}
1447
1449 attach()->followRotation = enable;
1450 if (attach()->enabled) syncAttach();
1451}
1452
1454 clearAttachSources(*attach());
1455}
1456
1457bool ParticleEmitter::isAttached() { return attach()->enabled; }
1458int ParticleEmitter::getAttachBone() { return attach()->boneIndex; }
1459
1461 switch (attach()->kind) {
1463 return "anim";
1465 return "spine";
1466 case Attach::Kind::Ik2D:
1467 return "ik2d";
1468 case Attach::Kind::Ik3D:
1469 return "ik3d";
1470 case Attach::Kind::None:
1471 default:
1472 return "none";
1473 }
1474}
1475
1477 syncEmitterSources(*config(), *sim(), *attach(), *skinSource());
1478}
1479
1481 auto s = skinSource();
1482 s->skin = skin;
1483 s->pose = pose;
1484 s->enabled = skin != nullptr && pose != nullptr;
1485 s->candidatesDirty = true;
1486 s->lastSkinnedFrame = -1;
1487}
1488
1489void ParticleEmitter::setSkinBoneFilter(int skeletonBoneIndex, float minWeight) {
1490 auto s = skinSource();
1491 s->filterBone = skeletonBoneIndex;
1492 s->minWeight = minWeight < 0.f ? 0.f : minWeight;
1493 s->candidatesDirty = true;
1494}
1495
1497 const std::string &boneName, float minWeight) {
1498 auto s = skinSource();
1499 s->skeleton = skeleton;
1500 int idx = -1;
1501 if (skeleton) idx = skeleton->findBone(boneName);
1502 setSkinBoneFilter(idx, minWeight);
1503}
1504
1505void ParticleEmitter::setSkinPlane(const std::string &plane) {
1506 skinSource()->plane = normalizePlane(plane);
1507}
1508
1509void ParticleEmitter::setSkinScale(float scale) { skinSource()->scale = scale; }
1510
1512 auto s = skinSource();
1513 s->enabled = false;
1514 s->skin = nullptr;
1515 s->pose = nullptr;
1516 s->filterBone = -1;
1517 s->candidates.clear();
1518 s->candidatesDirty = true;
1519}
1520
1521bool ParticleEmitter::hasSkinSource() { return skinSource()->enabled; }
1522
1524 if (count <= 0) return;
1525 auto s = skinSource();
1526 if (!s->enabled) return;
1527 auto c = config();
1528 auto simc = sim();
1529 for (int i = 0; i < count; ++i) {
1530 float sx = c->x, sy = c->y;
1531 if (!sampleSkinSpawn(*s, *simc, sx, sy)) break;
1532 spawnParticleAt(*c, *simc, sx, sy);
1533 }
1534}
1535
1536} // namespace eve::particles
Tok kind
std::string type
gpgpu::Gpgpu * gpgpu
int y
Definition Grass.cpp:135
int z
Definition Grass.cpp:135
float height
Definition Grass.cpp:235
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
uint32_t a
uint32_t b
uint32_t c
int width
TileLayer * layer
int idx
float f
glm::vec4 p[6]
Shader * shader
const char * name
Definition RockMesh.cpp:21
SettlementPipeline::Stage fn
bool enabled
int d
int v
float scale
Definition TreeMesh.cpp:122
float m[16]
uint32_t s
Definition Weather.cpp:28
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
Definition AnimPose.h:15
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
int findBone(const std::string &name) const
CPU linear-blend skinning binding for one mesh against an AnimSkeleton.
Definition AnimSkin.h:25
float getSkinnedPositionX(int vertexIndex) const
Cached skinned position component (requires updateSkinnedPositions).
Definition AnimSkin.cpp:266
float getSkinnedPositionZ(int vertexIndex) const
Definition AnimSkin.cpp:276
float getSkinnedPositionY(int vertexIndex) const
Definition AnimSkin.cpp:271
int findBone(const std::string &name) const
Runtime Spine skeleton pose (local + world bone transforms, slot attachments). Script type: SpineSkel...
SpineSkeletonData * getData() const
Backend-agnostic compute program. Bind storage buffers then dispatch via Gpgpu::dispatch....
GPGPU module — compute shaders + storage buffers via the active Graphics backend. Uses the graphics q...
Definition Gpgpu.h:20
Declarative 2D camera (viewport center + zoom).
Custom GPU program.
Definition Shader.h:30
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
Script-facing 2D skeleton + pose state (ik::skeleton2d + ik::ecs2d). Bone indices are stable after ea...
Definition Skeleton2D.h:12
int getBoneCount() const
Script-facing 3D skeleton + pose state (ik::skeleton3d + ik::ecs3d). Local angles are yaw/pitch in th...
Definition Skeleton3D.h:11
int getBoneCount() const
float sample(float t, float fallback) const
Sample at normalized t; fallback is returned when the curve is empty.
ECS emitter entity. Script configures components; ParticleSimSystem / ParticleRenderSystem drive per-...
void applyPreset(const std::string &name)
Named preset: "spark" / "smoke" / "fire". Unknown → no-op.
void addSizeCurvePoint(float t, float v)
void setFlipbook(int hframes, int vframes, float framesPerSecond=0.f, float randomStart=0.f)
void addColorStop(float t, float r, float g, float b, float a)
void setSkinBoneFilter(int skeletonBoneIndex, float minWeight=0.f)
void addVelocityCurvePoint(float t, float v)
void attachToSkeleton2D(eve::ik::Skeleton2D *skeleton, int boneId)
void attachToSkeleton3D(eve::ik::Skeleton3D *skeleton, int boneId)
void setSpin(float minSpin, float maxSpin)
void setTexture(graphics::Texture *texture)
void setPosition(float x, float y)
void setRenderMode(const std::string &mode, float stretchFactor=1.f)
void attachToSpineBone(animation::SpineSkeleton *spine, int boneIndex)
static ParticleEmitter * createEmitter(int bufferSize=1000)
void setSkinBoneFilterByName(animation::AnimSkeleton *skeleton, const std::string &boneName, float minWeight=0.f)
bool loadConfig(const std::string &path)
void setCollisionBounds(bool enabled, float minX, float minY, float maxX, float maxY)
std::string getAttachKind()
"none" | "anim" | "spine" | "ik2d" | "ik3d"
void setSkinPlane(const std::string &plane)
void setSizes(float startScale, float endScale)
void setRadialAcceleration(float minA, float maxA)
void setSpeed(float minSpeed, float maxSpeed)
void setBlendMode(const std::string &mode)
void addForceField(float x, float y, float radius, float strength, float falloff=1.f)
void setCanvas(graphics::Canvas *canvas)
void attachToBone(animation::AnimPose *pose, int boneIndex)
void setOverflowMode(const std::string &mode)
void setStartRotation(float minDeg, float maxDeg)
void setCamera(graphics::Camera2D *camera)
void setLinearAcceleration(float xmin, float ymin, float xmax, float ymax)
void emitFromSkin(int count)
Burst-emit count particles from the current skinned surface.
void syncAttach()
Sync Config.x/y (and direction) from the attached bone. Also called by ParticleSimSystem.
void setTangentialAcceleration(float minA, float maxA)
bool applyConfig(const std::string &json)
void setSkinSource(animation::AnimSkin *skin, animation::AnimPose *pose)
void addRotationCurvePoint(float t, float v)
void setCollision(const std::string &mode, float radius=0.f, float restitution=0.6f, float lifetimeLoss=0.f)
void addSubEmitter(ParticleEmitter *target, const std::string &trigger, float inheritVelocity=0.f)
void setColorEnd(float r, float g, float b, float a=1.f)
void setEmissionArea(const std::string &type, float x, float y)
void setColorStart(float r, float g, float b, float a=1.f)
void setNoise(float strength, float frequency=1.f, float speed=1.f)
void attachToSpineBoneByName(animation::SpineSkeleton *spine, const std::string &boneName)
void setShader(graphics::Shader *shader)
void setSizeVariation(float variation)
void attachToBoneByName(animation::AnimPose *pose, animation::AnimSkeleton *skeleton, const std::string &boneName)
void setSimulationSpace(const std::string &space)
void setAttachOffset(float x, float y, float z)
void setLights(bool enabled, float radius=120.f, float intensity=1.f, float r=1.f, float g=1.f, float b=1.f, int maxLights=4)
void setParticleLifetime(float minLife, float maxLife)
void addBurst(float time, int count)
void setAttachPlane(const std::string &plane)
void setInheritVelocity(float fraction)
void setParticleSize(float width, float height)
const char * kParticleGpuKernel
GLSL compute kernel for GPU-accelerated particle integration.
bool stepEmitterSimGpu(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, ParticleEmitter::GpuSim &gpu, float dt)
GPU-accelerated integration step; false = unavailable, caller falls back to CPU.
void spawnParticleAt(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, float x, float y)
void setWorldCollisionResolver(WorldCollisionFn fn)
bool loadConfigFile(ParticleEmitter *emitter, const std::string &path, std::string *error)
Read path via Filesystem, apply, and bind Resource.path + modtime for hot reload. Returns false if fi...
bool(*)(float x, float y, float radius, float &nx, float &ny) WorldCollisionFn
World collision query used by emitters with worldCollision enabled.
bool reloadConfigFile(ParticleEmitter *emitter, std::string *error)
Re-read Resource.path if set; updates modtime.
void spawnParticle(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim)
void stepEmitterSim(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, float dt)
bool applyConfigText(ParticleEmitter *emitter, const std::string &json, std::string *error)
Parse JSON text and apply.
WorldCollisionFn getWorldCollisionResolver()
bool sampleSkinSpawn(ParticleEmitter::SkinSource &skinSrc, ParticleEmitter::Sim &sim, float &outX, float &outY)
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.
static Mat4 fromTRS(const TransformTRS &t)
Definition AnimMath.h:102
void transformPoint(float x, float y, float z, float &ox, float &oy, float &oz) const
Definition AnimMath.h:142
Optional bone attachment. When enabled, syncAttach() writes Config.x/y (and optionally direction) fro...
Timed burst emission (fired once while the emitter is active).
Radial attract/repel force fields (strength > 0 attract, < 0 repel).
Script-linked sub-emitters (birth / death / collision triggers).
float limitVelocity
Max speed; 0 = unlimited. Applied after forces each step.
float maxDeltaTime
Cap per-step delta time (0 = unlimited).
float noiseStrength
Turbulence: random per-particle acceleration scaled by strength.
std::string simSpace
"world" (default) or "local" (particles track the emitter).
std::string overflowMode
Buffer-full strategy: "drop" (default) | "pause" | "warn".
ParticleCurve velocityCurve
Optional speed multiplier curve over lifetime.
float inheritVelocity
Fraction [0,1] of the emitter's current velocity added to new particles.
float damping
Per-second velocity damping fraction in [0,1].
float gravityX
Gravity applied every step (world units/s²).
bool worldCollision
Query the engine-level world collision resolver each step.
std::string collisionMode
"none" | "kill" | "bounce" | "stop" on collision.
GPU-accelerated simulation state (see ParticleGpuKernel.h for layout).
std::shared_ptr< eve::gpgpu::GpuBuffer > buffer
Optional skinned-mesh surface source. When enabled, newly spawned particles sample random (optionally...
std::vector< int > candidates
Vertex indices eligible for sampling (rebuilt when filter changes).
Single live particle (CPU simulation).