Skip to content

Introduce comet effect usermod with fire particle system#5347

Open
gustebeast wants to merge 3 commits intowled:mainfrom
gustebeast:wled-gtb-ps-comet
Open

Introduce comet effect usermod with fire particle system#5347
gustebeast wants to merge 3 commits intowled:mainfrom
gustebeast:wled-gtb-ps-comet

Conversation

@gustebeast
Copy link
Contributor

@gustebeast gustebeast commented Feb 5, 2026

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

Summary by CodeRabbit

  • New Features

    • Added PS Comet effect: a 2D falling-comet particle animation with fire-style particles, small/large comet sizes, and configurable speed, frequency, size probability, and length.
    • Supports custom color palettes (default: Fire).
  • Documentation

    • Added README with installation steps, demo link, and parameter descriptions.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

Walkthrough

Adds a new PS Comet usermod: a 2D falling-comet particle effect implementation, a README with installation/parameters, and a library manifest. Introduces effect logic, a Usermod class with setup/loop, and registers the usermod with the framework.

Changes

Cohort / File(s) Summary
PS Comet implementation
usermods/PS_Comet/PS_Comet.cpp
New file adding mode_pscomet() which implements a 2D particle-based falling comet effect, particle spawn/movement/lifecycle, configurable parameters (speed, frequency, size), PSCometUsermod class with setup()/loop() overrides, and REGISTER_USERMOD(ps_comet) registration.
Documentation
usermods/PS_Comet/README.md
New README describing the effect, installation/activation instructions, demo link, and listed configurable parameters (Falling Speed, Comet Frequency, Large Comet Probability, Comet Length).
Package manifest
usermods/PS_Comet/library.json
New library manifest declaring the "PS Comet" package and build setting (libArchive: false).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • blazoncek
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing a new comet effect usermod with fire particle system, which directly matches the primary focus of the pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@usermods/PS_Comet/PS_Comet.cpp`:
- Around line 23-60: Guard against division-by-zero when computing pixel mapping
and vertical shifts by using a safe denominator (e.g., uint32_t denomW = max(1u,
SEGMENT.vWidth() - 1) and uint32_t denomH = max(1u, SEGMENT.vHeight() - 1)) and
replace any uses of (SEGMENT.vWidth() - 1) / (SEGMENT.vHeight() - 1) with those
safe denominators; fix the 3x->column mapping in mode_pscomet by computing
baseCol = i / 3, offset = (int)(i % 3) - 1 (so center = 0, left = -1, right =
+1), then targetCol = clamp(baseCol + offset, 0u, SEGMENT.vWidth() - 1) and set
sourceParticle.x = targetCol * PartSys->maxX / denomW; similarly compute
sideCometVerticalShift using denomH (uint32_t sideCometVerticalShift = 2 *
PartSys->maxY / denomH) and ensure all divisions use the safe denominators and
clamp results to valid ranges to avoid out-of-bounds writes.

In `@usermods/PS_Comet/README.md`:
- Line 3: Fix the spelling mistake "palletes" to "palettes" in the README
sentence describing color support (the phrase "Works with custom color palletes,
defaulting to "Fire""). Update that phrase to read "Works with custom color
palettes, defaulting to 'Fire'".
🧹 Nitpick comments (1)
usermods/PS_Comet/PS_Comet.cpp (1)

4-4: Avoid global spawn timer to prevent cross‑segment coupling.

nextCometCreationTime is global, so multiple segments running PS Comet will share a single spawn cadence. Consider storing this per segment (e.g., in SEGENV.aux0/aux1 or a small state struct tied to SEGENV.data) to keep animations independent.

@softhack007 softhack007 added the usermod usermod related label Feb 5, 2026
@softhack007 softhack007 requested a review from DedeHai February 5, 2026 10:12
@DedeHai
Copy link
Collaborator

DedeHai commented Feb 5, 2026

looks good in general, I'll add some comments to consider

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you enable gravity and enable killoutofbounds and let the PS handle the movement of your sources, you do not need this function and can just check if TTL=0. you can set gravity to 1 for a very slight acceleration only if that is not disturbing your intentions.
another option would be to set the initial TTL accordingly so your sources die once off-screen.
or just do this check inside the FX, or add a lambda function.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not make the emit speed handle this? the downside would be that the speed would not react immediately to to a slider change.

@DedeHai
Copy link
Collaborator

DedeHai commented Feb 5, 2026

I gave it a quick spin. Parameters may need some tuning: set all sliders except speed to max and it breaks the effect.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@usermods/PS_Comet/PS_Comet.cpp`:
- Line 4: The global variable nextCometCreationTime causes all segments to share
one spawn timer; replace this file-scope unsigned long with per-segment storage
by packing a uint32_t timestamp into SEGENV.aux0/aux1 or by creating a small
context struct (containing the ParticleSystem2D* and a uint32_t
nextCometCreationTime) and storing a pointer in SEGENV.data; update the comet
spawn logic (references to nextCometCreationTime) to read/write the per-segment
field from SEGENV.aux0/aux1 or the context in SEGENV.data and initialize that
per-segment storage where the ParticleSystem2D is created/attached.
- Around line 57-61: The current expression for canLargeCometSpawn calls
hw_random8(254) even when no comet will spawn (chosenIndex == NULL_INDEX),
wasting RNG; change the condition order or split it so chosenIndex is validated
first: check chosenIndex != NULL_INDEX (and the edge guards chosenIndex != 0 &&
chosenIndex != numComets - 1) before calling SEGMENT.custom1 > hw_random8(254).
Update the canLargeCometSpawn computation (or add a short if) so hw_random8(254)
is only invoked when chosenIndex is a real column index.
- Around line 54-55: When SEGMENT.intensity can make cometFrequencyDelay zero,
avoid calling hw_random16(0) and prevent scheduling comets every frame: clamp
cometFrequencyDelay to a minimum (e.g., 1) before calling hw_random16 or skip
the random offset when cometFrequencyDelay == 0 and instead add a safe non-zero
fallback to nextCometCreationTime; update the code that computes
cometFrequencyDelay, the hw_random16 call, and the assignment to
nextCometCreationTime to use the clamped/fallback value (references:
cometFrequencyDelay, SEGMENT.intensity, hw_random16, nextCometCreationTime,
strip.now).

