11#include <Poco/Dynamic/Var.h>
12#include <Poco/JSON/Array.h>
13#include <Poco/JSON/Object.h>
20float asFloat(
const Poco::Dynamic::Var &
v,
float fallback) {
22 if (
v.isEmpty())
return fallback;
23 return static_cast<float>(
v.convert<
double>());
29bool asBool(
const Poco::Dynamic::Var &
v,
bool fallback) {
31 if (
v.isEmpty())
return fallback;
32 return v.convert<
bool>();
38std::string asString(
const Poco::Dynamic::Var &
v) {
40 if (
v.isEmpty())
return {};
41 return v.convert<std::string>();
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;
51 arr = o->getArray(key);
55 if (!arr || arr->size() < 2)
return false;
56 a = asFloat(arr->get(0),
a);
57 b = asFloat(arr->get(1),
b);
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;
65 arr = o->getArray(key);
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);
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;
81 arr = o->getArray(key);
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);
92bool readCurveArray(Poco::JSON::Array::Ptr arr,
ParticleCurve &curve) {
93 if (!arr)
return false;
95 for (
size_t i = 0; i < arr->size(); ++i) {
97 if (arr->isObject(
int(i))) {
98 auto obj = arr->getObject(
int(i));
100 curve.
add(asFloat(obj->get(
"t"), 0.f), asFloat(obj->get(
"v"), 0.f));
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));
116bool readGradientArray(Poco::JSON::Array::Ptr arr,
ParticleGradient &gradient) {
117 if (!arr)
return 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;
122 if (arr->isObject(
int(i))) {
123 auto obj = arr->getObject(
int(i));
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);
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);
151void tryLoadTexture(
ParticleEmitter *emitter,
const std::string &path) {
152 if (path.empty())
return;
153 auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>(
"Graphics");
158 if (
auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
159 hot->bind(path,
"texture");
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;
176 if (!emitter || !doc || !doc->
isObject())
return false;
178 if (!obj)
return false;
181 if (obj->has(
"preset")) {
182 std::string preset = asString(obj->get(
"preset"));
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);
194 if (obj->has(
"emissionRate"))
199 if (readVec2(obj,
"particleLifetime", lifeMin, lifeMax))
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"))
208 if (obj->has(
"emitterLife"))
211 if (obj->has(
"direction"))
213 if (obj->has(
"spread"))
216 float speedMin = 0.f, speedMax = 0.f;
217 if (readVec2(obj,
"speed", speedMin, speedMax))
218 emitter->
setSpeed(speedMin, speedMax);
220 float ax0 = 0, ay0 = 0, ax1 = 0, ay1 = 0;
221 if (readVec4(obj,
"linearAcceleration", ax0, ay0, ax1, ay1))
224 float rad0 = 0, rad1 = 0;
225 if (readVec2(obj,
"radialAcceleration", rad0, rad1))
228 float tan0 = 0, tan1 = 0;
229 if (readVec2(obj,
"tangentialAcceleration", tan0, tan1))
232 if (obj->has(
"emissionArea")) {
234 auto area = obj->getObject(
"emissionArea");
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;
243 }
else if (obj->has(
"areaType")) {
246 readVec2(obj,
"areaSize", ax, ay);
251 if (readVec2(obj,
"particleSize", pw, ph))
254 float ss = 1.f, se = 1.f;
255 if (readVec2(obj,
"sizes", ss, se))
258 if (obj->has(
"sizeVariation"))
262 float spin0 = 0, spin1 = 0;
263 if (readVec2(obj,
"spin", spin0, spin1)) emitter->
setSpin(spin0, spin1);
265 float sr0 = 0, sr1 = 0;
266 if (readVec2(obj,
"startRotation", sr0, sr1))
269 if (obj->has(
"bursts")) {
271 auto arr = obj->getArray(
"bursts");
274 for (
size_t i = 0; i < arr->size(); ++i) {
277 if (arr->isObject(
int(i))) {
278 auto b = arr->getObject(
int(i));
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));
288 if (count > 0) emitter->
addBurst(t, count);
295 if (obj->has(
"prewarm"))
296 emitter->
setPrewarm(asFloat(obj->get(
"prewarm"), 0.f));
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"))
304 if (obj->has(
"velocityOverLifetime")) {
306 auto arr = obj->getArray(
"velocityOverLifetime");
309 readCurveArray(arr, emitter->config()->velocityCurve);
314 if (obj->has(
"inheritVelocity"))
316 if (obj->has(
"simulationSpace"))
319 if (obj->has(
"noise")) {
321 auto n = obj->getObject(
"noise");
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);
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);
341 if (obj->has(
"collision")) {
343 auto col = obj->getObject(
"collision");
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;
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;
359 if (obj->has(
"collisionBounds")) {
361 auto cb = obj->getObject(
"collisionBounds");
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;
378 if (obj->has(
"worldCollision"))
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));
388 if (obj->has(
"overflowMode"))
390 if (obj->has(
"maxDeltaTime"))
392 if (obj->has(
"gpuSimulation"))
395 if (obj->has(
"forceFields")) {
397 auto arr = obj->getArray(
"forceFields");
400 for (
size_t i = 0; i < arr->size(); ++i) {
401 if (!arr->isObject(
int(i)))
continue;
402 auto f = arr->getObject(
int(i));
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;
416 if (obj->has(
"lights")) {
418 auto lg = obj->getObject(
"lights");
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)) {
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;
442 float r = 1, g = 1,
b = 1,
a = 1;
443 if (readVec4(obj,
"colorStart", r, g,
b,
a))
445 if (readVec4(obj,
"colorEnd", r, g,
b,
a))
448 if (obj->has(
"colorOverLifetime")) {
450 auto arr = obj->getArray(
"colorOverLifetime");
453 readGradientArray(arr, emitter->config()->colorGradient);
459 if (obj->has(
"sizeOverLifetime")) {
461 auto arr = obj->getArray(
"sizeOverLifetime");
464 readCurveArray(arr, emitter->config()->sizeCurve);
470 if (obj->has(
"rotationOverLifetime")) {
472 auto arr = obj->getArray(
"rotationOverLifetime");
475 readCurveArray(arr, emitter->config()->rotationCurve);
481 if (obj->has(
"blendMode"))
484 if (obj->has(
"flipbook")) {
486 auto fb = obj->getObject(
"flipbook");
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)
493 float rs = fb->has(
"frameRandomStart")
494 ? asFloat(fb->get(
"frameRandomStart"), 0.f)
495 :
c->frameRandomStart;
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;
511 if (obj->has(
"layer"))
512 emitter->
setLayer(
static_cast<int>(asFloat(obj->get(
"layer"),
float(emitter->
getLayer()))));
513 if (obj->has(
"visible"))
516 if (obj->has(
"texture")) {
517 std::string texPath = asString(obj->get(
"texture"));
518 emitter->resource()->texturePath = texPath;
519 tryLoadTexture(emitter, texPath);
522 if (obj->has(
"autoReload"))
523 emitter->resource()->autoReload = asBool(obj->get(
"autoReload"),
true);
525 if (obj->has(
"autoStart") && asBool(obj->get(
"autoStart"),
false))
532 auto *dm = eve::data::DataModule::create();
534 std::unique_ptr<data::JsonDocument> doc(dm->decodeJson(json, &err));
536 if (
error) *
error = err.empty() ?
"invalid json" : err;
540 if (
error) *
error =
"config root must be object";
547 if (!emitter || path.empty()) {
551 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>(
"Filesystem");
552 if (!fs) fs = eve::filesystem::Filesystem::create();
554 std::unique_ptr<eve::filesystem::FileData>
data;
556 data.reset(fs->read(path));
561 if (!
data ||
data->getSize() == 0) {
566 std::string text(
static_cast<const char *
>(
data->getData()),
data->getSize());
569 auto res = emitter->resource();
571 res->modtime = fileModtime(path);
574 if (
auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
575 hot->bind(path,
"particle");
580 if (!emitter)
return false;
581 const std::string &path = emitter->resource()->path;
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.
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 setPrewarm(float seconds)
void applyPreset(const std::string &name)
Named preset: "spark" / "smoke" / "fire". Unknown → no-op.
void setMaxDeltaTime(float seconds)
void setGpuSimulation(bool enable)
void clearVelocityCurve()
void setFlipbook(int hframes, int vframes, float framesPerSecond=0.f, float randomStart=0.f)
float getParticleHeight()
void setLimitVelocity(float maxSpeed)
void setWorldCollision(bool enabled)
void setSpread(float radians)
void setSpin(float minSpin, float maxSpin)
void setTexture(graphics::Texture *texture)
void setEmitterLifetime(float seconds)
void setPosition(float x, float y)
void setRenderMode(const std::string &mode, float stretchFactor=1.f)
void setVisible(bool visible)
void setCollisionBounds(bool enabled, float minX, float minY, float maxX, float maxY)
void setSizes(float startScale, float endScale)
float getParticleLifetimeMin()
void setRadialAcceleration(float minA, float maxA)
void setSpeed(float minSpeed, float maxSpeed)
void setBlendMode(const std::string &mode)
void setDamping(float perSecond)
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 setEmissionRate(float rate)
void setCollision(const std::string &mode, float radius=0.f, float restitution=0.6f, float lifetimeLoss=0.f)
void clearRotationCurve()
float getEmitterLifetime()
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 clearColorGradient()
float getParticleLifetimeMax()
void setDirection(float radians)
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 setGravity(float x, float y)
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)....