#include "ObjectParser.h" #include "EffectParser.h" #include "MaterialParser.h" #include "ModelParser.h" #include "ShaderConstantParser.h" #include "TextureParser.h" #include "UserSettingParser.h" #include "WallpaperEngine/Data/Builders/ColorBuilder.h" #include "WallpaperEngine/Data/Model/Object.h" #include "WallpaperEngine/Data/Model/Project.h" #include "WallpaperEngine/Logging/Log.h" #include #include using namespace WallpaperEngine::Data::Parsers; using namespace WallpaperEngine::Data::Model; ObjectUniquePtr ObjectParser::parse (const JSON& it, const Project& project) { const auto imageIt = it.find ("image"); const auto soundIt = it.find ("sound"); const auto particleIt = it.find ("particle"); const auto textIt = it.find ("text"); const auto lightIt = it.find ("light"); // use shape to refer to VolumeLight const auto shapeIt = it.find ("shape"); // Parse base object data // Some particle objects have numeric 'name' fields, so handle type mismatches gracefully ObjectData basedata; try { basedata = ObjectData { .id = it.require ("id", "Object must have an id"), .name = it.require ("name", "Object must have a name"), .dependencies = parseDependencies (it), .parent = it.optional ("parent"), .origin = it.user ("origin", project.properties, glm::vec3 (0.0f)), .groupScale = it.user ("scale", project.properties, glm::vec3 (1.0f)), .groupAngles = it.user ("angles", project.properties, glm::vec3 (0.0f)), .groupVisible = it.user ("visible", project.properties, true), }; } catch (const std::exception& e) { sLog.error ("Error parsing object base data: ", e.what ()); const auto idIt = it.find ("id"); const auto nameIt = it.find ("name"); int id = (idIt != it.end () && idIt->is_number ()) ? idIt->get () : -1; std::string name = "unknown"; if (nameIt != it.end ()) { if (nameIt->is_string ()) { name = nameIt->get (); } else if (nameIt->is_number ()) { name = std::to_string (nameIt->get ()); } } basedata = ObjectData { .id = id, .name = name, .dependencies = parseDependencies (it), .parent = it.optional ("parent"), .origin = it.user ("origin", project.properties, glm::vec3 (0.0f)), .groupScale = it.user ("scale", project.properties, glm::vec3 (1.0f)), .groupAngles = it.user ("angles", project.properties, glm::vec3 (0.0f)), .groupVisible = it.user ("visible", project.properties, true), }; } if (imageIt != it.end () && imageIt->is_string ()) { return parseImage (it, project, std::move (basedata), *imageIt); } else if (soundIt != it.end () && soundIt->is_array ()) { return parseSound (it, std::move (basedata)); } else if (particleIt != it.end ()) { return parseParticle (it, project, std::move (basedata)); } else if (textIt != it.end ()) { return parseText (it, project, std::move (basedata)); } else if (lightIt != it.end ()) { sLog.error ("Light objects are not supported yet"); } else if (shapeIt != it.end ()) { sLog.error ("VolumeLight objects are not supported yet"); } else { if (!it.optional ("solid", false)) { // dump the object for now, might want to change later // TODO: RE-EVALUATE IF THIS MAKES SENSE, THERE'S OBJECTS THAT CONTAIN OTHER OBJECTS AND THUS AREN'T REALLY // ANYTHING SPECIAL sLog.error ("Unknown object type found: ", it.dump ()); } } return std::make_unique (std::move (basedata)); } std::vector ObjectParser::parseDependencies (const JSON& it) { const auto dependenciesIt = it.find ("dependencies"); if (dependenciesIt == it.end () || !dependenciesIt->is_array ()) { return {}; } std::vector result = {}; for (const auto& cur : *dependenciesIt) { result.push_back (cur); } return result; } SoundUniquePtr ObjectParser::parseSound (const JSON& it, ObjectData base) { const auto soundIt = it.require ("sound", "Object must have a sound"); std::vector sounds = {}; for (const auto& cur : soundIt) { sounds.push_back (cur); } return std::make_unique ( std::move (base), SoundData { .playbackmode = it.optional ("playbackmode"), .sounds = sounds, } ); } TextUniquePtr ObjectParser::parseText (const JSON& it, const Project& project, ObjectData base) { return std::make_unique ( std::move (base), TextData { .text = it.user ("text", project.properties), .font = it.optional ("font", std::string ()), .pointSize = it.user ("pointsize", project.properties, 32.0f), .size = it.optional ("size", glm::vec2 (0.0f)), .scale = it.user ("scale", project.properties, glm::vec3 (1.0f)), .color = it.color ("color", project.properties, Builders::ColorBuilder::White), .alpha = it.user ("alpha", project.properties, 1.0f), .visible = it.user ("visible", project.properties, true), .alignment = it.optional ("horizontalalign", it.optional ("alignment", std::string ("center"))), .verticalalign = it.optional ("verticalalign", std::string ("center")), .padding = it.optional ("padding", 0), } ); } ImageUniquePtr ObjectParser::parseImage (const JSON& it, const Project& project, ObjectData base, const std::string& image) { const auto& properties = project.properties; const auto& effects = it.optional ("effects"); const auto& animationLayers = it.optional ("animationlayers"); auto result = std::make_unique ( std::move (base), ImageData { .scale = it.user ("scale", properties, glm::vec3 (1.0f)), .angles = it.user ("angles", properties, glm::vec3 (0.0f)), .visible = it.user ("visible", properties, true), .alpha = it.user ("alpha", properties, 1.0f), .color = it.color ("color", properties, Builders::ColorBuilder::White), .alignment = it.optional ("horizontalalign", it.optional ("alignment", std::string ("center"))), .size = it.user ("size", properties, glm::vec2 (0.0f))->value->getVec2 (), .parallaxDepth = it.user ("parallaxDepth", properties, glm::vec2 (0.0f)), .colorBlendMode = it.user ("colorBlendMode", properties, 0), .brightness = it.user ("brightness", properties, 1.0f), .model = ModelParser::load (project, image), .effects = effects.has_value () ? parseEffects (*effects, project) : std::vector {}, .animationLayers = animationLayers.has_value () ? parseAnimationLayers (*animationLayers, project) : std::vector {}, } ); const auto instance = it.optional ("instance"); if (instance.has_value () && instance->is_object () && !result->model->material->passes.empty ()) { auto& firstPass = **result->model->material->passes.begin (); const auto instanceTextures = instance->optional ("textures"); if (instanceTextures.has_value ()) { const auto parsed = TextureParser::parseTextureMap (*instanceTextures); firstPass.textures.insert (parsed.begin (), parsed.end ()); } const auto instanceUserTextures = instance->optional ("usertextures"); if (instanceUserTextures.has_value ()) { const auto parsed = TextureParser::parseTextureMap (*instanceUserTextures); firstPass.usertextures.insert (parsed.begin (), parsed.end ()); } } return result; } std::vector ObjectParser::parseEffects (const JSON& it, const Project& project) { if (!it.is_array ()) { return {}; } std::vector result = {}; for (const auto& cur : it) { result.push_back (parseEffect (cur, project)); } return result; } ImageEffectUniquePtr ObjectParser::parseEffect (const JSON& it, const Project& project) { const auto& passsOverrides = it.optional ("passes"); return std::make_unique (ImageEffect { .id = it.optional ("id", -1), .name = it.optional ("name", "Effect without name"), .visible = it.user ("visible", project.properties, true), .passOverrides = passsOverrides.has_value () ? parseEffectPassOverrides (passsOverrides.value (), project) : std::vector {}, .effect = EffectParser::load (project, it.require ("file", "Image effect must have an effect")) }); } std::vector ObjectParser::parseEffectPassOverrides (const JSON& it, const Project& project) { if (!it.is_array ()) { return {}; } std::vector result = {}; for (const auto& cur : it) { result.push_back (parseEffectPass (cur, project)); } return result; } ImageEffectPassOverrideUniquePtr ObjectParser::parseEffectPass (const JSON& it, const Project& project) { const auto& combos = it.optional ("combos"); const auto& textures = it.optional ("textures"); const auto& constants = it.optional ("constantshadervalues"); const auto& usertextures = it.optional ("usertextures"); // TODO: PARSE CONSTANT SHADER VALUES AND FIND REFS? return std::make_unique (ImageEffectPassOverride { .id = it.optional ("id", -1), .combos = combos.has_value () ? parseComboMap (combos.value ()) : ComboMap {}, .constants = constants.has_value () ? ShaderConstantParser::parse (constants.value (), project) : ShaderConstantMap {}, .textures = textures.has_value () ? TextureParser::parseTextureMap (textures.value ()) : TextureMap {}, .usertextures = usertextures.has_value () ? TextureParser::parseTextureMap (usertextures.value ()) : TextureMap {}, }); } ComboMap ObjectParser::parseComboMap (const JSON& it) { if (!it.is_object ()) { return {}; } ComboMap result = {}; for (const auto& cur : it.items ()) { result.emplace (cur.key (), cur.value ()); } return result; } std::vector ObjectParser::parseAnimationLayers (const JSON& it, const Project& project) { if (!it.is_array ()) { return {}; } std::vector result = {}; for (const auto& cur : it.items ()) { result.push_back (parseAnimationLayer (cur.value (), project)); } return result; } ImageAnimationLayerUniquePtr ObjectParser::parseAnimationLayer (const JSON& it, const Project& project) { const auto& properties = project.properties; return std::make_unique (ImageAnimationLayer { .id = it.require ("id", "Animation layer must have an id"), .rate = it.user ("rate", properties, 1.0f), .visible = it.user ("visible", properties, false), .blend = it.user ("blend", properties, 1.0f), .animation = it.user ("animation", properties, 0), }); } ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& project, ObjectData base) { try { const auto& properties = project.properties; const auto particleIt = it.find ("particle"); if (particleIt == it.end ()) { sLog.error ("Particle object must have a particle definition"); return std::make_unique ( std::move (base), ParticleData { .scale = it.user ("scale", properties, glm::vec3 (1.0f)), .angles = it.user ("angles", properties, glm::vec3 (0.0f)), .visible = it.user ("visible", properties, true), .parallaxDepth = it.user ("parallaxDepth", properties, glm::vec2 (0.0f)), .particleFile = "", .animationMode = "sequence", .sequenceMultiplier = 1.0f, .maxCount = 100, .startTime = 0, .flags = 0, .material = nullptr, .emitters = {}, .initializers = {}, .operators = {}, .renderers = {}, .controlPoints = {}, .children = {}, .instanceOverride = { .enabled = Builders::UserSettingBuilder::fromValue(false), .alpha = Builders::UserSettingBuilder::fromValue(1.0f), .size = Builders::UserSettingBuilder::fromValue(1.0f), .lifetime = Builders::UserSettingBuilder::fromValue(1.0f), .rate = Builders::UserSettingBuilder::fromValue(1.0f), .speed = Builders::UserSettingBuilder::fromValue(1.0f), .count = Builders::UserSettingBuilder::fromValue(1.0f), .color = Builders::UserSettingBuilder::fromValue(1.0f), .colorn = Builders::UserSettingBuilder::fromValue(1.0f), }, } ); } std::string particleFile; if (particleIt->is_string ()) { particleFile = particleIt->get (); } // Load particle definition from file if it's a string reference JSON particleJson = JSON::object (); if (!particleFile.empty ()) { try { particleJson = WallpaperEngine::Data::JSON::JSON::parse (project.assetLocator->readString (particleFile)); } catch (std::runtime_error& e) { sLog.error ("Cannot load particle file: ", particleFile, " - ", e.what ()); } } else if (particleIt->is_object ()) { particleJson = *particleIt; } // Parse emitters (note: field is named "emitter" not "emitters") std::vector emitters; const auto emittersIt = particleJson.find ("emitter"); if (emittersIt != particleJson.end () && emittersIt->is_array ()) { for (const auto& emitter : *emittersIt) { emitters.push_back (parseParticleEmitter (emitter)); } } // Parse initializers (note: field is named "initializer" not "initializers") std::vector initializers; const auto initializersIt = particleJson.find ("initializer"); if (initializersIt != particleJson.end () && initializersIt->is_array ()) { for (const auto& initializer : *initializersIt) { auto init = parseParticleInitializer (initializer, project.properties); if (init) { initializers.push_back (std::move (init)); } } } // Parse operators (note: field is named "operator" not "operators") std::vector operators; const auto operatorsIt = particleJson.find ("operator"); if (operatorsIt != particleJson.end () && operatorsIt->is_array ()) { for (const auto& op : *operatorsIt) { auto oper = parseParticleOperator (op, project.properties); if (oper) { operators.push_back (std::move (oper)); } } } // Parse renderers (note: field is named "renderer" not "renderers") std::vector renderers; const auto renderersIt = particleJson.find ("renderer"); if (renderersIt != particleJson.end () && renderersIt->is_array ()) { for (const auto& renderer : *renderersIt) { renderers.push_back (parseParticleRenderer (renderer)); } } // Add default sprite renderer if none specified if (renderers.empty ()) { renderers.push_back ( ParticleRenderer { .name = "sprite", .length = 0.05f, .maxLength = 10.0f, .minLength = 0.0f, .subdivision = 1.0f, .segments = 4.0f, .uvScale = 1.0f, .uvScrolling = false, .uvSmoothing = true, .fadeAlpha = false, .fadeSize = false, } ); } // Parse control points (note: field is named "controlpoint" not "controlpoints") std::vector controlPoints; const auto controlPointsIt = particleJson.find ("controlpoint"); if (controlPointsIt != particleJson.end () && controlPointsIt->is_array ()) { for (const auto& cp : *controlPointsIt) { controlPoints.push_back (parseParticleControlPoint (cp)); } } // Parse children std::vector children; const auto childrenIt = particleJson.optional ("children"); if (childrenIt.has_value () && childrenIt->is_array ()) { for (const auto& child : *childrenIt) { children.push_back (parseParticleChild (child, project)); } } // Parse instance override ParticleInstanceOverride instanceOverride = { .enabled = Builders::UserSettingBuilder::fromValue (false), .alpha = Builders::UserSettingBuilder::fromValue (1.0f), .size = Builders::UserSettingBuilder::fromValue (1.0f), .lifetime = Builders::UserSettingBuilder::fromValue (1.0f), .rate = Builders::UserSettingBuilder::fromValue (1.0f), .speed = Builders::UserSettingBuilder::fromValue (1.0f), .count = Builders::UserSettingBuilder::fromValue (1.0f), .color = Builders::UserSettingBuilder::fromValue (1.0f), .colorn = Builders::UserSettingBuilder::fromValue (1.0f), }; const auto instanceOverrideIt = it.optional ("instanceoverride"); if (instanceOverrideIt.has_value ()) { instanceOverride = parseParticleInstanceOverride (*instanceOverrideIt, project.properties); } // Parse material - particles reference materials directly, not models ModelUniquePtr material = nullptr; const auto materialIt = particleJson.find ("material"); if (materialIt != particleJson.end () && materialIt->is_string ()) { try { std::string materialPath = materialIt->get (); // Particle materials are stored as just material definitions, not model files // So we need to wrap them in a model structure auto mat = MaterialParser::load (project, materialPath); material = std::make_unique (ModelStruct { .filename = materialPath, .material = std::move (mat), .solidlayer = false, .fullscreen = false, .passthrough = false, .autosize = false, .nopadding = false, .width = std::nullopt, .height = std::nullopt, .puppet = std::nullopt, }); } catch (std::runtime_error& e) { sLog.error ("Cannot load particle material: ", materialIt->get (), " - ", e.what ()); } } // Parse string fields safely std::string animationMode = "sequence"; const auto animModeIt = particleJson.find ("animationmode"); if (animModeIt != particleJson.end () && animModeIt->is_string ()) { animationMode = animModeIt->get (); } // Parse numeric fields safely float sequenceMultiplier = 1.0f; uint32_t maxCount = 100; uint32_t startTime = 0; uint32_t flags = 0; const auto seqMultIt = particleJson.find ("sequencemultiplier"); if (seqMultIt != particleJson.end () && seqMultIt->is_number ()) { sequenceMultiplier = seqMultIt->get (); } const auto maxCountIt = particleJson.find ("maxcount"); if (maxCountIt != particleJson.end () && maxCountIt->is_number ()) { maxCount = maxCountIt->get (); } const auto startTimeIt = particleJson.find ("starttime"); if (startTimeIt != particleJson.end () && startTimeIt->is_number ()) { startTime = startTimeIt->get (); } const auto flagsIt = particleJson.find ("flags"); if (flagsIt != particleJson.end () && flagsIt->is_number ()) { flags = flagsIt->get (); } return std::make_unique ( std::move (base), ParticleData { .scale = it.user ("scale", properties, glm::vec3 (1.0f)), .angles = it.user ("angles", properties, glm::vec3 (0.0f)), .visible = it.user ("visible", properties, true), .parallaxDepth = it.user ("parallaxDepth", properties, glm::vec2 (0.0f)), .particleFile = particleFile, .animationMode = animationMode, .sequenceMultiplier = sequenceMultiplier, .maxCount = maxCount, .startTime = startTime, .flags = flags, .material = std::move (material), .emitters = std::move (emitters), .initializers = std::move (initializers), .operators = std::move (operators), .renderers = std::move (renderers), .controlPoints = std::move (controlPoints), .children = std::move (children), .instanceOverride = std::move (instanceOverride), } ); } catch (nlohmann::json::exception& e) { sLog.error ("Error parsing particle '", base.name, "': ", e.what ()); sLog.error ("Particle JSON: ", it.dump ()); throw; } } ParticleEmitter ObjectParser::parseParticleEmitter (const JSON& it) { // Parse string name safely std::string name; const auto nameIt = it.find ("name"); if (nameIt != it.end () && nameIt->is_string ()) { name = nameIt->get (); } // Helper lambda to parse vec3 fields that might be strings, arrays, single numbers, or missing auto parseVec3 = [&] (const char* fieldName, const glm::vec3& defaultValue) -> glm::vec3 { const auto fieldIt = it.find (fieldName); if (fieldIt == it.end ()) { return defaultValue; } if (fieldIt->is_string ()) { return it.optional (fieldName, defaultValue); } if (fieldIt->is_number ()) { // Single number - use for all components (common for distancemax/distancemin) float val = fieldIt->get (); return glm::vec3 (val, val, val); } if (fieldIt->is_array () && fieldIt->size () >= 3) { return glm::vec3 ((*fieldIt)[0].get (), (*fieldIt)[1].get (), (*fieldIt)[2].get ()); } return defaultValue; }; auto parseIVec3 = [&] (const char* fieldName, const glm::ivec3& defaultValue) -> glm::ivec3 { const auto fieldIt = it.find (fieldName); if (fieldIt == it.end ()) { return defaultValue; } if (fieldIt->is_array () && fieldIt->size () >= 3) { return glm::ivec3 ((*fieldIt)[0].get (), (*fieldIt)[1].get (), (*fieldIt)[2].get ()); } return defaultValue; }; auto parseVec2 = [&] (const char* fieldName, const glm::vec2& defaultValue) -> glm::vec2 { const auto fieldIt = it.find (fieldName); if (fieldIt == it.end ()) { return defaultValue; } if (fieldIt->is_string ()) { return it.optional (fieldName, defaultValue); } if (fieldIt->is_array () && fieldIt->size () >= 2) { return glm::vec2 ((*fieldIt)[0].get (), (*fieldIt)[1].get ()); } return defaultValue; }; try { return ParticleEmitter { .id = it.optional ("id", -1), .name = name, .directions = parseVec3 ("directions", glm::vec3 (1.0f, 1.0f, 0.0f)), .distanceMin = parseVec3 ("distancemin", glm::vec3 (0.0f, 0.0f, 0.0f)), .distanceMax = parseVec3 ("distancemax", glm::vec3 (256.0f, 256.0f, 0.0f)), .origin = parseVec3 ("origin", glm::vec3 (0.0f)), .sign = parseIVec3 ("sign", glm::ivec3 (0)), .instantaneous = it.optional ("instantaneous", 0u), .speedMin = it.optional ("speedmin", 0.0f), .speedMax = it.optional ("speedmax", 0.0f), .rate = it.optional ("rate", 10.0f), .controlPoint = it.optional ("controlpoint", 0), .flags = it.optional ("flags", 0u), .cone = it.optional ("cone", 0.0f), .delay = it.optional ("delay", 0.0f), .duration = it.optional ("duration", 0.0f), .audioProcessingBounds = parseVec2 ("audioprocessingbounds", glm::vec2 (0.8f, 1.0f)), .audioProcessingExponent = it.optional ("audioprocessingexponent", 2), .audioProcessingFrequencyStart = it.optional ("audioprocessingfrequencystart", 0), .audioProcessingFrequencyEnd = it.optional ("audioprocessingfrequencyend", 1), .audioProcessingMode = it.optional ("audioprocessingmode", 0), .minPeriodicDelay = it.optional ("minperiodicdelay", 1.0f), .maxPeriodicDelay = it.optional ("maxperiodicdelay", 2.0f), .minPeriodicDuration = it.optional ("minperiodicduration", 2.0f), .maxPeriodicDuration = it.optional ("maxperiodicduration", 3.0f), }; } catch (nlohmann::json::exception& e) { sLog.error ("Error parsing emitter: ", e.what ()); sLog.error ("Emitter JSON: ", it.dump ()); throw; } } ParticleInitializerUniquePtr ObjectParser::parseParticleInitializer (const JSON& it, const Properties& properties) { std::string name = it.optional ("name", ""); if (name == "colorrandom") { return std::make_unique ( it.color ("min", properties, Builders::ColorBuilder::Black), it.color ("max", properties, Builders::ColorBuilder::White) ); } else if (name == "sizerandom") { return std::make_unique ( it.user ("min", properties, 0.0f), it.user ("max", properties, 20.0f), it.user ("exponent", properties, 1.0f) ); } else if (name == "alpharandom") { return std::make_unique ( it.user ("min", properties, 0.05f), it.user ("max", properties, 1.0f) ); } else if (name == "lifetimerandom") { return std::make_unique ( it.user ("min", properties, 0.0f), it.user ("max", properties, 1.0f) ); } else if (name == "velocityrandom") { return std::make_unique ( it.user ("min", properties, glm::vec3 (-32.0f)), it.user ("max", properties, glm::vec3 (32.0f)) ); } else if (name == "rotationrandom") { return std::make_unique ( it.user ("min", properties, glm::vec3 (0.0f)), it.user ("max", properties, glm::vec3 (0.0f, 0.0f, glm::two_pi ())) ); } else if (name == "angularvelocityrandom") { return std::make_unique ( it.user ("min", properties, glm::vec3 (0.0f, 0.0f, -5.0f)), it.user ("max", properties, glm::vec3 (0.0f, 0.0f, 5.0f)), it.user ("exponent", properties, 1.0f) ); } else if (name == "turbulentvelocityrandom") { return std::make_unique ( it.user ("speedmin", properties, 100.0f), it.user ("speedmax", properties, 250.0f), it.user ("scale", properties, 1.0f), it.user ("offset", properties, 0.0f), it.user ("forward", properties, glm::vec3 (0.0f, 1.0f, 0.0f)), it.user ("timescale", properties, 1.0f), it.user ("phasemin", properties, 0.0f), it.user ("phasemax", properties, 0.1f), it.user ("right", properties, glm::vec3 (0.0f, 0.0f, 1.0f)) ); } else if (name == "mapsequencearoundcontrolpoint") { return std::make_unique ( it.user ("controlpoint", properties, 0), it.user ("count", properties, 1), it.user ("speedmin", properties, glm::vec3 (0.0f)), it.user ("speedmax", properties, glm::vec3 (100.0f)) ); } return nullptr; } ParticleOperatorUniquePtr ObjectParser::parseParticleOperator (const JSON& it, const Properties& properties) { std::string name = it.optional ("name", ""); if (name == "movement") { return std::make_unique ( it.user ("drag", properties, 0.0f), it.user ("gravity", properties, glm::vec3 (0.0f)) ); } else if (name == "angularmovement") { return std::make_unique ( it.user ("drag", properties, 0.0f), it.user ("force", properties, glm::vec3 (0.0f)) ); } else if (name == "alphafade") { return std::make_unique ( it.user ("fadeintime", properties, 0.5f), it.user ("fadeouttime", properties, 0.5f) ); } else if (name == "sizechange") { return std::make_unique ( it.user ("starttime", properties, 0.0f), it.user ("endtime", properties, 1.0f), it.user ("startvalue", properties, 1.0f), it.user ("endvalue", properties, 0.0f) ); } else if (name == "alphachange") { return std::make_unique ( it.user ("starttime", properties, 0.0f), it.user ("endtime", properties, 1.0f), it.user ("startvalue", properties, 1.0f), it.user ("endvalue", properties, 0.0f) ); } else if (name == "colorchange") { return std::make_unique ( it.user ("starttime", properties, 0.0f), it.user ("endtime", properties, 1.0f), it.user ("startvalue", properties, glm::vec3 (1.0f)), it.user ("endvalue", properties, glm::vec3 (1.0f)) ); } else if (name == "turbulence") { return std::make_unique ( it.user ("scale", properties, 0.005f), it.user ("speedmin", properties, 500.0f), it.user ("speedmax", properties, 1000.0f), it.user ("timescale", properties, 0.01f), it.user ("mask", properties, glm::vec3 (1.0f, 1.0f, 0.0f)), it.user ("phasemin", properties, 0.0f), it.user ("phasemax", properties, 0.0f), it.user ("audioprocessingmode", properties, 0), it.user ("audioprocessingbounds", properties, glm::vec2 (0.0f, 1.0f)), it.user ("audioprocessingexponent", properties, 1.0f), it.user ("audioprocessingfrequencystart", properties, 0), it.user ("audioprocessingfrequencyend", properties, 15) ); } else if (name == "vortex" || name == "vortex_v2") { return std::make_unique ( it.optional ("controlpoint", 0), it.optional ("flags", 0), // 1 = infinite axis, 2 = maintain distance, 4 = ring shape it.user ("axis", properties, glm::vec3 (0.0f, 0.0f, 1.0f)), it.user ("offset", properties, glm::vec3 (0.0f)), it.user ("distanceinner", properties, 500.0f), it.user ("distanceouter", properties, 650.0f), it.user ("speedinner", properties, 2500.0f), it.user ("speedouter", properties, 0.0f), it.user ("centerforce", properties, 1.0f), it.user ("ringradius", properties, 300.0f), it.user ("ringwidth", properties, 50.0f), it.user ("ringpulldistance", properties, 50.0f), it.user ("ringpullforce", properties, 10.0f), it.user ("audioprocessingmode", properties, 0), it.user ("audioprocessingbounds", properties, glm::vec2 (0.0f, 1.0f)) ); } else if (name == "controlpointattract") { return std::make_unique ( it.optional ("controlpoint", 0), it.user ("origin", properties, glm::vec3 (0.0f)), it.user ("scale", properties, 100.0f), it.user ("threshold", properties, 1000.0f) ); } else if (name == "oscillatealpha") { return std::make_unique ( it.user ("frequencymin", properties, 0.0f), it.user ("frequencymax", properties, 10.0f), it.user ("scalemin", properties, 0.0f), it.user ("scalemax", properties, 1.0f), it.user ("phasemin", properties, 0.0f), it.user ("phasemax", properties, glm::two_pi ()) ); } else if (name == "oscillatesize") { return std::make_unique ( it.user ("frequencymin", properties, 0.0f), it.user ("frequencymax", properties, 10.0f), it.user ("scalemin", properties, 0.8f), it.user ("scalemax", properties, 1.2f), it.user ("phasemin", properties, 0.0f), it.user ("phasemax", properties, glm::two_pi ()) ); } else if (name == "oscillateposition") { return std::make_unique ( it.user ("frequencymin", properties, 0.0f), it.user ("frequencymax", properties, 5.0f), it.user ("scalemin", properties, 0.0f), it.user ("scalemax", properties, 10.0f), it.user ("phasemin", properties, 0.0f), it.user ("phasemax", properties, glm::two_pi ()), it.user ("mask", properties, glm::vec3 (1.0f, 1.0f, 0.0f)) ); } return nullptr; } ParticleRenderer ObjectParser::parseParticleRenderer (const JSON& it) { std::string name = "sprite"; const auto nameIt = it.find ("name"); if (nameIt != it.end () && nameIt->is_string ()) { name = nameIt->get (); } // Renderer-type-specific defaults float subdivisionDefault = (name == "rope") ? 4.0f : 1.0f; float lengthDefault = (name == "ropetrail") ? 1.0f : 0.05f; return ParticleRenderer { .name = name, .length = it.optional ("length", lengthDefault), .maxLength = it.optional ("maxlength", 10.0f), .minLength = it.optional ("minlength", 0.0f), .subdivision = it.optional ("subdivision", subdivisionDefault), .segments = it.optional ("segments", 4.0f), .uvScale = it.optional ("uvscale", 1.0f), .uvScrolling = it.optional ("uvscrolling", false), .uvSmoothing = it.optional ("uvsmoothing", true), .fadeAlpha = it.optional ("fadealpha", false), .fadeSize = it.optional ("fadesize", false), }; } ParticleControlPoint ObjectParser::parseParticleControlPoint (const JSON& it) { // Parse offset - can be string "x y z" or array [x,y,z] glm::vec3 offset (0.0f); const auto offsetIt = it.find ("offset"); if (offsetIt != it.end ()) { if (offsetIt->is_string ()) { // Parse string format "x y z" std::string offsetStr = offsetIt->get (); std::istringstream iss (offsetStr); iss >> offset.x >> offset.y >> offset.z; } else { // Try parsing as vec3 directly try { offset = it.optional ("offset", glm::vec3 (0.0f)); } catch (...) { offset = glm::vec3 (0.0f); } } } return ParticleControlPoint { .id = it.optional ("id", -1), .flags = it.optional ("flags", 0u), .offset = offset, .lockToPointer = it.optional ("locktopointer", false), }; } ParticleChild ObjectParser::parseParticleChild (const JSON& it, const Project& project) { std::string particleFile = ""; const auto particleIt = it.find ("particle"); if (particleIt != it.end () && particleIt->is_string ()) { particleFile = particleIt->get (); } std::string type = "static"; const auto typeIt = it.find ("type"); if (typeIt != it.end () && typeIt->is_string ()) { type = typeIt->get (); } std::string name = ""; const auto nameIt = it.find ("name"); if (nameIt != it.end () && nameIt->is_string ()) { name = nameIt->get (); } // Helper lambda to parse vec3 fields that might be strings, arrays, single numbers, or missing auto parseVec3 = [&] (const char* fieldName, const glm::vec3& defaultValue) -> glm::vec3 { const auto fieldIt = it.find (fieldName); if (fieldIt == it.end ()) { return defaultValue; } if (fieldIt->is_string ()) { return it.optional (fieldName, defaultValue); } if (fieldIt->is_number ()) { // Single number - use for all components float val = fieldIt->get (); return glm::vec3 (val, val, val); } if (fieldIt->is_array () && fieldIt->size () >= 3) { return glm::vec3 ((*fieldIt)[0].get (), (*fieldIt)[1].get (), (*fieldIt)[2].get ()); } return defaultValue; }; return ParticleChild { .type = type, .name = name, .maxCount = it.optional ("maxcount", 20), .controlPointStartIndex = it.optional ("controlpointstartindex", 0), .probability = it.optional ("probability", 1.0f), .angles = parseVec3 ("angles", glm::vec3 (0.0f)), .origin = parseVec3 ("origin", glm::vec3 (0.0f)), .scale = parseVec3 ("scale", glm::vec3 (1.0f)), .particleFile = particleFile, }; } ParticleInstanceOverride ObjectParser::parseParticleInstanceOverride (const JSON& it, const Properties& properties) { return ParticleInstanceOverride { .enabled = it.user ("enabled", properties, true), .alpha = it.user ("alpha", properties, 1.0f), .size = it.user ("size", properties, 1.0f), .lifetime = it.user ("lifetime", properties, 1.0f), .rate = it.user ("rate", properties, 1.0f), .speed = it.user ("speed", properties, 1.0f), .count = it.user ("count", properties, 1.0f), .color = it.user ("color", properties, glm::vec3 (1.0f)), .colorn = it.user ("colorn", properties, glm::vec3 (1.0f)), }; }