载入中...
搜索中...
未找到
ParticleConfig.cpp
浏览该文件的文档.
2
3#include "common/Module.h"
4#include "data/DataModule.h"
5#include "data/JsonDocument.h"
9#include "graphics/Graphics.h"
10
11#include <Poco/Dynamic/Var.h>
12#include <Poco/JSON/Array.h>
13#include <Poco/JSON/Object.h>
14
15#include <memory>
16
17namespace eve::particles {
18namespace {
19
20float asFloat(const Poco::Dynamic::Var &v, float fallback) {
21 try {
22 if (v.isEmpty()) return fallback;
23 return static_cast<float>(v.convert<double>());
24 } catch (...) {
25 return fallback;
26 }
27}
28
29bool asBool(const Poco::Dynamic::Var &v, bool fallback) {
30 try {
31 if (v.isEmpty()) return fallback;
32 return v.convert<bool>();
33 } catch (...) {
34 return fallback;
35 }
36}
37
38std::string asString(const Poco::Dynamic::Var &v) {
39 try {
40 if (v.isEmpty()) return {};
41 return v.convert<std::string>();
42 } catch (...) {
43 return {};
44 }
45}
46
47bool readVec2(Poco::JSON::Object::Ptr o, const char *key, float &a, float &b) {
48 if (!o || !o->has(key)) return false;
49 Poco::JSON::Array::Ptr arr;
50 try {
51 arr = o->getArray(key);
52 } catch (...) {
53 return false;
54 }
55 if (!arr || arr->size() < 2) return false;
56 a = asFloat(arr->get(0), a);
57 b = asFloat(arr->get(1), b);
58 return true;
59}
60
61bool readVec4(Poco::JSON::Object::Ptr o, const char *key, float &a, float &b, float &c, float &d) {
62 if (!o || !o->has(key)) return false;
63 Poco::JSON::Array::Ptr arr;
64 try {
65 arr = o->getArray(key);
66 } catch (...) {
67 return false;
68 }
69 if (!arr || arr->size() < 4) return false;
70 a = asFloat(arr->get(0), a);
71 b = asFloat(arr->get(1), b);
72 c = asFloat(arr->get(2), c);
73 d = asFloat(arr->get(3), d);
74 return true;
75}
76
77bool readVec3(Poco::JSON::Object::Ptr o, const char *key, float &a, float &b, float &c) {
78 if (!o || !o->has(key)) return false;
79 Poco::JSON::Array::Ptr arr;
80 try {
81 arr = o->getArray(key);
82 } catch (...) {
83 return false;
84 }
85 if (!arr || arr->size() < 3) return false;
86 a = asFloat(arr->get(0), a);
87 b = asFloat(arr->get(1), b);
88 c = asFloat(arr->get(2), c);
89 return true;
90}
91
92bool readCurveArray(Poco::JSON::Array::Ptr arr, ParticleCurve &curve) {
93 if (!arr) return false;
94 bool any = false;
95 for (size_t i = 0; i < arr->size(); ++i) {
96 try {
97 if (arr->isObject(int(i))) {
98 auto obj = arr->getObject(int(i));
99 if (obj) {
100 curve.add(asFloat(obj->get("t"), 0.f), asFloat(obj->get("v"), 0.f));
101 any = true;
102 }
103 } else if (arr->isArray(int(i))) {
104 auto sub = arr->getArray(int(i));
105 if (sub && sub->size() >= 2) {
106 curve.add(asFloat(sub->get(0), 0.f), asFloat(sub->get(1), 0.f));
107 any = true;
108 }
109 }
110 } catch (...) {
111 }
112 }
113 return any;
114}
115
116bool readGradientArray(Poco::JSON::Array::Ptr arr, ParticleGradient &gradient) {
117 if (!arr) return false;
118 bool any = false;
119 for (size_t i = 0; i < arr->size(); ++i) {
120 float t = 0.f, r = 1.f, g = 1.f, b = 1.f, a = 1.f;
121 try {
122 if (arr->isObject(int(i))) {
123 auto obj = arr->getObject(int(i));
124 if (obj) {
125 t = asFloat(obj->get("t"), t);
126 r = asFloat(obj->get("r"), r);
127 g = asFloat(obj->get("g"), g);
128 b = asFloat(obj->get("b"), b);
129 a = obj->has("a") ? asFloat(obj->get("a"), a) : a;
130 gradient.add(t, r, g, b, a);
131 any = true;
132 }
133 } else if (arr->isArray(int(i))) {
134 auto sub = arr->getArray(int(i));
135 if (sub && sub->size() >= 5) {
136 t = asFloat(sub->get(0), t);
137 r = asFloat(sub->get(1), r);
138 g = asFloat(sub->get(2), g);
139 b = asFloat(sub->get(3), b);
140 a = asFloat(sub->get(4), a);
141 gradient.add(t, r, g, b, a);
142 any = true;
143 }
144 }
145 } catch (...) {
146 }
147 }
148 return any;
149}
150
151void tryLoadTexture(ParticleEmitter *emitter, const std::string &path) {
152 if (path.empty()) return;
153 auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
154 if (!gfx) return;
155 try {
156 graphics::Texture *tex = gfx->newTextureFromFile(path);
157 emitter->setTexture(tex);
158 if (auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>("HotReload"))
159 hot->bind(path, "texture");
160 } catch (...) {
161 // Leave existing / null texture; config still applied.
162 }
163}
164
165int64_t fileModtime(const std::string &path) {
166 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
167 if (!fs) fs = eve::filesystem::Filesystem::create();
169 if (!fs->getInfo(path, info)) return -1;
170 return info.modtime;
171}
172
173} // namespace
174
176 if (!emitter || !doc || !doc->isObject()) return false;
177 auto obj = doc->object();
178 if (!obj) return false;
179
180 // Optional named preset first; later keys override.
181 if (obj->has("preset")) {
182 std::string preset = asString(obj->get("preset"));
183 if (!preset.empty()) emitter->applyPreset(preset);
184 }
185
186 if (obj->has("x") || obj->has("y")) {
187 float x = emitter->getX();
188 float y = emitter->getY();
189 if (obj->has("x")) x = asFloat(obj->get("x"), x);
190 if (obj->has("y")) y = asFloat(obj->get("y"), y);
191 emitter->setPosition(x, y);
192 }
193
194 if (obj->has("emissionRate"))
195 emitter->setEmissionRate(asFloat(obj->get("emissionRate"), emitter->getEmissionRate()));
196
197 float lifeMin = emitter->getParticleLifetimeMin();
198 float lifeMax = emitter->getParticleLifetimeMax();
199 if (readVec2(obj, "particleLifetime", lifeMin, lifeMax))
200 emitter->setParticleLifetime(lifeMin, lifeMax);
201 else {
202 if (obj->has("lifeMin")) lifeMin = asFloat(obj->get("lifeMin"), lifeMin);
203 if (obj->has("lifeMax")) lifeMax = asFloat(obj->get("lifeMax"), lifeMax);
204 if (obj->has("lifeMin") || obj->has("lifeMax"))
205 emitter->setParticleLifetime(lifeMin, lifeMax);
206 }
207
208 if (obj->has("emitterLife"))
209 emitter->setEmitterLifetime(asFloat(obj->get("emitterLife"), emitter->getEmitterLifetime()));
210
211 if (obj->has("direction"))
212 emitter->setDirection(asFloat(obj->get("direction"), emitter->getDirection()));
213 if (obj->has("spread"))
214 emitter->setSpread(asFloat(obj->get("spread"), emitter->getSpread()));
215
216 float speedMin = 0.f, speedMax = 0.f;
217 if (readVec2(obj, "speed", speedMin, speedMax))
218 emitter->setSpeed(speedMin, speedMax);
219
220 float ax0 = 0, ay0 = 0, ax1 = 0, ay1 = 0;
221 if (readVec4(obj, "linearAcceleration", ax0, ay0, ax1, ay1))
222 emitter->setLinearAcceleration(ax0, ay0, ax1, ay1);
223
224 float rad0 = 0, rad1 = 0;
225 if (readVec2(obj, "radialAcceleration", rad0, rad1))
226 emitter->setRadialAcceleration(rad0, rad1);
227
228 float tan0 = 0, tan1 = 0;
229 if (readVec2(obj, "tangentialAcceleration", tan0, tan1))
230 emitter->setTangentialAcceleration(tan0, tan1);
231
232 if (obj->has("emissionArea")) {
233 try {
234 auto area = obj->getObject("emissionArea");
235 if (area) {
236 std::string type = area->has("type") ? asString(area->get("type")) : "none";
237 float ax = area->has("x") ? asFloat(area->get("x"), 0.f) : 0.f;
238 float ay = area->has("y") ? asFloat(area->get("y"), 0.f) : 0.f;
239 emitter->setEmissionArea(type, ax, ay);
240 }
241 } catch (...) {
242 }
243 } else if (obj->has("areaType")) {
244 float ax = emitter->getEmissionAreaX();
245 float ay = emitter->getEmissionAreaY();
246 readVec2(obj, "areaSize", ax, ay);
247 emitter->setEmissionArea(asString(obj->get("areaType")), ax, ay);
248 }
249
250 float pw = emitter->getParticleWidth(), ph = emitter->getParticleHeight();
251 if (readVec2(obj, "particleSize", pw, ph))
252 emitter->setParticleSize(pw, ph);
253
254 float ss = 1.f, se = 1.f;
255 if (readVec2(obj, "sizes", ss, se))
256 emitter->setSizes(ss, se);
257
258 if (obj->has("sizeVariation"))
259 emitter->setSizeVariation(
260 asFloat(obj->get("sizeVariation"), emitter->getSizeVariation()));
261
262 float spin0 = 0, spin1 = 0;
263 if (readVec2(obj, "spin", spin0, spin1)) emitter->setSpin(spin0, spin1);
264
265 float sr0 = 0, sr1 = 0;
266 if (readVec2(obj, "startRotation", sr0, sr1))
267 emitter->setStartRotation(sr0, sr1);
268
269 if (obj->has("bursts")) {
270 try {
271 auto arr = obj->getArray("bursts");
272 if (arr) {
273 emitter->clearBursts();
274 for (size_t i = 0; i < arr->size(); ++i) {
275 float t = 0.f;
276 int count = 0;
277 if (arr->isObject(int(i))) {
278 auto b = arr->getObject(int(i));
279 if (!b) continue;
280 t = asFloat(b->get("time"), 0.f);
281 count = int(asFloat(b->get("count"), 0.f));
282 } else if (arr->isArray(int(i))) {
283 auto sub = arr->getArray(int(i));
284 if (!sub || sub->size() < 2) continue;
285 t = asFloat(sub->get(0), 0.f);
286 count = int(asFloat(sub->get(1), 0.f));
287 }
288 if (count > 0) emitter->addBurst(t, count);
289 }
290 }
291 } catch (...) {
292 }
293 }
294
295 if (obj->has("prewarm"))
296 emitter->setPrewarm(asFloat(obj->get("prewarm"), 0.f));
297
298 float gx = 0, gy = 0;
299 if (readVec2(obj, "gravity", gx, gy)) emitter->setGravity(gx, gy);
300 if (obj->has("damping"))
301 emitter->setDamping(asFloat(obj->get("damping"), 0.f));
302 if (obj->has("limitVelocity"))
303 emitter->setLimitVelocity(asFloat(obj->get("limitVelocity"), 0.f));
304 if (obj->has("velocityOverLifetime")) {
305 try {
306 auto arr = obj->getArray("velocityOverLifetime");
307 if (arr) {
308 emitter->clearVelocityCurve();
309 readCurveArray(arr, emitter->config()->velocityCurve);
310 }
311 } catch (...) {
312 }
313 }
314 if (obj->has("inheritVelocity"))
315 emitter->setInheritVelocity(asFloat(obj->get("inheritVelocity"), 0.f));
316 if (obj->has("simulationSpace"))
317 emitter->setSimulationSpace(asString(obj->get("simulationSpace")));
318
319 if (obj->has("noise")) {
320 try {
321 auto n = obj->getObject("noise");
322 if (n) {
323 float strength = n->has("strength") ? asFloat(n->get("strength"), 0.f)
324 : emitter->config()->noiseStrength;
325 float freq = n->has("frequency") ? asFloat(n->get("frequency"), 1.f)
326 : emitter->config()->noiseFrequency;
327 float speed = n->has("speed") ? asFloat(n->get("speed"), 1.f)
328 : emitter->config()->noiseSpeed;
329 emitter->setNoise(strength, freq, speed);
330 }
331 } catch (...) {
332 }
333 } else if (obj->has("noiseStrength")) {
334 emitter->setNoise(asFloat(obj->get("noiseStrength"), 0.f),
335 obj->has("noiseFrequency") ? asFloat(obj->get("noiseFrequency"), 1.f)
336 : emitter->config()->noiseFrequency,
337 obj->has("noiseSpeed") ? asFloat(obj->get("noiseSpeed"), 1.f)
338 : emitter->config()->noiseSpeed);
339 }
340
341 if (obj->has("collision")) {
342 try {
343 auto col = obj->getObject("collision");
344 if (col) {
345 std::string mode = col->has("mode") ? asString(col->get("mode")) : "none";
346 float radius = col->has("radius") ? asFloat(col->get("radius"), 0.f)
347 : emitter->config()->collisionRadius;
348 float restitution =
349 col->has("restitution") ? asFloat(col->get("restitution"), 0.6f)
350 : emitter->config()->collisionRestitution;
351 float loss = col->has("lifetimeLoss")
352 ? asFloat(col->get("lifetimeLoss"), 0.f)
353 : emitter->config()->collisionLifetimeLoss;
354 emitter->setCollision(mode, radius, restitution, loss);
355 }
356 } catch (...) {
357 }
358 }
359 if (obj->has("collisionBounds")) {
360 try {
361 auto cb = obj->getObject("collisionBounds");
362 if (cb) {
363 bool enabled = cb->has("enabled") ? asBool(cb->get("enabled"), false)
364 : emitter->config()->collisionBoundsEnabled;
365 float minX = cb->has("minX") ? asFloat(cb->get("minX"), 0.f)
366 : emitter->config()->boundsMinX;
367 float minY = cb->has("minY") ? asFloat(cb->get("minY"), 0.f)
368 : emitter->config()->boundsMinY;
369 float maxX = cb->has("maxX") ? asFloat(cb->get("maxX"), 0.f)
370 : emitter->config()->boundsMaxX;
371 float maxY = cb->has("maxY") ? asFloat(cb->get("maxY"), 0.f)
372 : emitter->config()->boundsMaxY;
373 emitter->setCollisionBounds(enabled, minX, minY, maxX, maxY);
374 }
375 } catch (...) {
376 }
377 }
378 if (obj->has("worldCollision"))
379 emitter->setWorldCollision(asBool(obj->get("worldCollision"), false));
380
381 if (obj->has("renderMode")) {
382 float stretch = obj->has("stretch") ? asFloat(obj->get("stretch"), 1.f)
383 : emitter->config()->stretchFactor;
384 emitter->setRenderMode(asString(obj->get("renderMode")), stretch);
385 } else if (obj->has("stretch")) {
386 emitter->setRenderMode("stretched", asFloat(obj->get("stretch"), 1.f));
387 }
388 if (obj->has("overflowMode"))
389 emitter->setOverflowMode(asString(obj->get("overflowMode")));
390 if (obj->has("maxDeltaTime"))
391 emitter->setMaxDeltaTime(asFloat(obj->get("maxDeltaTime"), 0.f));
392 if (obj->has("gpuSimulation"))
393 emitter->setGpuSimulation(asBool(obj->get("gpuSimulation"), false));
394
395 if (obj->has("forceFields")) {
396 try {
397 auto arr = obj->getArray("forceFields");
398 if (arr) {
399 emitter->clearForceFields();
400 for (size_t i = 0; i < arr->size(); ++i) {
401 if (!arr->isObject(int(i))) continue;
402 auto f = arr->getObject(int(i));
403 if (!f) continue;
404 float x = asFloat(f->get("x"), 0.f);
405 float y = asFloat(f->get("y"), 0.f);
406 float radius = asFloat(f->get("radius"), 0.f);
407 float strength = asFloat(f->get("strength"), 0.f);
408 float falloff = f->has("falloff") ? asFloat(f->get("falloff"), 1.f) : 1.f;
409 emitter->addForceField(x, y, radius, strength, falloff);
410 }
411 }
412 } catch (...) {
413 }
414 }
415
416 if (obj->has("lights")) {
417 try {
418 auto lg = obj->getObject("lights");
419 if (lg) {
420 bool enabled = lg->has("enabled") ? asBool(lg->get("enabled"), false)
421 : emitter->config()->lights.enabled;
422 float radius = lg->has("radius") ? asFloat(lg->get("radius"), 120.f)
423 : emitter->config()->lights.radius;
424 float intensity = lg->has("intensity") ? asFloat(lg->get("intensity"), 1.f)
425 : emitter->config()->lights.intensity;
426 float lr = emitter->config()->lights.r;
427 float lg2 = emitter->config()->lights.g;
428 float lb = emitter->config()->lights.b;
429 if (readVec3(lg, "color", lr, lg2, lb)) {
430 }
431 if (lg->has("r")) lr = asFloat(lg->get("r"), lr);
432 if (lg->has("g")) lg2 = asFloat(lg->get("g"), lg2);
433 if (lg->has("b")) lb = asFloat(lg->get("b"), lb);
434 int maxL = lg->has("max") ? int(asFloat(lg->get("max"), 4.f))
435 : emitter->config()->lights.max;
436 emitter->setLights(enabled, radius, intensity, lr, lg2, lb, maxL);
437 }
438 } catch (...) {
439 }
440 }
441
442 float r = 1, g = 1, b = 1, a = 1;
443 if (readVec4(obj, "colorStart", r, g, b, a))
444 emitter->setColorStart(r, g, b, a);
445 if (readVec4(obj, "colorEnd", r, g, b, a))
446 emitter->setColorEnd(r, g, b, a);
447
448 if (obj->has("colorOverLifetime")) {
449 try {
450 auto arr = obj->getArray("colorOverLifetime");
451 if (arr) {
452 emitter->clearColorGradient();
453 readGradientArray(arr, emitter->config()->colorGradient);
454 }
455 } catch (...) {
456 }
457 }
458
459 if (obj->has("sizeOverLifetime")) {
460 try {
461 auto arr = obj->getArray("sizeOverLifetime");
462 if (arr) {
463 emitter->clearSizeCurve();
464 readCurveArray(arr, emitter->config()->sizeCurve);
465 }
466 } catch (...) {
467 }
468 }
469
470 if (obj->has("rotationOverLifetime")) {
471 try {
472 auto arr = obj->getArray("rotationOverLifetime");
473 if (arr) {
474 emitter->clearRotationCurve();
475 readCurveArray(arr, emitter->config()->rotationCurve);
476 }
477 } catch (...) {
478 }
479 }
480
481 if (obj->has("blendMode"))
482 emitter->setBlendMode(asString(obj->get("blendMode")));
483
484 if (obj->has("flipbook")) {
485 try {
486 auto fb = obj->getObject("flipbook");
487 if (fb) {
488 auto c = emitter->config();
489 int h = fb->has("hframes") ? int(asFloat(fb->get("hframes"), 1.f)) : c->hframes;
490 int v = fb->has("vframes") ? int(asFloat(fb->get("vframes"), 1.f)) : c->vframes;
491 float rate = fb->has("frameRate") ? asFloat(fb->get("frameRate"), 0.f)
492 : c->frameRate;
493 float rs = fb->has("frameRandomStart")
494 ? asFloat(fb->get("frameRandomStart"), 0.f)
495 : c->frameRandomStart;
496 emitter->setFlipbook(h, v, rate, rs);
497 }
498 } catch (...) {
499 }
500 } else if (obj->has("hframes") || obj->has("vframes") || obj->has("frameRate") ||
501 obj->has("frameRandomStart")) {
502 auto c = emitter->config();
503 int h = obj->has("hframes") ? int(asFloat(obj->get("hframes"), 1.f)) : c->hframes;
504 int v = obj->has("vframes") ? int(asFloat(obj->get("vframes"), 1.f)) : c->vframes;
505 float rate = obj->has("frameRate") ? asFloat(obj->get("frameRate"), 0.f) : c->frameRate;
506 float rs = obj->has("frameRandomStart") ? asFloat(obj->get("frameRandomStart"), 0.f)
507 : c->frameRandomStart;
508 emitter->setFlipbook(h, v, rate, rs);
509 }
510
511 if (obj->has("layer"))
512 emitter->setLayer(static_cast<int>(asFloat(obj->get("layer"), float(emitter->getLayer()))));
513 if (obj->has("visible"))
514 emitter->setVisible(asBool(obj->get("visible"), emitter->isVisible()));
515
516 if (obj->has("texture")) {
517 std::string texPath = asString(obj->get("texture"));
518 emitter->resource()->texturePath = texPath;
519 tryLoadTexture(emitter, texPath);
520 }
521
522 if (obj->has("autoReload"))
523 emitter->resource()->autoReload = asBool(obj->get("autoReload"), true);
524
525 if (obj->has("autoStart") && asBool(obj->get("autoStart"), false))
526 emitter->start();
527
528 return true;
529}
530
531bool applyConfigText(ParticleEmitter *emitter, const std::string &json, std::string *error) {
532 auto *dm = eve::data::DataModule::create();
533 std::string err;
534 std::unique_ptr<data::JsonDocument> doc(dm->decodeJson(json, &err));
535 if (!doc) {
536 if (error) *error = err.empty() ? "invalid json" : err;
537 return false;
538 }
539 if (!applyConfigDocument(emitter, doc.get())) {
540 if (error) *error = "config root must be object";
541 return false;
542 }
543 return true;
544}
545
546bool loadConfigFile(ParticleEmitter *emitter, const std::string &path, std::string *error) {
547 if (!emitter || path.empty()) {
548 if (error) *error = "empty path";
549 return false;
550 }
551 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
552 if (!fs) fs = eve::filesystem::Filesystem::create();
553
554 std::unique_ptr<eve::filesystem::FileData> data;
555 try {
556 data.reset(fs->read(path));
557 } catch (...) {
558 if (error) *error = "read failed: " + path;
559 return false;
560 }
561 if (!data || data->getSize() == 0) {
562 if (error) *error = "empty file: " + path;
563 return false;
564 }
565
566 std::string text(static_cast<const char *>(data->getData()), data->getSize());
567 if (!applyConfigText(emitter, text, error)) return false;
568
569 auto res = emitter->resource();
570 res->path = path;
571 res->modtime = fileModtime(path);
572 // Prefer OS watch; mtime remains a fallback in ParticleConfigSystem.
573 fs->watch(path);
574 if (auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>("HotReload"))
575 hot->bind(path, "particle");
576 return true;
577}
578
579bool reloadConfigFile(ParticleEmitter *emitter, std::string *error) {
580 if (!emitter) return false;
581 const std::string &path = emitter->resource()->path;
582 if (path.empty()) {
583 if (error) *error = "no config path";
584 return false;
585 }
586 return loadConfigFile(emitter, path, error);
587}
588
589} // namespace eve::particles
std::string type
int y
Definition Grass.cpp:135
int x
Definition Grass.cpp:135
float area
Definition Grass.cpp:63
glm::vec3 n
Definition Grass.cpp:64
int h
std::string error
uint32_t a
uint32_t b
uint32_t c
float f
Light2D::Data * data
bool enabled
int d
int v
Thin RAII wrapper over a Poco JSON value (object/array/scalar).
bool isObject() const
Root type predicates.
Poco::JSON::Object::Ptr object()
Root as JSON object/array (may be null when the type differs).
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
Multi-point scalar curve sampled with linear interpolation between points. Empty curve means "use the...
void add(float t, float v)
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 setFlipbook(int hframes, int vframes, float framesPerSecond=0.f, float randomStart=0.f)
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 setCollisionBounds(bool enabled, float minX, float minY, float maxX, float maxY)
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 setOverflowMode(const std::string &mode)
void setStartRotation(float minDeg, float maxDeg)
void setLinearAcceleration(float xmin, float ymin, float xmax, float ymax)
void setTangentialAcceleration(float minA, float maxA)
void setCollision(const std::string &mode, float radius=0.f, float restitution=0.6f, float lifetimeLoss=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 setSizeVariation(float variation)
void setSimulationSpace(const std::string &space)
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 setInheritVelocity(float fraction)
void setParticleSize(float width, float height)
Multi-stop color gradient sampled with linear interpolation between stops. Stops are kept sorted by t...
void add(float t, float r, float g, float b, float a)
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 reloadConfigFile(ParticleEmitter *emitter, std::string *error)
Re-read Resource.path if set; updates modtime.
bool applyConfigText(ParticleEmitter *emitter, const std::string &json, std::string *error)
Parse JSON text and apply.
bool applyConfigDocument(ParticleEmitter *emitter, data::JsonDocument *doc)
Apply particle JSON config onto an emitter (Config + Draw + Resource.texturePath)....