Эх сурвалжийг харах

feat: comprehensive particle rendering improvements and fixes (#400)

* fix: apply particle count instance override multiplier

* fix: support single number values for vec3 particle fields

* fix: correct particle Y-axis coordinate space conversion

* feat: implement 3D rotation for particle billboards

* feat: implement randomframe animation mode for particles

* feat: add size exponent support for particle initializers

* feat: implement turbulent velocity initializer for particles

* feat: implement mouse-linked control points for particles

* fix: add Y-axis flips for velocity and gravity in centered coordinate space

* fix: improve turbulent velocity with fallback direction and offset rotation

* fix: change turbulent velocity offset from rotation to noise offset

* fix: initialize time on first render frame

* fix: properly use UserSettingUniquePtr and DynamicValue pointers

* feat: add turblance operator & init constants for materials

* feat: add vortex operator

* feat: initial sprite trail support for particles

* feat: add control point attract operator

* fix: set proper eye position uniform for shaders

* fix: update eye direction to use model matrix inverse

* fix: properly calculate texture ratio

* revert: remove eye position and model matrix inverse uniforms

* perf: optimize particle rendering with indexed draw calls

* fix: make trails follow historical paths

* fix: initialize trail renderer config before using it in constructor

* fix: properly size trails based on particle size

* fix: properly choose randomframe sprite

* fix: apply control point offset to mouse control points

* fix: make turbelence operator and initializer use noise state

* fix: properly detect and use window/screen resolutions

* fix: detect spritesheet data from .tex file when available

* fix: validate spritesheet grid can hold all frames before populating metadata

* fix: initialize frame to -1 at spawn for randomframe mode to work correctly

* chore: rename rightDir to widthDir for clarity in trail rendering

* fix: detect audio mode and set sane defaults until proper support is added

* fix: properly scale sphere emitter to 3D coordinates - default to 2D

* fix: vortex operator should not use curl noise, switch to control point rotation

* feat: add mapSequenceAroundControlPoint initializer

* fix: improve controlPointAttract operator to behave more similarly to original implementation

* fix: improve vortex operator by adding distance-range logic

* fix: prevent infinite loop in sphere emitter with invalid directions mask

* fix: add random offset to turbulence noise position to prevent clustering

* fix: enable trail rendering for subdivision == 1
Aziz Hasanain 10 сар өмнө
parent
commit
3e3d652252

+ 2 - 0
src/WallpaperEngine/Data/Model/Material.h

@@ -51,6 +51,8 @@ struct MaterialPass {
     TextureMap usertextures;
     /** The combos and their values to pass onto the shader */
     ComboMap combos;
+    /** Constant shader values (e.g., overbright, bloom settings) */
+    ShaderConstantMap constants;
 };
 
 struct Material {

+ 62 - 1
src/WallpaperEngine/Data/Model/Object.h

@@ -141,6 +141,7 @@ struct ParticleControlPoint {
     int id;
     uint32_t flags;
     glm::vec3 offset;
+    bool lockToPointer;
 };
 
 /**
@@ -179,9 +180,11 @@ class ColorRandomInitializer : public ParticleInitializerBase {
 
 class SizeRandomInitializer : public ParticleInitializerBase {
   public:
-    SizeRandomInitializer (UserSettingUniquePtr min, UserSettingUniquePtr max) : min (std::move (min)), max (std::move (max)) {}
+    SizeRandomInitializer (UserSettingUniquePtr min, UserSettingUniquePtr max, UserSettingUniquePtr exponent)
+        : min (std::move (min)), max (std::move (max)), exponent (std::move (exponent)) {}
     UserSettingUniquePtr min;
     UserSettingUniquePtr max;
+    UserSettingUniquePtr exponent;
 };
 
 class AlphaRandomInitializer : public ParticleInitializerBase {
@@ -219,6 +222,26 @@ class AngularVelocityRandomInitializer : public ParticleInitializerBase {
     UserSettingUniquePtr max;
 };
 
+class TurbulentVelocityRandomInitializer : public ParticleInitializerBase {
+  public:
+    TurbulentVelocityRandomInitializer (UserSettingUniquePtr speedMin, UserSettingUniquePtr speedMax, UserSettingUniquePtr scale, UserSettingUniquePtr offset)
+        : speedMin (std::move (speedMin)), speedMax (std::move (speedMax)), scale (std::move (scale)), offset (std::move (offset)) {}
+    UserSettingUniquePtr speedMin;
+    UserSettingUniquePtr speedMax;
+    UserSettingUniquePtr scale;
+    UserSettingUniquePtr offset;
+};
+
+class MapSequenceAroundControlPointInitializer : public ParticleInitializerBase {
+  public:
+    MapSequenceAroundControlPointInitializer (UserSettingUniquePtr controlPoint, UserSettingUniquePtr count, UserSettingUniquePtr speedMin, UserSettingUniquePtr speedMax)
+        : controlPoint (std::move (controlPoint)), count (std::move (count)), speedMin (std::move (speedMin)), speedMax (std::move (speedMax)) {}
+    UserSettingUniquePtr controlPoint;
+    UserSettingUniquePtr count;
+    UserSettingUniquePtr speedMin;
+    UserSettingUniquePtr speedMax;
+};
+
 using ParticleInitializerUniquePtr = std::unique_ptr<ParticleInitializerBase>;
 
 /**
@@ -280,6 +303,44 @@ class ColorChangeOperator : public ParticleOperatorBase {
     UserSettingUniquePtr endValue;
 };
 
+class TurbulenceOperator : public ParticleOperatorBase {
+  public:
+    TurbulenceOperator (UserSettingUniquePtr scale, UserSettingUniquePtr speedMin, UserSettingUniquePtr speedMax, UserSettingUniquePtr timeScale, UserSettingUniquePtr audioProcessingMode, UserSettingUniquePtr audioProcessingBounds, UserSettingUniquePtr audioProcessingFrequencyEnd)
+        : scale (std::move (scale)), speedMin (std::move (speedMin)), speedMax (std::move (speedMax)), timeScale (std::move (timeScale)), audioProcessingMode(std::move(audioProcessingMode)), audioProcessingBounds(std::move(audioProcessingBounds)), audioProcessingFrequencyEnd(std::move(audioProcessingFrequencyEnd)) {}
+    UserSettingUniquePtr scale;
+    UserSettingUniquePtr speedMin;
+    UserSettingUniquePtr speedMax;
+    UserSettingUniquePtr timeScale;
+    UserSettingUniquePtr audioProcessingMode;
+    UserSettingUniquePtr audioProcessingBounds;      // Min/max audio amplitude range (e.g., "0.8 1")
+    UserSettingUniquePtr audioProcessingFrequencyEnd; // Max frequency bin to sample (e.g., 15)
+};
+
+class VortexOperator : public ParticleOperatorBase {
+  public:
+    VortexOperator (int controlPoint, UserSettingUniquePtr axis, UserSettingUniquePtr offset, UserSettingUniquePtr distanceInner, UserSettingUniquePtr distanceOuter, UserSettingUniquePtr speedInner, UserSettingUniquePtr speedOuter, UserSettingUniquePtr audioProcessingMode, UserSettingUniquePtr audioProcessingBounds)
+        : controlPoint (controlPoint), axis (std::move (axis)), offset (std::move (offset)), distanceInner (std::move (distanceInner)), distanceOuter (std::move (distanceOuter)), speedInner (std::move (speedInner)), speedOuter (std::move (speedOuter)), audioProcessingMode(std::move(audioProcessingMode)), audioProcessingBounds(std::move(audioProcessingBounds)) {}
+    int controlPoint;
+    UserSettingUniquePtr axis;
+    UserSettingUniquePtr offset;
+    UserSettingUniquePtr distanceInner;
+    UserSettingUniquePtr distanceOuter;
+    UserSettingUniquePtr speedInner;
+    UserSettingUniquePtr speedOuter;
+    UserSettingUniquePtr audioProcessingMode;
+    UserSettingUniquePtr audioProcessingBounds;
+};
+
+class ControlPointAttractOperator : public ParticleOperatorBase {
+  public:
+    ControlPointAttractOperator (int controlPoint, UserSettingUniquePtr origin, UserSettingUniquePtr scale, UserSettingUniquePtr threshold)
+        : controlPoint (controlPoint), origin (std::move (origin)), scale (std::move (scale)), threshold (std::move (threshold)) {}
+    int controlPoint;
+    UserSettingUniquePtr origin;
+    UserSettingUniquePtr scale;
+    UserSettingUniquePtr threshold;
+};
+
 using ParticleOperatorUniquePtr = std::unique_ptr<ParticleOperatorBase>;
 
 /**

+ 9 - 7
src/WallpaperEngine/Data/Parsers/MaterialParser.cpp

@@ -2,6 +2,7 @@
 
 #include "WallpaperEngine/Data/Model/Material.h"
 #include "WallpaperEngine/Data/Model/Project.h"
+#include "WallpaperEngine/Data/Parsers/ShaderConstantParser.h"
 #include "WallpaperEngine/FileSystem/Container.h"
 
 using namespace WallpaperEngine::Data::Parsers;
@@ -10,17 +11,17 @@ using namespace WallpaperEngine::Data::Model;
 MaterialUniquePtr MaterialParser::load (const Project& project, const std::string& filename) {
     const auto materialJson = JSON::parse (project.assetLocator->readString (filename));
 
-    return parse (materialJson, filename);
+    return parse (materialJson, filename, project);
 }
 
-MaterialUniquePtr MaterialParser::parse (const JSON& it, const std::string& filename) {
+MaterialUniquePtr MaterialParser::parse (const JSON& it, const std::string& filename, const Project& project) {
     return std::make_unique <Material> (Material {
         .filename = filename,
-        .passes = parsePasses (it.require ("passes", "Material must have passes to render")),
+        .passes = parsePasses (it.require ("passes", "Material must have passes to render"), project),
     });
 }
 
-std::vector <MaterialPassUniquePtr> MaterialParser::parsePasses (const JSON& it) {
+std::vector <MaterialPassUniquePtr> MaterialParser::parsePasses (const JSON& it, const Project& project) {
     std::vector <MaterialPassUniquePtr> result = {};
 
     if (!it.is_array ()) {
@@ -28,17 +29,17 @@ std::vector <MaterialPassUniquePtr> MaterialParser::parsePasses (const JSON& it)
     }
 
     for (const auto& cur : it) {
-        result.push_back (parsePass (cur));
+        result.push_back (parsePass (cur, project));
     }
 
     return result;
 }
 
-MaterialPassUniquePtr MaterialParser::parsePass (const JSON& it) {
+MaterialPassUniquePtr MaterialParser::parsePass (const JSON& it, const Project& project) {
     const auto textures = it.optional ("textures");
     const auto usertextures = it.optional ("usertextures");
     const auto combos = it.optional ("combos");
-    const auto constants = it.optional ("constants");
+    const auto constants = it.optional ("constantshadervalues");
 
     return std::make_unique <MaterialPass>(MaterialPass {
         //TODO: REMOVE THIS UGLY STD::STRING CREATION
@@ -50,6 +51,7 @@ MaterialPassUniquePtr MaterialParser::parsePass (const JSON& it) {
         .textures = textures.has_value () ? parseTextures (*textures) : TextureMap {},
         .usertextures = usertextures.has_value () ? parseTextures (*usertextures) : TextureMap {},
         .combos = combos.has_value () ? parseCombos (*combos) : ComboMap {},
+        .constants = constants.has_value () ? ShaderConstantParser::parse (*constants, project) : ShaderConstantMap {},
     });
 }
 

+ 3 - 3
src/WallpaperEngine/Data/Parsers/MaterialParser.h

@@ -11,9 +11,9 @@ using namespace WallpaperEngine::Data::Model;
 class MaterialParser {
   public:
     static MaterialUniquePtr load (const Project& project, const std::string& filename);
-    static MaterialUniquePtr parse (const JSON& it, const std::string& filename);
-    static std::vector <MaterialPassUniquePtr> parsePasses (const JSON& it);
-    static MaterialPassUniquePtr parsePass (const JSON& it);
+    static MaterialUniquePtr parse (const JSON& it, const std::string& filename, const Project& project);
+    static std::vector <MaterialPassUniquePtr> parsePasses (const JSON& it, const Project& project);
+    static MaterialPassUniquePtr parsePass (const JSON& it, const Project& project);
     static std::map <int, std::string> parseTextures (const JSON& it);
     static std::map <std::string, int> parseCombos (const JSON& it);
     static BlendingMode parseBlendMode (const std::string& mode);

+ 59 - 4
src/WallpaperEngine/Data/Parsers/ObjectParser.cpp

@@ -527,7 +527,7 @@ ParticleEmitter ObjectParser::parseParticleEmitter (const JSON& it) {
         name = nameIt->get<std::string> ();
     }
 
-    // Helper lambda to parse vec3 fields that might be strings, arrays, or missing
+    // 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 ()) {
@@ -536,6 +536,11 @@ ParticleEmitter ObjectParser::parseParticleEmitter (const JSON& it) {
         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<float> ();
+            return glm::vec3 (val, val, val);
+        }
         if (fieldIt->is_array () && fieldIt->size () >= 3) {
             return glm::vec3 (
                 (*fieldIt)[0].get<float> (),
@@ -574,7 +579,7 @@ ParticleEmitter ObjectParser::parseParticleEmitter (const JSON& it) {
             .speedMin = it.optional ("speedmin", 0.0f),
             .speedMax = it.optional ("speedmax", 0.0f),
             .rate = it.optional ("rate", 5.0f),
-            .controlPoint = it.optional ("controlpoint", 0),
+            .controlPoint = it.optional ("controlpoint", -1),
             .flags = it.optional ("flags", 0u),
         };
     } catch (nlohmann::json::exception& e) {
@@ -608,7 +613,8 @@ ParticleInitializerUniquePtr ObjectParser::parseParticleInitializer (const JSON&
     } else if (name == "sizerandom") {
         return std::make_unique<SizeRandomInitializer> (
             it.user ("min", properties, 0.0f),
-            it.user ("max", properties, 20.0f)
+            it.user ("max", properties, 20.0f),
+            it.user ("exponent", properties, 1.0f)
         );
     } else if (name == "alpharandom") {
         return std::make_unique<AlphaRandomInitializer> (
@@ -635,6 +641,20 @@ ParticleInitializerUniquePtr ObjectParser::parseParticleInitializer (const JSON&
             it.user ("min", properties, glm::vec3 (0.0f, 0.0f, -5.0f)),
             it.user ("max", properties, glm::vec3 (0.0f, 0.0f, 5.0f))
         );
+    } else if (name == "turbulentvelocityrandom") {
+        return std::make_unique<TurbulentVelocityRandomInitializer> (
+            it.user ("speedmin", properties, 0.0f),
+            it.user ("speedmax", properties, 100.0f),
+            it.user ("scale", properties, 1.0f),
+            it.user ("offset", properties, 0.0f)
+        );
+    } else if (name == "mapsequencearoundcontrolpoint") {
+        return std::make_unique<MapSequenceAroundControlPointInitializer> (
+            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;
@@ -679,6 +699,35 @@ ParticleOperatorUniquePtr ObjectParser::parseParticleOperator (const JSON& it, c
             it.user ("startvalue", properties, glm::vec3 (1.0f)),
             it.user ("endvalue", properties, glm::vec3 (1.0f))
         );
+    } else if (name == "turbulence") {
+        return std::make_unique<TurbulenceOperator> (
+            it.user ("scale", properties, 0.005f),
+            it.user ("speedmin", properties, 0.0f),
+            it.user ("speedmax", properties, 1000.0f),
+            it.user ("timescale", properties, 1.0f),
+            it.user ("audioprocessingmode", properties, 0),  // 0 = no audio processing
+            it.user ("audioprocessingbounds", properties, glm::vec2(0.0f, 1.0f)),
+            it.user ("audioprocessingfrequencyend", properties, 64)  // Default to full spectrum
+        );
+    } else if (name == "vortex") {
+        return std::make_unique<VortexOperator> (
+            it.optional ("controlpoint", 0),
+            it.user ("axis", properties, glm::vec3 (0.0f, 0.0f, 1.0f)),
+            it.user ("offset", properties, glm::vec3 (0.0f)),
+            it.user ("distanceinner", properties, 0.0f),
+            it.user ("distanceouter", properties, 1000.0f),
+            it.user ("speedinner", properties, 100.0f),
+            it.user ("speedouter", properties, 0.0f),
+            it.user ("audioprocessingmode", properties, 0),
+            it.user ("audioprocessingbounds", properties, glm::vec2(0.0f, 1.0f))
+        );
+    } else if (name == "controlpointattract") {
+        return std::make_unique<ControlPointAttractOperator> (
+            it.optional ("controlpoint", 0),
+            it.user ("origin", properties, glm::vec3 (0.0f)),
+            it.user ("scale", properties, 100.0f),
+            it.user ("threshold", properties, 1000.0f)
+        );
     }
 
     return nullptr;
@@ -723,6 +772,7 @@ ParticleControlPoint ObjectParser::parseParticleControlPoint (const JSON& it) {
         .id = it.optional ("id", -1),
         .flags = it.optional ("flags", 0u),
         .offset = offset,
+        .lockToPointer = it.optional ("locktopointer", false),
     };
 }
 
@@ -745,7 +795,7 @@ ParticleChild ObjectParser::parseParticleChild (const JSON& it, const Project& p
         name = nameIt->get<std::string> ();
     }
 
-    // Helper lambda to parse vec3 fields
+    // 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 ()) {
@@ -754,6 +804,11 @@ ParticleChild ObjectParser::parseParticleChild (const JSON& it, const Project& p
         if (fieldIt->is_string ()) {
             return it.optional (fieldName, defaultValue);
         }
+        if (fieldIt->is_number ()) {
+            // Single number - use for all components
+            float val = fieldIt->get<float> ();
+            return glm::vec3 (val, val, val);
+        }
         if (fieldIt->is_array () && fieldIt->size () >= 3) {
             return glm::vec3 (
                 (*fieldIt)[0].get<float> (),

+ 28 - 0
src/WallpaperEngine/Data/Parsers/TextureParser.cpp

@@ -218,6 +218,34 @@ void TextureParser::parseAnimations (Texture& header, const BinaryReader& file)
         header.gifWidth = (*header.frames.begin ())->width1;
         header.gifHeight = (*header.frames.begin ())->height1;
     }
+
+    // Calculate spritesheet grid dimensions from animation frames
+    // Spritesheets are grid-based textures where each frame is at a specific position
+    if (!header.frames.empty () && header.width > 0 && header.height > 0) {
+        auto& firstFrame = *header.frames.front ();
+        float frameWidth = firstFrame.width1;
+        float frameHeight = firstFrame.height1;
+
+        if (frameWidth > 0.0f && frameHeight > 0.0f) {
+            const uint32_t cols = static_cast<uint32_t> (std::round (static_cast<double> (header.width) / frameWidth));
+            const uint32_t rows = static_cast<uint32_t> (std::round (static_cast<double> (header.height) / frameHeight));
+            const uint32_t frameCount = static_cast<uint32_t> (header.frames.size ());
+
+            // Only populate spritesheet metadata if the inferred grid can actually hold all frames
+            // This prevents GIFs (where frameWidth == textureWidth) from being treated as 1×1 spritesheets
+            if (cols > 0 && rows > 0 && cols * rows >= frameCount) {
+                header.spritesheetCols = cols;
+                header.spritesheetRows = rows;
+                header.spritesheetFrames = frameCount;
+
+                float totalDuration = 0.0f;
+                for (const auto& frame : header.frames) {
+                    totalDuration += frame->frametime;
+                }
+                header.spritesheetDuration = totalDuration;
+            }
+        }
+    }
 }
 
 uint32_t TextureParser::parseTextureFlags (uint32_t value) {

+ 4 - 0
src/WallpaperEngine/Render/CWallpaper.cpp

@@ -259,6 +259,10 @@ AudioContext& CWallpaper::getAudioContext () const {
     return this->m_audioContext;
 }
 
+const WallpaperState& CWallpaper::getState () const {
+    return this->m_state;
+}
+
 std::shared_ptr<const CFBO> CWallpaper::findFBO (const std::string& name) const {
     const auto fbo = this->find (name);
 

+ 5 - 0
src/WallpaperEngine/Render/CWallpaper.h

@@ -77,6 +77,11 @@ class CWallpaper : public Helpers::ContextAware, public FBOProvider {
      */
     AudioContext& getAudioContext () const;
 
+    /**
+     * @return The wallpaper state
+     */
+    [[nodiscard]] const WallpaperState& getState () const;
+
     /**
      * @return The scene's framebuffer
      */

+ 5 - 4
src/WallpaperEngine/Render/Objects/CImage.cpp

@@ -292,7 +292,7 @@ void CImage::setup () {
                         continue;
                     }
 
-                    const auto virtualPass = MaterialPass {
+                    auto virtualPass = std::make_unique<MaterialPass>(MaterialPass {
                         .blending = BlendingMode_Normal,
                         .cullmode = CullingMode_Disable,
                         .depthtest = DepthtestMode_Disabled,
@@ -301,10 +301,11 @@ void CImage::setup () {
                         .textures = {
                             {0, *(*curEffect)->source}
                         },
-                        .combos = {}
-                    };
+                        .combos = {},
+                        .constants = {}
+                    });
 
-                    const auto& config = this->m_virtualPassess.emplace_back (virtualPass);
+                    const auto& config = *this->m_virtualPassess.emplace_back (std::move(virtualPass));
 
                     // build a pass for a copy shader
                     this->m_passes.push_back (

+ 1 - 1
src/WallpaperEngine/Render/Objects/CImage.h

@@ -85,7 +85,7 @@ class CImage final : public CObject, public FBOProvider {
     Effects::CMaterial* m_material = nullptr;
     Effects::CMaterial* m_colorBlendMaterial = nullptr;
     std::vector<Effects::CPass*> m_passes = {};
-    std::vector<MaterialPass> m_virtualPassess = {};
+    std::vector<MaterialPassUniquePtr> m_virtualPassess = {};
 
     glm::vec4 m_pos = {};
 

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 667 - 70
src/WallpaperEngine/Render/Objects/CParticle.cpp


+ 29 - 1
src/WallpaperEngine/Render/Objects/CParticle.h

@@ -16,7 +16,7 @@ using namespace WallpaperEngine::Data::Model;
 
 namespace WallpaperEngine::Render::Objects {
 
-constexpr uint32_t MAX_PARTICLES = 10000;
+constexpr uint32_t DEFAULT_MAX_PARTICLES = 1000;
 
 /**
  * Runtime particle instance state
@@ -42,6 +42,9 @@ struct ParticleInstance {
     float lifetime {1.0f};      // Total lifetime in seconds
     float age {0.0f};           // Current age in seconds
 
+    // Turbulent velocity state
+    glm::vec3 noisePos {0.0f};  // Position in noise field for turbulent velocity
+
     // Initial values for resets/multipliers
     struct {
         glm::vec3 color {1.0f};
@@ -117,6 +120,8 @@ class CParticle final : public CObject {
     InitializerFunc createVelocityRandomInitializer (const VelocityRandomInitializer& init);
     InitializerFunc createRotationRandomInitializer (const RotationRandomInitializer& init);
     InitializerFunc createAngularVelocityRandomInitializer (const AngularVelocityRandomInitializer& init);
+    InitializerFunc createTurbulentVelocityRandomInitializer (const TurbulentVelocityRandomInitializer& init);
+    InitializerFunc createMapSequenceAroundControlPointInitializer (const MapSequenceAroundControlPointInitializer& init);
 
     // Operator creators
     OperatorFunc createMovementOperator (const MovementOperator& op);
@@ -125,6 +130,9 @@ class CParticle final : public CObject {
     OperatorFunc createSizeChangeOperator (const SizeChangeOperator& op);
     OperatorFunc createAlphaChangeOperator (const AlphaChangeOperator& op);
     OperatorFunc createColorChangeOperator (const ColorChangeOperator& op);
+    OperatorFunc createTurbulenceOperator (const TurbulenceOperator& op);
+    OperatorFunc createVortexOperator (const VortexOperator& op);
+    OperatorFunc createControlPointAttractOperator (const ControlPointAttractOperator& op);
 
     // Rendering
     void renderSprites ();
@@ -135,6 +143,7 @@ class CParticle final : public CObject {
 
     std::vector<ParticleInstance> m_particles;
     uint32_t m_particleCount {0};
+    uint32_t m_maxParticles {DEFAULT_MAX_PARTICLES};
 
     std::vector<EmitterFunc> m_emitters;
     std::vector<InitializerFunc> m_initializers;
@@ -147,6 +156,7 @@ class CParticle final : public CObject {
     // OpenGL buffers
     GLuint m_vao {0};
     GLuint m_vbo {0};
+    GLuint m_ebo {0}; // Element Buffer Object for indexed rendering
     GLuint m_shaderProgram {0};
 
     // Cached uniform locations
@@ -154,6 +164,11 @@ class CParticle final : public CObject {
     GLint m_uniformHasTexture {-1};
     GLint m_uniformTextureFormat {-1};
     GLint m_uniformSpritesheetSize {-1};
+    GLint m_uniformOverbright {-1};
+    GLint m_uniformUseTrailRenderer {-1};
+    GLint m_uniformTrailLength {-1};
+    GLint m_uniformTrailMaxLength {-1};
+    GLint m_uniformTextureRatio {-1};
 
     // Particle material texture
     std::shared_ptr<const TextureProvider> m_texture {nullptr};
@@ -166,9 +181,22 @@ class CParticle final : public CObject {
     int m_spritesheetFrames {0};
     float m_spritesheetDuration {1.0f};
 
+    // Material shader constants
+    float m_overbright {1.0f};  // Brightness multiplier for additive particles
+
+    // Renderer configuration
+    bool m_useTrailRenderer {false};
+    float m_trailLength {0.05f};
+    float m_trailMaxLength {10.0f};
+    int m_trailSubdivision {3}; // Number of segments per trail
+
     // Transformed origin (screen space to centered space conversion)
     glm::vec3 m_transformedOrigin {0.0f};
 
+    // Last known resolution for detecting changes
+    float m_lastScreenWidth {0.0f};
+    float m_lastScreenHeight {0.0f};
+
     // Random number generator
     std::mt19937 m_rng;
 

+ 141 - 0
src/WallpaperEngine/Render/Utils/NoiseUtils.h

@@ -0,0 +1,141 @@
+#pragma once
+
+#include <glm/glm.hpp>
+#include <cmath>
+
+namespace WallpaperEngine::Render::Utils {
+
+// Perlin noise permutation table
+static const unsigned char PERLIN_PERM[] = {
+    151, 160, 137, 91,  90,  15,  131, 13,  201, 95,  96,  53,  194, 233, 7,   225, 140, 36,
+    103, 30,  69,  142, 8,   99,  37,  240, 21,  10,  23,  190, 6,   148, 247, 120, 234, 75,
+    0,   26,  197, 62,  94,  252, 219, 203, 117, 35,  11,  32,  57,  177, 33,  88,  237, 149,
+    56,  87,  174, 20,  125, 136, 171, 168, 68,  175, 74,  165, 71,  134, 139, 48,  27,  166,
+    77,  146, 158, 231, 83,  111, 229, 122, 60,  211, 133, 230, 220, 105, 92,  41,  55,  46,
+    245, 40,  244, 102, 143, 54,  65,  25,  63,  161, 1,   216, 80,  73,  209, 76,  132, 187,
+    208, 89,  18,  169, 200, 196, 135, 130, 116, 188, 159, 86,  164, 100, 109, 198, 173, 186,
+    3,   64,  52,  217, 226, 250, 124, 123, 5,   202, 38,  147, 118, 126, 255, 82,  85,  212,
+    207, 206, 59,  227, 47,  16,  58,  17,  182, 189, 28,  42,  223, 183, 170, 213, 119, 248,
+    152, 2,   44,  154, 163, 70,  221, 153, 101, 155, 167, 43,  172, 9,   129, 22,  39,  253,
+    19,  98,  108, 110, 79,  113, 224, 232, 178, 185, 112, 104, 218, 246, 97,  228, 251, 34,
+    242, 193, 238, 210, 144, 12,  191, 179, 162, 241, 81,  51,  145, 235, 249, 14,  239, 107,
+    49,  192, 214, 31,  181, 199, 106, 157, 184, 84,  204, 176, 115, 121, 50,  45,  127, 4,
+    150, 254, 138, 236, 205, 93,  222, 114, 67,  29,  24,  72,  243, 141, 128, 195, 78,  66,
+    215, 61,  156, 180,
+    // Duplicate for wrapping
+    151, 160, 137, 91,  90,  15,  131, 13,  201, 95,  96,  53,  194, 233, 7,   225, 140, 36,
+    103, 30,  69,  142, 8,   99,  37,  240, 21,  10,  23,  190, 6,   148, 247, 120, 234, 75,
+    0,   26,  197, 62,  94,  252, 219, 203, 117, 35,  11,  32,  57,  177, 33,  88,  237, 149,
+    56,  87,  174, 20,  125, 136, 171, 168, 68,  175, 74,  165, 71,  134, 139, 48,  27,  166,
+    77,  146, 158, 231, 83,  111, 229, 122, 60,  211, 133, 230, 220, 105, 92,  41,  55,  46,
+    245, 40,  244, 102, 143, 54,  65,  25,  63,  161, 1,   216, 80,  73,  209, 76,  132, 187,
+    208, 89,  18,  169, 200, 196, 135, 130, 116, 188, 159, 86,  164, 100, 109, 198, 173, 186,
+    3,   64,  52,  217, 226, 250, 124, 123, 5,   202, 38,  147, 118, 126, 255, 82,  85,  212,
+    207, 206, 59,  227, 47,  16,  58,  17,  182, 189, 28,  42,  223, 183, 170, 213, 119, 248,
+    152, 2,   44,  154, 163, 70,  221, 153, 101, 155, 167, 43,  172, 9,   129, 22,  39,  253,
+    19,  98,  108, 110, 79,  113, 224, 232, 178, 185, 112, 104, 218, 246, 97,  228, 251, 34,
+    242, 193, 238, 210, 144, 12,  191, 179, 162, 241, 81,  51,  145, 235, 249, 14,  239, 107,
+    49,  192, 214, 31,  181, 199, 106, 157, 184, 84,  204, 176, 115, 121, 50,  45,  127, 4,
+    150, 254, 138, 236, 205, 93,  222, 114, 67,  29,  24,  72,  243, 141, 128, 195, 78,  66,
+    215, 61,  156, 180
+};
+
+// Perlin noise gradient function
+inline double perlinGrad(int hash, double x, double y, double z) {
+    switch (hash & 0xF) {
+        case 0x0: return  x + y;
+        case 0x1: return -x + y;
+        case 0x2: return  x - y;
+        case 0x3: return -x - y;
+        case 0x4: return  x + z;
+        case 0x5: return -x + z;
+        case 0x6: return  x - z;
+        case 0x7: return -x - z;
+        case 0x8: return  y + z;
+        case 0x9: return -y + z;
+        case 0xA: return  y - z;
+        case 0xB: return -y - z;
+        case 0xC: return  y + x;
+        case 0xD: return -y + z;
+        case 0xE: return  y - x;
+        case 0xF: return -y - z;
+        default: return 0;
+    }
+}
+
+// Perlin noise ease curve (6t^5 - 15t^4 + 10t^3)
+inline double perlinEase(double t) {
+    return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
+}
+
+// Linear interpolation
+inline double lerpDouble(double t, double a, double b) {
+    return a + t * (b - a);
+}
+
+// Perlin noise implementation
+inline double perlinNoise(double x, double y, double z) {
+    int X = static_cast<int>(std::floor(x)) & 255;
+    int Y = static_cast<int>(std::floor(y)) & 255;
+    int Z = static_cast<int>(std::floor(z)) & 255;
+
+    x -= std::floor(x);
+    y -= std::floor(y);
+    z -= std::floor(z);
+
+    double u = perlinEase(x);
+    double v = perlinEase(y);
+    double w = perlinEase(z);
+
+    int A  = PERLIN_PERM[X] + Y;
+    int AA = PERLIN_PERM[A] + Z;
+    int AB = PERLIN_PERM[A + 1] + Z;
+    int B  = PERLIN_PERM[X + 1] + Y;
+    int BA = PERLIN_PERM[B] + Z;
+    int BB = PERLIN_PERM[B + 1] + Z;
+
+    return lerpDouble(w,
+        lerpDouble(v,
+            lerpDouble(u, perlinGrad(PERLIN_PERM[AA], x, y, z),
+                    perlinGrad(PERLIN_PERM[BA], x - 1, y, z)),
+            lerpDouble(u, perlinGrad(PERLIN_PERM[AB], x, y - 1, z),
+                    perlinGrad(PERLIN_PERM[BB], x - 1, y - 1, z))),
+        lerpDouble(v,
+            lerpDouble(u, perlinGrad(PERLIN_PERM[AA + 1], x, y, z - 1),
+                    perlinGrad(PERLIN_PERM[BA + 1], x - 1, y, z - 1)),
+            lerpDouble(u, perlinGrad(PERLIN_PERM[AB + 1], x, y - 1, z - 1),
+                    perlinGrad(PERLIN_PERM[BB + 1], x - 1, y - 1, z - 1))));
+}
+
+// Perlin noise vec3 (3 independent noise samples with different offsets)
+inline glm::vec3 perlinNoiseVec3(const glm::vec3& p) {
+    return glm::vec3(
+        static_cast<float>(perlinNoise(p.x, p.y, p.z)),
+        static_cast<float>(perlinNoise(p.x + 89.2, p.y + 33.1, p.z + 57.3)),
+        static_cast<float>(perlinNoise(p.x + 100.3, p.y + 120.1, p.z + 142.2))
+    );
+}
+
+// Curl noise - smooth, swirling patterns ideal for fluid-like particle motion
+inline glm::vec3 curlNoise(const glm::vec3& p) {
+    const float e = 1e-4f;
+
+    glm::vec3 dx(e, 0, 0);
+    glm::vec3 dy(0, e, 0);
+    glm::vec3 dz(0, 0, e);
+
+    glm::vec3 x0 = perlinNoiseVec3(p - dx);
+    glm::vec3 x1 = perlinNoiseVec3(p + dx);
+    glm::vec3 y0 = perlinNoiseVec3(p - dy);
+    glm::vec3 y1 = perlinNoiseVec3(p + dy);
+    glm::vec3 z0 = perlinNoiseVec3(p - dz);
+    glm::vec3 z1 = perlinNoiseVec3(p + dz);
+
+    float x = (y1.z - y0.z) - (z1.y - z0.y);
+    float y = (z1.x - z0.x) - (x1.z - x0.z);
+    float z = (x1.y - x0.y) - (y1.x - y0.x);
+
+    return glm::vec3(x, y, z) / (2.0f * e);
+}
+
+} // namespace WallpaperEngine::Render::Utils

+ 11 - 5
src/WallpaperEngine/Render/Wallpapers/CScene.cpp

@@ -278,16 +278,22 @@ void CScene::renderFrame (const glm::ivec4& viewport) {
 void CScene::updateMouse (const glm::ivec4& viewport) {
     // update virtual mouse position first
     const glm::dvec2 position = this->getContext ().getInputContext ().getMouseInput ().position ();
-    // TODO: PROPERLY TRANSLATE THESE TO WHAT'S VISIBLE ON SCREEN (FOR BACKGROUNDS THAT DO NOT EXACTLY FIT ON SCREEN)
 
     // rollover the position to the last
     this->m_mousePositionLast = this->m_mousePosition;
 
-    // calculate the current position of the mouse
-    this->m_mousePosition.x = glm::clamp ((position.x - viewport.x) / viewport.z, 0.0, 1.0);
-    this->m_mousePosition.y = glm::clamp ((position.y - viewport.y) / viewport.w, 0.0, 1.0);
+    // calculate the current position of the mouse in viewport space [0, 1]
+    double mouseX = glm::clamp ((position.x - viewport.x) / viewport.z, 0.0, 1.0);
+    double mouseY = glm::clamp ((position.y - viewport.y) / viewport.w, 0.0, 1.0);
 
-    // screen-space positions have to be transposed to what the screen will actually show
+    // Account for UV cropping when using fill/fit scaling modes
+    // The scene may be rendered larger than viewport and cropped via UVs
+    const auto uvs = this->getState ().getTextureUVs ();
+
+    // Map mouse position from viewport space to scene UV space
+    // UVs define what portion of the scene texture is visible
+    this->m_mousePosition.x = uvs.ustart + mouseX * (uvs.uend - uvs.ustart);
+    this->m_mousePosition.y = uvs.vstart + mouseY * (uvs.vend - uvs.vstart);
 }
 
 const Scene& CScene::getScene () const {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно