-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Perlinscape FX (2D) in the user_fx usermod #5587
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
73ca612
427e61a
c58ccb8
5c65919
e76d4af
1af6f89
8d8e107
0dcdbaa
4d1d818
b41d994
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1258,6 +1258,101 @@ static void mode_morsecode(void) { | |||||||||
| static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color mode,Color by Word,Punctuation,EndOfMessage;;!;1;sx=192,c3=8,o1=1,o2=1"; | ||||||||||
|
|
||||||||||
|
|
||||||||||
| /* | ||||||||||
| / Perlinscape effect - a Perlin noise Landscape | ||||||||||
| * Created by stepko as part of Stepko Land on soulmatelights.com | ||||||||||
| * Adapted to WLED by Bob Loeffler with additional features (and help from Claude) | ||||||||||
| * First slider is for speed | ||||||||||
| * Second slider is for zooming in/out (Perlin scaling) | ||||||||||
| * Third slider is the X multiplier | ||||||||||
| * Fourth slider is the Y multiplier | ||||||||||
| * First checkbox will rotate the animation | ||||||||||
| * Second checkbox will randomize the horizonal and vertical directions | ||||||||||
| */ | ||||||||||
| static void mode_2D_perlinscape(void) { | ||||||||||
| if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up | ||||||||||
| const uint16_t width = SEG_W; | ||||||||||
| const uint16_t height = SEG_H; | ||||||||||
| if (!SEGENV.allocateData(5 * sizeof(float))) FX_FALLBACK_STATIC; | ||||||||||
| uint32_t speedDiv = map(SEGMENT.speed, 0, 255, 30, 1); | ||||||||||
| uint32_t t = strip.now / speedDiv; | ||||||||||
| uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); | ||||||||||
| uint8_t Ymult = map(SEGMENT.custom2, 0, 255, 0, 64); | ||||||||||
|
|
||||||||||
| float *offX = reinterpret_cast<float*>(SEGENV.data) + 0; | ||||||||||
| float *offY = reinterpret_cast<float*>(SEGENV.data) + 1; | ||||||||||
| float *stepX = reinterpret_cast<float*>(SEGENV.data) + 2; | ||||||||||
| float *stepY = reinterpret_cast<float*>(SEGENV.data) + 3; | ||||||||||
| float *prevT = reinterpret_cast<float*>(SEGENV.data) + 4; | ||||||||||
|
|
||||||||||
| if (SEGENV.call == 0) { | ||||||||||
| SEGENV.aux0 = hw_random16(5000, 10000); | ||||||||||
| SEGENV.aux1 = 0b00; | ||||||||||
| *offX = 0.0f; | ||||||||||
| *offY = 0.0f; | ||||||||||
| *stepX = 1.0f; | ||||||||||
| *stepY = 1.0f; | ||||||||||
| *prevT = (float)t; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (SEGMENT.check3 && (strip.now - SEGENV.step > SEGENV.aux0)) { | ||||||||||
| SEGENV.aux0 = hw_random16(5000, 10000); | ||||||||||
| SEGENV.aux1 = hw_random8(4); | ||||||||||
| SEGENV.step = strip.now; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| bool flipX = SEGMENT.check3 ? (SEGENV.aux1 & 0x01) : false; | ||||||||||
| bool flipY = SEGMENT.check3 ? (SEGENV.aux1 & 0x02) : false; | ||||||||||
|
|
||||||||||
| float targetX = flipX ? -1.0f : 1.0f; | ||||||||||
| float targetY = flipY ? -1.0f : 1.0f; | ||||||||||
| *stepX += (targetX - *stepX) * 0.05f; | ||||||||||
| *stepY += (targetY - *stepY) * 0.05f; | ||||||||||
|
|
||||||||||
| float dt = (float)t - *prevT; | ||||||||||
| *offX += *stepX * dt; | ||||||||||
| *offY += *stepY * dt; | ||||||||||
| *prevT = (float)t; | ||||||||||
|
|
||||||||||
| int32_t tX = (int32_t)*offX; | ||||||||||
| int32_t tY = (int32_t)*offY; | ||||||||||
|
|
||||||||||
| // Rotation | ||||||||||
| float cosA = 1.0f, sinA = 0.0f; | ||||||||||
| float cx = width * 0.5f; | ||||||||||
| float cy = height * 0.5f; | ||||||||||
|
|
||||||||||
| if (SEGMENT.check2) { | ||||||||||
| float angle = strip.now / 5000.0f; | ||||||||||
| cosA = cos_approx(angle); | ||||||||||
| sinA = sin_approx(angle); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| float scale = map(SEGMENT.intensity, 0, 255, 10, 200) / 100.0f; // range 0.1 to 2.0 | ||||||||||
|
|
||||||||||
| for (byte x = 0; x < width; x++) { | ||||||||||
| for (byte y = 0; y < height; y++) { | ||||||||||
|
Comment on lines
+1333
to
+1334
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent loop-counter overflow on wider matrices. Line 1333 and Line 1334 use Suggested fix- for (byte x = 0; x < width; x++) {
- for (byte y = 0; y < height; y++) {
+ for (uint16_t x = 0; x < width; x++) {
+ for (uint16_t y = 0; y < height; y++) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @BobLoeffler68 in the main FX.cpp we usually don't use 8bit types at all. They are not faster, and just add subtle bugs like the "fails when width > 255" highlighted by the rabbit. It's true that wled currently does not work with a panel width > 255, but there are plans to change this in future. Simply changing "byte" to "unsigned" in both loops should do the trick.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
| float rx = cosA * (x - cx) - sinA * (y - cy) + cx; | ||||||||||
| float ry = sinA * (x - cx) + cosA * (y - cy) + cy; | ||||||||||
|
|
||||||||||
| float scaled_x = rx * Xmult * scale; | ||||||||||
| float scaled_y = ry * Ymult * scale; | ||||||||||
|
|
||||||||||
| if (SEGMENT.palette == 0) { | ||||||||||
| // Raw RGB mode (original perlin noise colors) | ||||||||||
| SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x, scaled_y, t), perlin8(scaled_x, scaled_y + tY), perlin8(scaled_x + tX, scaled_y)); | ||||||||||
| } else { | ||||||||||
| // Use the selected palette's colors | ||||||||||
| uint8_t paletteIndex = perlin8(scaled_x, scaled_y, t); | ||||||||||
| uint8_t brightness = perlin8(scaled_x + tX, scaled_y + tY); | ||||||||||
| SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, brightness)); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (+/-),X multiplier,Y multiplier,,,Rotation,Random direction;;!;2;"; | ||||||||||
|
|
||||||||||
|
|
||||||||||
| ///////////////////// | ||||||||||
| // UserMod Class // | ||||||||||
| ///////////////////// | ||||||||||
|
|
@@ -1272,6 +1367,7 @@ class UserFxUsermod : public Usermod { | |||||||||
| strip.addEffect(255, &mode_2D_magma, _data_FX_MODE_2D_MAGMA); | ||||||||||
| strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS); | ||||||||||
| strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE); | ||||||||||
| strip.addEffect(255, &mode_2D_perlinscape, _data_FX_MODE_2D_PERLINSCAPE); | ||||||||||
|
|
||||||||||
| //////////////////////////////////////// | ||||||||||
| // add your effect function(s) here // | ||||||||||
|
|
||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: wled/WLED
Length of output: 3778
🏁 Script executed:
Repository: wled/WLED
Length of output: 1837
Change
prevTstorage fromfloattouint32_tand compute timestamp delta as signed integer before float conversion to prevent precision loss and wraparound artifacts.The current code (line 1312) subtracts two separately-cast floats:
(float)t - *prevT. When theuint32_t tvalue wraps (after ~49 days), casting each to float independently loses precision. The fix: storeprevTasuint32_t, then compute the delta as a signed integer first before converting to float—matching the correct pattern already used elsewhere in this file (e.g., lines 934, 956, 980).Suggested changes
🤖 Prompt for AI Agents
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this finding is right.
floatis stored in IEEE754 format (22 bit mantissa, 10bit exponent). It means that in worst case (near the uint32 roll-over) , float is only accurate for 22bits and you lose 10bits = 1-2 seconds of accuracy.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@softhack007, exactly right — thank you for spelling out the precision math. To summarize the complete impact:t = strip.now / speedDivis a deriveduint32_tthat can reach values in the hundreds of millions.float(23-bit mantissa, ~7 decimal significant digits) means oncet > 2²³ ≈ 8 388 608, the least-significant bit of the float representation covers more than one millisecond — and near the 32-bit rollover (~4 billion) that grows to the 1–2 second error you describe.dt = (float)t - *prevTcan therefore be significantly wrong, causing jitter or jumps in the animation offset.The correct fix (as noted in the suggestion) is to keep
prevTas auint32_tand compute the signed integer delta before converting:This matches the pattern already used in the Ants effect for
lastBumpUpdate(e.g.,float(int(strip.now - ants[i].lastBumpUpdate))).@BobLoeffler68— this is a clear correctness bug worth fixing before merging. Would you like to apply the change?🐇 📐
✏️ Learnings added
🧠 Learnings used