From 231942be66172b876207143f19f7e6f777fb4e1b Mon Sep 17 00:00:00 2001 From: gustebeast Date: Thu, 29 Jan 2026 21:58:37 +0000 Subject: [PATCH 1/2] Introduce comet effect usermod with fire particle system --- usermods/PS_Comet/PS_Comet.cpp | 130 +++++++++++++++++++++++++++++++++ usermods/PS_Comet/README.md | 25 +++++++ usermods/PS_Comet/library.json | 4 + 3 files changed, 159 insertions(+) create mode 100644 usermods/PS_Comet/PS_Comet.cpp create mode 100644 usermods/PS_Comet/README.md create mode 100644 usermods/PS_Comet/library.json diff --git a/usermods/PS_Comet/PS_Comet.cpp b/usermods/PS_Comet/PS_Comet.cpp new file mode 100644 index 0000000000..4491bf2ddc --- /dev/null +++ b/usermods/PS_Comet/PS_Comet.cpp @@ -0,0 +1,130 @@ +#include "wled.h" +#include "FXparticleSystem.h" + +unsigned long nextCometCreationTime = 0; + +static bool has_particle_fallen_off_screen(const ParticleSystem2D* PartSys, const PSparticle& particle) { + return particle.y < PartSys->maxY * -1; +} + +static bool has_fallen_off_screen(const ParticleSystem2D* PartSys, uint32_t particleIndex) { + return has_particle_fallen_off_screen(PartSys, PartSys->sources[particleIndex].source); +} + +static uint16_t mode_static(void) { + SEGMENT.fill(SEGCOLOR(0)); + return strip.isOffRefreshRequired() ? FRAMETIME : 350; +} + +/////////////////////// +// Effect Function // +/////////////////////// + +uint16_t mode_pscomet(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + // Allocate three comets for every column to allow for large comets with side emitters + uint32_t numComets = SEGMENT.vWidth() * 3; + + if (SEGMENT.call == 0) { // Initialization + if (!initParticleSystem2D(PartSys, numComets)) { + return mode_static(); // Allocation failed or not 2D + } + PartSys->setMotionBlur(170); // Enable motion blur + PartSys->setParticleSize(0); // Allow small comets to be a single pixel wide + } + else { + PartSys = reinterpret_cast(SEGENV.data); // If not first call, use existing data + } + if (PartSys == nullptr) { + return mode_static(); // Something went wrong, no data! + } + + PartSys->updateSystem(); // Update system properties (dimensions and data pointers) + + uint32_t sideCometVerticalShift = 2 * PartSys->maxY / (SEGMENT.vHeight() - 1); // Shift emitters on left and right of comet up by 1 pixel + uint32_t chosenIndex = hw_random(numComets); // Only allow one column to spawn a comet each frame + uint8_t fallingSpeed = 1 + (SEGMENT.speed >> 2); + uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3); + + // Update the comets + for (i = 0; i < numComets; i++) { + auto& source = PartSys->sources[i]; + auto& sourceParticle = source.source; + // Large comets use three particle sources, with i indicating left, center or right: + // 0: left, 1: center, 2: right, 3: left, 4: center, 5: right, ... + // Small comets leave the left and right indices unused + bool isCometCenter = (i % 3) == 1; + + // Map the 3x comet index into an output pixel index + sourceParticle.x = ((i / 3) + (i % 3)) * PartSys->maxX / (SEGMENT.vWidth() - 1); + if (isCometCenter) { + // Slider 4 controls comet length via particle lifetime and fire intensity adjustments + source.maxLife = 16 + (SEGMENT.custom2 >> 2); + sourceParticle.ttl = 16 - (SEGMENT.custom2 >> 4); + } else { + source.maxLife = 16; + sourceParticle.ttl = 16; + } + source.minLife = source.maxLife >> 1; + source.vy = 1 + (SEGMENT.speed >> 5); // Emitting speed (upwards) + + bool hasFallenOffScreen = has_fallen_off_screen(PartSys, i); + + if (!hasFallenOffScreen) { + // Active comets fall downwards and emit flames + sourceParticle.y -= fallingSpeed; + PartSys->flameEmit(PartSys->sources[i]); + } + + // Inactive comets have a random chance to respawn at the top + if (hasFallenOffScreen + && strip.now > nextCometCreationTime + && i == chosenIndex + && isCometCenter + // Ensure there are no comets too close to the left + && (i < 3 || has_fallen_off_screen(PartSys, i - 3)) + // And to the right + && (i > numComets - 4 || has_fallen_off_screen(PartSys, i + 3)) + ) { + // Spawn a bit above the top to avoid popping into view + sourceParticle.y = PartSys->maxY + (2 * fallingSpeed); + // Slider 3 determines % of large comets with extra particle sources on their sides + if (hw_random8(254) < SEGMENT.custom1) { + PartSys->sources[i - 1].source.y = PartSys->maxY + sideCometVerticalShift; // left comet + PartSys->sources[i + 1].source.y = PartSys->maxY + sideCometVerticalShift; // right comet + } + nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay); + } + } + + // To avoid stretching comets at faster falling speeds, we need to shift the emitted particles as well + for (i = 0; i < PartSys->usedParticles; i++) { + auto& particle = PartSys->particles[i]; + if (!has_particle_fallen_off_screen(PartSys, particle)) { + particle.y -= fallingSpeed; + } + } + + // Slider 4 controls comet length via particle lifetime and fire intensity adjustments + PartSys->updateFire(max(255 - SEGMENT.custom2, 45), false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PSCOMET[] PROGMEM = "PS Comet@Falling Speed,Comet Frequency,Large Comet Probability,Comet Length;;!;2;pal=35,sx=128,ix=255,c1=32,c2=128"; + +///////////////////// +// UserMod Class // +///////////////////// + +class PSCometUsermod : public Usermod { + public: + void setup() override { + strip.addEffect(255, &mode_pscomet, _data_FX_MODE_PSCOMET); + } + + void loop() override {} +}; + +static PSCometUsermod ps_comet; +REGISTER_USERMOD(ps_comet); diff --git a/usermods/PS_Comet/README.md b/usermods/PS_Comet/README.md new file mode 100644 index 0000000000..c42618eed2 --- /dev/null +++ b/usermods/PS_Comet/README.md @@ -0,0 +1,25 @@ +## Description + +A 2D falling comet effect similar to "Matrix" but with a fire particle simulation to enhance the comet trail visuals. Works with custom color palletes, defaulting to "Fire". Supports "small" and "large" comets which are 1px and 3px wide respectively. + +Demo: [https://imgur.com/a/i1v5WAy](https://imgur.com/a/i1v5WAy) + +## Installation + +To activate the usermod, add the following line to your platformio_override.ini +```ini +custom_usermods = ps_comet +``` +Or if you are already using a usermod, append ps_comet to the list +```ini +custom_usermods = audioreactive, ps_comet +``` + +You should now see "PS Comet" appear in your effect list. + +## Parameters + +1. **Falling Speed** sets how fast the comets fall +2. **Comet Frequency** determines how many comets are on screen at a time +3. **Large Comet Probability** determines how often large 3px wide comets spawn +4. **Comet Length** sets how far comet trails stretch vertically \ No newline at end of file diff --git a/usermods/PS_Comet/library.json b/usermods/PS_Comet/library.json new file mode 100644 index 0000000000..d6f569387c --- /dev/null +++ b/usermods/PS_Comet/library.json @@ -0,0 +1,4 @@ +{ + "name": "PS Comet", + "build": { "libArchive": false } +} \ No newline at end of file From b63d274f93785625dddda95fa4f4ff93a126910d Mon Sep 17 00:00:00 2001 From: gustebeast Date: Thu, 29 Jan 2026 21:58:37 +0000 Subject: [PATCH 2/2] Introduce comet effect usermod with fire particle system --- usermods/PS_Comet/PS_Comet.cpp | 130 +++++++++++++++++++++++++++++++++ usermods/PS_Comet/README.md | 25 +++++++ usermods/PS_Comet/library.json | 4 + 3 files changed, 159 insertions(+) create mode 100644 usermods/PS_Comet/PS_Comet.cpp create mode 100644 usermods/PS_Comet/README.md create mode 100644 usermods/PS_Comet/library.json diff --git a/usermods/PS_Comet/PS_Comet.cpp b/usermods/PS_Comet/PS_Comet.cpp new file mode 100644 index 0000000000..4491bf2ddc --- /dev/null +++ b/usermods/PS_Comet/PS_Comet.cpp @@ -0,0 +1,130 @@ +#include "wled.h" +#include "FXparticleSystem.h" + +unsigned long nextCometCreationTime = 0; + +static bool has_particle_fallen_off_screen(const ParticleSystem2D* PartSys, const PSparticle& particle) { + return particle.y < PartSys->maxY * -1; +} + +static bool has_fallen_off_screen(const ParticleSystem2D* PartSys, uint32_t particleIndex) { + return has_particle_fallen_off_screen(PartSys, PartSys->sources[particleIndex].source); +} + +static uint16_t mode_static(void) { + SEGMENT.fill(SEGCOLOR(0)); + return strip.isOffRefreshRequired() ? FRAMETIME : 350; +} + +/////////////////////// +// Effect Function // +/////////////////////// + +uint16_t mode_pscomet(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + // Allocate three comets for every column to allow for large comets with side emitters + uint32_t numComets = SEGMENT.vWidth() * 3; + + if (SEGMENT.call == 0) { // Initialization + if (!initParticleSystem2D(PartSys, numComets)) { + return mode_static(); // Allocation failed or not 2D + } + PartSys->setMotionBlur(170); // Enable motion blur + PartSys->setParticleSize(0); // Allow small comets to be a single pixel wide + } + else { + PartSys = reinterpret_cast(SEGENV.data); // If not first call, use existing data + } + if (PartSys == nullptr) { + return mode_static(); // Something went wrong, no data! + } + + PartSys->updateSystem(); // Update system properties (dimensions and data pointers) + + uint32_t sideCometVerticalShift = 2 * PartSys->maxY / (SEGMENT.vHeight() - 1); // Shift emitters on left and right of comet up by 1 pixel + uint32_t chosenIndex = hw_random(numComets); // Only allow one column to spawn a comet each frame + uint8_t fallingSpeed = 1 + (SEGMENT.speed >> 2); + uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3); + + // Update the comets + for (i = 0; i < numComets; i++) { + auto& source = PartSys->sources[i]; + auto& sourceParticle = source.source; + // Large comets use three particle sources, with i indicating left, center or right: + // 0: left, 1: center, 2: right, 3: left, 4: center, 5: right, ... + // Small comets leave the left and right indices unused + bool isCometCenter = (i % 3) == 1; + + // Map the 3x comet index into an output pixel index + sourceParticle.x = ((i / 3) + (i % 3)) * PartSys->maxX / (SEGMENT.vWidth() - 1); + if (isCometCenter) { + // Slider 4 controls comet length via particle lifetime and fire intensity adjustments + source.maxLife = 16 + (SEGMENT.custom2 >> 2); + sourceParticle.ttl = 16 - (SEGMENT.custom2 >> 4); + } else { + source.maxLife = 16; + sourceParticle.ttl = 16; + } + source.minLife = source.maxLife >> 1; + source.vy = 1 + (SEGMENT.speed >> 5); // Emitting speed (upwards) + + bool hasFallenOffScreen = has_fallen_off_screen(PartSys, i); + + if (!hasFallenOffScreen) { + // Active comets fall downwards and emit flames + sourceParticle.y -= fallingSpeed; + PartSys->flameEmit(PartSys->sources[i]); + } + + // Inactive comets have a random chance to respawn at the top + if (hasFallenOffScreen + && strip.now > nextCometCreationTime + && i == chosenIndex + && isCometCenter + // Ensure there are no comets too close to the left + && (i < 3 || has_fallen_off_screen(PartSys, i - 3)) + // And to the right + && (i > numComets - 4 || has_fallen_off_screen(PartSys, i + 3)) + ) { + // Spawn a bit above the top to avoid popping into view + sourceParticle.y = PartSys->maxY + (2 * fallingSpeed); + // Slider 3 determines % of large comets with extra particle sources on their sides + if (hw_random8(254) < SEGMENT.custom1) { + PartSys->sources[i - 1].source.y = PartSys->maxY + sideCometVerticalShift; // left comet + PartSys->sources[i + 1].source.y = PartSys->maxY + sideCometVerticalShift; // right comet + } + nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay); + } + } + + // To avoid stretching comets at faster falling speeds, we need to shift the emitted particles as well + for (i = 0; i < PartSys->usedParticles; i++) { + auto& particle = PartSys->particles[i]; + if (!has_particle_fallen_off_screen(PartSys, particle)) { + particle.y -= fallingSpeed; + } + } + + // Slider 4 controls comet length via particle lifetime and fire intensity adjustments + PartSys->updateFire(max(255 - SEGMENT.custom2, 45), false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PSCOMET[] PROGMEM = "PS Comet@Falling Speed,Comet Frequency,Large Comet Probability,Comet Length;;!;2;pal=35,sx=128,ix=255,c1=32,c2=128"; + +///////////////////// +// UserMod Class // +///////////////////// + +class PSCometUsermod : public Usermod { + public: + void setup() override { + strip.addEffect(255, &mode_pscomet, _data_FX_MODE_PSCOMET); + } + + void loop() override {} +}; + +static PSCometUsermod ps_comet; +REGISTER_USERMOD(ps_comet); diff --git a/usermods/PS_Comet/README.md b/usermods/PS_Comet/README.md new file mode 100644 index 0000000000..c42618eed2 --- /dev/null +++ b/usermods/PS_Comet/README.md @@ -0,0 +1,25 @@ +## Description + +A 2D falling comet effect similar to "Matrix" but with a fire particle simulation to enhance the comet trail visuals. Works with custom color palletes, defaulting to "Fire". Supports "small" and "large" comets which are 1px and 3px wide respectively. + +Demo: [https://imgur.com/a/i1v5WAy](https://imgur.com/a/i1v5WAy) + +## Installation + +To activate the usermod, add the following line to your platformio_override.ini +```ini +custom_usermods = ps_comet +``` +Or if you are already using a usermod, append ps_comet to the list +```ini +custom_usermods = audioreactive, ps_comet +``` + +You should now see "PS Comet" appear in your effect list. + +## Parameters + +1. **Falling Speed** sets how fast the comets fall +2. **Comet Frequency** determines how many comets are on screen at a time +3. **Large Comet Probability** determines how often large 3px wide comets spawn +4. **Comet Length** sets how far comet trails stretch vertically \ No newline at end of file diff --git a/usermods/PS_Comet/library.json b/usermods/PS_Comet/library.json new file mode 100644 index 0000000000..d6f569387c --- /dev/null +++ b/usermods/PS_Comet/library.json @@ -0,0 +1,4 @@ +{ + "name": "PS Comet", + "build": { "libArchive": false } +} \ No newline at end of file