usermods/user_fx/README.md
This usermod is a common place to put various users’ WLED effects. It lets you load your own custom effects or bring back deprecated ones—without touching core WLED source code.
Multiple Effects can be specified inside this single usermod, as we will illustrate below. You will be able to define them with custom names, sliders, etc. as with any other Effect.
To activate the usermod, add the following line to your platformio_override.ini
custom_usermods = user_fx
Or if you are already using a usermod, append user_fx to the list
custom_usermods = audioreactive user_fx
The user_fx.cpp file can be broken down into four main parts:
We will go into greater detail on how custom effects work in the usermod and how to go about creating your own in the section below.
WLED effects generally follow a certain procedure for their operation:
SEGMENT.setPixelColor()Below are some helpful variables and functions to know as you start your journey towards WLED effect creation:
| Syntax Element | Size | Description |
|---|---|---|
SEGMENT.speed / intensity / custom1 / custom2 | 8-bit | These read-only variables help you control aspects of your custom effect using the UI sliders. You can edit these variables through the UI sliders when WLED is running your effect. (These variables can be controlled by the API as well.) Note that while SEGMENT.intensity through SEGMENT.custom2 are 8-bit variables, SEGMENT.custom3 is actually 5-bit. The other three bits are used by the boolean parameters SEGMENT.check1 through SEGMENT.check3 and are bit-packed to conserve data size and memory. |
SEGMENT.custom3 | 5-bit | Another optional UI slider for custom effect control. While SEGMENT.speed through SEGMENT.custom2 are 8-bit variables, SEGMENT.custom3 is actually 5-bit. |
SEGMENT.check1 / check2 / check3 | 1-bit | These variables are boolean parameters which show up as checkbox options in the User Interface. They are bit-packed along with SEGMENT.custom3 to conserve data size and memory. |
SEGENV.aux0 / aux1 | 16-bit | These are state variables that persists between function calls, and they are free to be overwritten by the user for any use case. |
SEGENV.step | 32-bit | This is a timestamp variable that contains the last update time. It is initially set during effect initialization to 0, and then it updates with the elapsed time after each frame runs. |
SEGENV.call | 32-bit | A counter for how many times this effect function has been invoked since it started. |
strip.now | 32-bit | Current timestamp in milliseconds. (Equivalent to millis(), but use strip.now() instead.) strip.now respects the timebase, which can be used to advance or reset effects in a preset. This can be useful to sync multiple segments. |
SEGLEN / SEG_W / SEG_H | 16-bit | These variables are macros that help define the length and width of your LED strip/matrix segment. |
SEGPALETTE | --- | Macro that gets the currently selected palette for the currently processing segment. |
hw_random8() | 8-bit | One of several functions that generates a random integer. (All of the "hw_" functions are similar to the FastLED library's random functions, but in WLED they use true hardware-based randomness instead of a pseudo random number. In short, they are better and faster.) |
SEGCOLOR(x) | 32-bit | Macro that gets user-selected colors from UI, where x is an integer 1, 2, or 3 for primary, secondary, and tertiary colors, respectively. |
SEGMENT.setPixelColor / setPixelColorXY | 32-bit | Function that paints one pixel. setPixelColor is 1‑D; setPixelColorXY expects (x, y) and an RGBW color value. |
SEGMENT.color_wheel() | 32-bit | Input 0–255 to get a color. Transitions r→g→b→r. In HSV terms, pos is H. Note: only returns palette color unless the Default palette is selected. |
SEGMENT.color_from_palette() | 32-bit | Gets a single color from the currently selected palette for a segment. (This function which should be favoured over ColorFromPalette() because this function returns an RGBW color with white from the SEGCOLOR passed, while also respecting the setting for palette wrapping. On the other hand, ColorFromPalette() simply gets the RGB palette color.) |
fade_out() | --- | fade out function, higher rate = quicker fade. fading is highly dependent on frame rate (higher frame rates, faster fading). each frame will fade at max 9% or as little as 0.8%. |
fadeToBlackBy() | --- | can be used to fade all pixels to black. |
fadeToSecondaryBy() | --- | fades all pixels to secondary color. |
move() | --- | Moves/shifts pixels in the desired direction. |
blur / blur2d | --- | Blurs all pixels for the desired segment. Blur also has the boolean option smear, which, when activated, does not fade the blurred pixel(s). |
You will see how these syntax elements work in the examples below.
In this section we give some advice to those who are new to WLED Effect creation. We will illustrate how to load in multiple Effects using this single usermod, and we will do a deep dive into the anatomy of a 1D Effect as well as a 2D Effect. (Special thanks to @mryndzionek for offering this "Diffusion Fire" 2D Effect for this tutorial.)
The first line of the code imports the wled.h file into this module. Importing wled.h brings all of the variables, files, and functions listed in the table above (and more) into your custom effect for you to use.
#include "wled.h"
The next code block is the mode_static definition. This is usually left as SEGMENT.fill(SEGCOLOR(0)); to leave all pixels off if the effect fails to load, but in theory one could use this as a 'fallback effect' to take on a different behavior, such as displaying some other color instead of leaving the pixels off.
FX_FALLBACK_STATIC is a macro that calls mode_static() and then returns.
Pre-loaded in this template is an example 2D Effect called "Diffusion Fire". (This is the name that would be shown in the UI once the binary is compiled and run on your device, as defined in the metadata string.) The effect starts off by checking to see if the segment that the effect is being applied to is a 2D Matrix, and if it is not, then it runs the static effect which displays no pattern:
if (!strip.isMatrix || !SEGMENT.is2D())
FX_FALLBACK_STATIC; // not a 2D set-up
The next code block contains several constant variable definitions which essentially serve to extract the dimensions of the user's 2D matrix and allow WLED to interpret the matrix as a 1D coordinate system (WLED must do this for all 2D animations):
const int cols = SEG_W;
const int rows = SEG_H;
const auto XY = [&](int x, int y) { return x + y * cols; };
XY to map (x, y) matrix coordinates into a 1D index in the LED array. This assumes row-major order (left to right, top to bottom).
The next lines of code further the setup process by defining variables that allow the effect's settings to be configurable using the UI sliders (or alternatively, through API calls):
const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80);
const unsigned refresh_ms = 1000 / refresh_hz;
const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100);
const uint8_t spark_rate = SEGMENT.intensity;
const uint8_t turbulence = SEGMENT.custom2;
custom1 control (0–255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0–100.
SEGMENT.intensity (user input 0–255) to a variable named spark_rate.
custom2 value to a variable called turbulence.
Next we will look at some lines of code that handle memory allocation and effect initialization:
unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D
cols * rows or (or SEGLEN) returns the total number of pixels in the current segment.NOTE: Virtual lengths
vWidth()andvHeight()will be evaluated differently based on your own custom effect, and based on what other settings are active. For example: If you have an LED strip of length = 60 and you enable grouping = 2, then the virtual length will be 30, so the FX will render 30 pixels instead of 60. This is also true for mirroring or adding gaps--it halves the size. For a 1D strip mapped to 2D, the virtual length depends on selected mode. Keep these things in mind during your custom effect's creation.
if (!SEGENV.allocateData(dataSize))
FX_FALLBACK_STATIC; // allocation failed
SEGENV.data). All subsequent calls simply ensure that the data is still valid.SEGENV.allocateData(n) requests a buffer of size n bytes (1 byte per pixel here).mode_static() fallback effect, which just fills the segment with a static color. We need to do this because WLED needs a fail-safe behavior if a custom effect can't run properly due to memory constraints.The next lines of code clear the LEDs and initialize timing:
if (SEGENV.call == 0) {
SEGMENT.fill(BLACK);
SEGENV.step = 0;
}
SEGENV.call is a counter for how many times this effect function has been invoked since it started.SEGENV.call equals 0 (which it does on the very first call, making it useful for initialization), then it clears the LED segment by filling it with black (turns off all LEDs).SEGENV.step, a timing marker, to 0. This value is later used as a timestamp to control when the next animation frame should occur (based on elapsed time).The next block of code is where the animation update logic starts to kick in:
if ((strip.now - SEGENV.step) >= refresh_ms) {
uint8_t tmp_row[cols]; // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.
SEGENV.step = strip.now;
// scroll up
for (unsigned y = 1; y < rows; y++)
for (unsigned x = 0; x < cols; x++) {
unsigned src = XY(x, y);
unsigned dst = XY(x, y - 1);
SEGENV.data[dst] = SEGENV.data[src];
}
strip.now is the current timestamp in milliseconds; SEGENV.step is the last update time (set during initialization or previous frame). refresh_ms is how long to wait between frames, computed earlier based on SEGMENT.speed.SEGENV.data.
SEGENV.data.IMPORTANT NOTE: Creating variable‑length arrays (VLAs) is non‑standard C++, but this practice is used throughout WLED and works in practice. But be aware that VLAs live on the stack, which is limited. If the array scales with segment length (1D), it can overflow the stack and crash. Keep VLAs ≲ ~1 KiB; an array with 4000 LEDs is ~4 KiB and will likely crash. It’s worse with
uint16_t. Anything larger than ~1 KiB should go intoSEGENV.data, which has a higher limit.
Now we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix:
if (hw_random8() > turbulence) {
// create new sparks at bottom row
for (unsigned x = 0; x < cols; x++) {
uint8_t p = hw_random8();
if (p < spark_rate) {
unsigned dst = XY(x, rows - 1);
SEGENV.data[dst] = 255;
}
}
}
hw_random8() gives a random number between 0–255 using a fast hardware RNG.turbulence is a user-controlled parameter (SEGMENT.custom2, set earlier).hw_random8() is less likely to exceed a high threshold).rows - 1).p, is used to probabilistically decide whether a spark appears at this (x, rows-1) position.spark_rate comes from SEGMENT.intensity (0–255).dst calculates the destination index in the bottom row at column x.Next we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels):
// diffuse
for (unsigned y = 0; y < rows; y++) {
for (unsigned x = 0; x < cols; x++) {
unsigned v = SEGENV.data[XY(x, y)];
if (x > 0) {
v += SEGENV.data[XY(x - 1, y)];
}
if (x < (cols - 1)) {
v += SEGENV.data[XY(x + 1, y)];
}
tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));
}
v:
if statements accomplish is: v = center + left + right.300 + diffusion means that with higher diffusion, you get more smoothing (since the sum is divided more).v * 100 scales things before dividing (preserving some dynamic range).min(255, ...) clamps the result to 8-bit range.After calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors:
for (unsigned x = 0; x < cols; x++) {
SEGENV.data[XY(x, y)] = tmp_row[x];
if (SEGMENT.check1) {
uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);
SEGMENT.setPixelColorXY(x, y, color);
} else {
uint32_t base = SEGCOLOR(0);
SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));
}
}
}
This next loop starts iterating over each row from top to bottom. (We're now doing this for color-rendering for each pixel row.)
Next we update the main segment data with the smoothed value for this pixel.
The if statement creates a conditional rendering path — the user can toggle this. If check1 is enabled in the effect metadata, we use a color palette to display the flame.
The next line converts the heat value (tmp_row[x]) into a color from the current palette with 255 brightness, and no wrapping in palette lookup.
Finally we set the rendered color for the pixel (x, y).
If palette use is disabled, we fallback to fading a base color.
SEGCOLOR(0) gets the first user-selected color for the segment.
The final line of code fades that base color according to the heat value (acts as brightness multiplier).
Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals (the FPS limit set in config) to check whether an update is needed. If nothing needs to change, the frame still needs to be re-rendered so color or brightness transitions will be smooth.
If you want to run your effect at a fixed frame rate you can use the following code to not update your effect state, be aware however that transitions for your effect will also run at this frame rate - for example if you limit your effect to say 5 FPS, brightness changes and color changes may not look smooth. Also SEGMENT.call is still incremented on each function call.
//limit update rate
if (strip.now - SEGENV.step < FRAMETIME_FIXED) return;
SEGENV.step = strip.now;
At the end of every effect is an important line of code called the metadata string. It defines how the effect is to be interacted with in the UI:
static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35";
This metadata string is passed into strip.addEffect() and parsed by WLED to determine how your effect appears and behaves in the UI.
The string follows the syntax of <Effect Parameters>;<Colors>;<Palette>;<Flags>;<Defaults>, where Effect Parameters are specified by a comma-separated list.
The values for Effect Parameters will always follow the convention in the table below:
| Parameter | Default tooltip label |
|---|---|
| sx | Effect Speed |
| ix | Effect Intensity |
| c1 | Custom 1 |
| c2 | Custom 2 |
| c3 | Custom 3 |
| o1 | Checkbox 1 |
| o2 | Checkbox 2 |
| o3 | Checkbox 3 |
Using this info, let’s split the Metadata string above into logical sections:
| Syntax Element | Description |
|---|---|
| "Diffusion Fire@! | Name. (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) |
| !, | Use default UI entry; for the first space, this will automatically create a slider for Speed |
| Spark rate, Diffusion Speed, Turbulence, | UI sliders for Spark Rate, Diffusion Speed, and Turbulence. Defining slider 2 as "Spark Rate" overwrites the default value of Intensity. |
| (blank), | unused (empty field with not even a space) |
| Use palette; | This occupies the spot for the 6th effect parameter, which automatically makes this a checkbox argument o1 called Use palette in the UI. When this is enabled, the effect uses SEGMENT.color_from_palette(...) (RGBW-aware, respects wrap), otherwise it fades from SEGCOLOR(0). The first semicolon marks the end of the Effect Parameters and the beginning of the Colors parameter. |
| Color; | Custom color field (SEGCOLOR(0)) |
| (blank); | Empty means the effect does not allow Palettes to be selected by the user. But used in conjunction with the checkbox argument, palette use can be turned on/off by the user. |
| 2; | Flag specifying that the effect requires a 2D matrix setup |
| pal=35" | Default Palette ID. this is the setting that the effect starts up with. |
More information on metadata strings can be found here.
Next, we will look at a 1D WLED effect called Sinelon. This one is an especially interesting example because it shows how a single effect function can be used to create several different selectable effects in the UI.
We will break this effect down step by step.
(This effect was originally one of the FastLED example effects; more information on FastLED can be found here.)
static void sinelon_base(bool dual, bool rainbow=false) {
sinelon base as static helper function. This is how all effects are initially defined. if (SEGLEN <= 1) FX_FALLBACK_STATIC;
The line of code helps create the "Fade Out" Trail:
SEGMENT.fade_out(SEGMENT.intensity);
Next, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well:
unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1);
if (SEGENV.call == 0) SEGENV.aux0 = pos;
beatsin16_t is an improved version of FastLED’s beatsin16 function, generating smooth oscillations(SEGENV.call == 0), stores initial position in SEGENV.aux0. (SEGENV.aux0 is a temporary state variable to keep track of last position.)The next lines of code help determine the colors to be used:
uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0);
uint32_t color2 = SEGCOLOR(2);
color1: main moving dot color, chosen from palette using the current position as index.color2: secondary color from user-configured color slot 2.The next part takes into account the optional argument for if a Rainbow colored palette is in use:
if (rainbow) {
color1 = SEGMENT.color_wheel((pos & 0x07) * 32);
}
rainbow is true, override color1 using a rainbow wheel, producing rainbow cycling colors.(pos & 0x07) * 32 ensures the color changes gradually with position. SEGMENT.setPixelColor(pos, color1);
The next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation:
if (dual) {
if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0);
if (rainbow) color2 = color1; // share rainbow color
SEGMENT.setPixelColor(SEGLEN-1-pos, color2);
}
color2 for mirrored dot on opposite side.color2 is not set (0), fallback to same palette color as color1.rainbow mode, force both dots to share the rainbow color.SEGLEN-1-pos to color2.This final part of the effect function will fill in the 'trailing' pixels to complete the animation:
if (SEGENV.aux0 < pos) {
for (unsigned i = SEGENV.aux0; i < pos ; i++) {
SEGMENT.setPixelColor(i, color1);
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
}
} else {
for (unsigned i = SEGENV.aux0; i > pos ; i--) {
SEGMENT.setPixelColor(i, color1);
if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);
}
}
SEGENV.aux0 = pos;
}
SEGENV.aux0 to current position at the end.The last part of this effect has the Wrapper functions for different Sinelon modes. Notice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function:
void mode_sinelon(void) {
sinelon_base(false);
}
// Calls sinelon_base with dual = false and rainbow = false
void mode_sinelon_dual(void) {
sinelon_base(true);
}
// Calls sinelon_base with dual = true and rainbow = false
void mode_sinelon_rainbow(void) {
sinelon_base(false, true);
}
// Calls sinelon_base with dual = false and rainbow = true
And then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI:
static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!";
static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!";
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
Refer to the section above for guidance on understanding metadata strings.
The UserFxUsermod class registers the mode_diffusionfire effect with WLED. This section starts right after the effect function and metadata string, and is responsible for making the effect usable in the WLED interface:
class UserFxUsermod : public Usermod {
private:
public:
void setup() override {
strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);
////////////////////////////////////////
// add your effect function(s) here //
////////////////////////////////////////
// use id=255 for all custom user FX (the final id is assigned when adding the effect)
// strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);
// strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);
// strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);
}
void loop() override {} // nothing to do in the loop
uint16_t getId() override { return USERMOD_ID_USER_FX; }
};
Usermod, which is the base class WLED uses for any pluggable user-defined modules.
setup(), loop(), and other lifecycle events.void setup() function runs once when WLED initializes the usermod.
override ensures that this matches the Usermod base class definition.strip.addEffect line is an important one that registers the custom effect so WLED knows about it.
&mode_diffusionfire: Pointer to the effect function._data_FX_MODE_DIFFUSIONFIRE: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders).loop() function remains empty because this usermod doesn’t need to do anything continuously. WLED still calls this every main loop, but nothing is done here.
The final part of this file handles instantiation and initialization:
static UserFxUsermod user_fx;
REGISTER_USERMOD(user_fx);
setup() and loop(), and lets it interact with the system.So now let's say that you wanted add the effects "Diffusion Fire" and "Sinelon" through this same Usermod file:
addEffect function in the Usermod class.Compiling WLED yourself is beyond the scope of this tutorial, but the complete guide to compiling WLED can be found here, on the official WLED documentation website.
This custom effect tutorial guide is still in development. If you have suggestions on what should be added, or if you've found any parts of this guide which seem incorrect, feel free to reach out here and help us improve this guide for future creators.