#include "wled.h"
#include "FXparticleSystem.h"

unsigned long nextCometCreationTime = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

nextCometCreationTime is a file-scope global — breaks multi-segment usage

All segments running this effect share the same spawn timer. When one segment creates a comet it sets the timer far into the future, silencing comet creation in every other segment for that duration.

Store the next-creation timestamp per segment, e.g. in SEGENV.aux0/aux1 (packed as a uint32_t) or inside a small context struct held in SEGENV.data alongside the ParticleSystem2D pointer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/PS_Comet/PS_Comet.cpp` at line 4, The global variable
nextCometCreationTime causes all segments to share one spawn timer; replace this
file-scope unsigned long with per-segment storage by packing a uint32_t
timestamp into SEGENV.aux0/aux1 or by creating a small context struct
(containing the ParticleSystem2D* and a uint32_t nextCometCreationTime) and
storing a pointer in SEGENV.data; update the comet spawn logic (references to
nextCometCreationTime) to read/write the per-segment field from SEGENV.aux0/aux1
or the context in SEGENV.data and initialize that per-segment storage where the
ParticleSystem2D is created/attached.

Comment on lines +54 to +55
uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3);
nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

hw_random16(0) UB at maximum intensity — root cause of reported breakage

When SEGMENT.intensity == 255, SEGMENT.intensity << 3 == 2040, making cometFrequencyDelay == 0. hw_random16(0) typically reduces to random16() % 0, which is undefined behaviour (integer division by zero). Even if the implementation short-circuits to 0, nextCometCreationTime stays at strip.now, so a new comet is attempted every frame — this matches the reviewer's observation that maxing out the sliders breaks the effect.

🛠️ Proposed fix
-    uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3);
-    nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay);
+    uint16_t cometFrequencyDelay = max(1U, (unsigned)(2040U - (SEGMENT.intensity << 3)));
+    nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/PS_Comet/PS_Comet.cpp` around lines 54 - 55, When SEGMENT.intensity
can make cometFrequencyDelay zero, avoid calling hw_random16(0) and prevent
scheduling comets every frame: clamp cometFrequencyDelay to a minimum (e.g., 1)
before calling hw_random16 or skip the random offset when cometFrequencyDelay ==
0 and instead add a safe non-zero fallback to nextCometCreationTime; update the
code that computes cometFrequencyDelay, the hw_random16 call, and the assignment
to nextCometCreationTime to use the clamped/fallback value (references:
cometFrequencyDelay, SEGMENT.intensity, hw_random16, nextCometCreationTime,
strip.now).

Comment on lines +57 to +61
uint8_t canLargeCometSpawn =
// Slider 3 determines % of large comets with extra particle sources on their sides
SEGMENT.custom1 > hw_random8(254)
&& chosenIndex != 0
&& chosenIndex != numComets - 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

canLargeCometSpawn can be true when chosenIndex == NULL_INDEX

When no comet will spawn (chosenIndex == NULL_INDEX), the edge-guard checks on lines 60–61 still pass (since NULL_INDEX is far outside the valid column range), so hw_random8(254) is consumed and canLargeCometSpawn can be set to true. This is functionally safe — no comet is actually spawned — but it wastes a random draw on every suppressed frame.

🛠️ Proposed fix
   uint8_t canLargeCometSpawn =
     SEGMENT.custom1 > hw_random8(254)
+    && chosenIndex != NULL_INDEX
     && chosenIndex != 0
     && chosenIndex != numComets - 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/PS_Comet/PS_Comet.cpp` around lines 57 - 61, The current expression
for canLargeCometSpawn calls hw_random8(254) even when no comet will spawn
(chosenIndex == NULL_INDEX), wasting RNG; change the condition order or split it
so chosenIndex is validated first: check chosenIndex != NULL_INDEX (and the edge
guards chosenIndex != 0 && chosenIndex != numComets - 1) before calling
SEGMENT.custom1 > hw_random8(254). Update the canLargeCometSpawn computation (or
add a short if) so hw_random8(254) is only invoked when chosenIndex is a real
column index.

@gustebeast
Copy link
Contributor Author

I gave it a quick spin. Parameters may need some tuning: set all sliders except speed to max and it breaks the effect.

This was due to the code using 3 sources for every column and not cleaning them up properly after falling out of view. With the new implementation it looks normal maxed out. The comet length max makes the large comets appear a bit faint but that's a limitation with the PS system requiring all particle sources to use the same intensity. Ideally the large comets could have their own intensity value since they have their own maxlife/minlife/ttl

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effect usermod usermod related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments