The pursuit of enhancing LED animations


Well-known member
I'm currently working on a versatile realtime rendering thing which is meant to generate procedural animations based on very few parameters. Stuff like spiral, caleidoscope, tunnel, lens, twist, rotation and so on, all based on multi layer procedural Perlin noise. Basically polar math + noise. It renders stuff like this:

If you have a >2 kLED setup around I'd appreciate if you could quickly run this code and report back how many kPixel/s and how many fps you get (just check serial monitor).

I get 80.000 pixel/s throughput on my Teensy 3.6, ESP32 is reported at 53 kpx/s, 110kpx/s at dual core operation. I'm really curious how much a Teensy 4 is capable of pushing out...and how that looks on a large panel / installation / setup.

Just send me an email & I'll send you the link. Thanks for your support!
Last edited:
Hello Stefan,
Love seeing your new work. Long time fan.
I am just not sure what you are after with >2Kled?
I am running Teensy 4.0 with 64x64 panels in many configurations
including 3x3 panels.
My circuitry is based on the SmartMatrix desig.
Let me know if I can be of help.
Hello Richard,

Teensy 4 + 64x64 sounds great! If you have some free minutes it would be a great support if you could run this code on your SmartMatrix.

I assume line 14, 15 and 71 need to be changed, maybe 44 & 291 as well.

If you get it up & running I'd like to know the the pixel/s and fps count you get (check serial monitor). Also if you could share the changes needed for SmartMatrix it would be awesone.

Thank you very much!
Hello Richard,

Teensy 4 + 64x64 sounds great! If you have some free minutes it would be a great support if you could run this code on your SmartMatrix.

I assume line 14, 15 and 71 need to be changed, maybe 44 & 291 as well.

If you get it up & running I'd like to know the the pixel/s and fps count you get (check serial monitor). Also if you could share the changes needed for SmartMatrix it would be awesone.

Thank you very much!

I don't use FastLED but does it support the HUB75 displays that are often used for the larger RGB displays? A quick glance at the FastLED source seemed to indicate it mostly supported serial protocols like WS2812B (neopixel) and APA102 (dotstar or Lumenati).

FWIW, the shields used to be called SmartMatrix, but they got renamed to SmartLED (and the company building them is called

While it should be fairly easy to change the code to use the HUB75 matrixes used by the SmartLED shield, it won't be just be changing 3 lines. I.e. write_pixel_to_framebuffer will need to be rewritten, along with the setup and show calls. I suspect you might also have to calculate the frame rate by hand.

Perhaps somebody should add HUB75/SmartLED support to FastLED, or perhaps they did, and I missed it.

FWIW, I have a few 32x64 and 64x64 HUB75 panels from Adafruit along with the SmartMatrix Teensy 4.x shield. I also bought the Teensy 3.5/3.6 shield, but I haven't used it. I haven't done any programming of the displays, other than just tweaking a few programs. My favorite display is the 2.0mm 64x64 pitch display:

My code is entirely independent from FastLED, it could easiely run on any other LED protocol or library, also on a LCD- or OLED interface...

All I need is a place to write the next frame into. Even the colordepth could be adjusted easiely, I convert 32 bit float values down to 0-255 brightness values in the very last step - it would be trivial to output 10 or 16 or any desired bits per color channel as well.

There is only one place where I write data (line 291), it should be really simple to have this "render engine" (is this an appropriate term?) running on any visual 2d setup.

edit: Even on only 8 bit color resolution the results are pretty satisfying.

The minimal flicker is due to camera interference, IRL it looks uncompromised smooth and soft.
Last edited:
This is the same algorithm outputing RGBs to Processing which draws colored rectangles based on the data the renderer provides.

...and here on low res 16x16 again...

Last edited:
I had an hour or so available tonight, so took a quick look at the code, and in some places, I was able to replace the FastLED screen stuff with SmartLED.

But unfortunately, you use a lot of FastLED support that may not map into SmartLED. I think you need somebody with a more detailed knowledge of both libraries to proceed further.

In particular:

  • You include something called FLOAT.h that isn't in my library set, but it seems ok if I comment it out.
  • You use something from FastLED called inoise16. I have no idea what this is. But I suspect I can't just comment it out.
  • The adjust_gamma function will likely need to be completely rethought. I don't know if you can get the current values of pixel at the X/Y position.. For the moment, I had just commented out the code.
  • The report_performance function uses a function getFPS that would have to be recoded.

FWIW, here is my initial stab at this. Note, I don't plan on spending any more time on this:

// Polar basics demo for the 
// FastLED Podcast #2
// VO.1 preview version
// by Stefan Petrick 2023
// This code is licenced under a 
// Creative Commons Attribution 
// License CC BY-NC 3.0

#if defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41)
#include <MatrixHardware_Teensy4_ShieldV5.h>		// SmartLED Shield for Teensy 4 (V5)

#elif defined(ARDUINO_TEENSY35) || defined(ARDUINO_TEENSY36)
#include <MatrixHardware_Teensy4_ShieldV4.h>		// SmartLED Shield for Teensy 4 (V5)

#error "Include the correct header for the microprocessor"

#include <SmartMatrix.h>

/* SmartMatrix configuration and memory allocation */
#define COLOR_DEPTH 24			// Choose the color depth used for storing pixels in the layers: 24 or 48
					// (24 is good for most sketches - If the sketch uses type `rgb24` directly,
					// COLOR_DEPTH must be 24)
#define kMatrixWidth	64U		// Set to the width of your display, must be a multiple of 8
#define kMatrixHeight	64U		// Set to the height of your display

const uint8_t kRefreshDepth = 36;	// Tradeoff of color quality vs refresh rate, max brightness, and RAM usage.
					// 36 is typically good, drop down to 24 if you need to.
					// On Teensy, multiples of 3, up to 48: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48.
					// On ESP32: 24, 36, 48

const uint8_t kDmaBufferRows = 4;       // known working: 2-4, use 2 to save RAM, more to keep from dropping frames
					// and automatically lowering refresh rate.  (This isn't used on ESP32, leave as default)

// Choose the configuration that matches your panels.  See more details in
// MatrixCommonHub75.h and the docs:
#if kMatrixHeight == 64
const uint8_t kPanelType = SM_PANELTYPE_HUB75_64ROW_MOD32SCAN;

#elif kMatrixHeight == 32
const uint8_t kPanelType = SM_PANELTYPE_HUB75_32ROW_MOD16SCAN;

#elif kMatrixHeight == 16
const uint8_t kPanelType = SM_PANELTYPE_HUB75_16ROW_MOD8SCAN;

#error "Update setting kPanelType"

// see docs for options:
const uint32_t kMatrixOptions		= (SM_HUB75_OPTIONS_NONE);
const uint8_t  kBackgroundLayerOptions	= (SM_BACKGROUND_OPTIONS_NONE);
const uint8_t  kScrollingLayerOptions	= (SM_SCROLLING_OPTIONS_NONE);



// range 0-255
const int defaultBrightness = 255;

const rgb24 COLOR_BLACK = { 0, 0, 0 };

//#include <FastLED.h>
//#include <FLOAT.h>

#define WIDTH  kMatrixWidth             // how many LEDs are in one row?
#define HEIGHT kMatrixHeight            // how many rows?
#define NUM_LEDS ((WIDTH) * (HEIGHT))

float runtime;                          // elapse ms since startup
float newdist, newangle;                // parameters for image reconstruction
float z;                                // 3rd dimension for the 3d noise function
float offset_x, offset_y;               // wanna shift the cartesians during runtime?
float scale_x, scale_y;                 // cartesian scaling in 2 dimensions
float dist, angle;                      // the actual polar coordinates

int x, y;                               // the cartesian coordiantes
int num_x = WIDTH;                      // horizontal pixel count
int num_y = HEIGHT;                     // vertical pixel count

// Background for setting the following 2 numbers: the FastLED inoise16() function returns
// raw values ranging from 0-65535. In order to improve contrast we filter this output and
// stretch the remains. In histogram (photography) terms this means setting a blackpoint and
// a whitepoint. low_limit MUST be smaller than high_limit.

uint16_t low_limit  = 30000;            // everything lower drawns in black
                                        // higher numer = more black & more contrast present
uint16_t high_limit = 50000;            // everything higher gets maximum brightness & bleeds out
                                        // lower number = the result will be more bright & shiny

float center_x = (num_x / 2) - 0.5;     // the reference point for polar coordinates
float center_y = (num_y / 2) - 0.5;     // (can also be outside of the actual xy matrix)
//float center_x = 20;                  // the reference point for polar coordinates
//float center_y = 20;                

//CRGB leds[WIDTH * HEIGHT];               // framebuffer

float theta   [WIDTH] [HEIGHT];          // look-up table for all angles
float distance[WIDTH] [HEIGHT];          // look-up table for all distances
float vignette[WIDTH] [HEIGHT];
float inverse_vignette[WIDTH] [HEIGHT];

float spd;                            // can be used for animation speed manipulation during runtime

float show1, show2, show3, show4, show5; // to save the rendered values of all animation layers
float red, green, blue;                  // for the final RGB results after the colormapping

float c, d, e, f;                                                   // factors for oscillators
float linear_c, linear_d, linear_e, linear_f;                       // linear offsets
float angle_c, angle_d, angle_e, angle_f;                           // angle offsets
float noise_angle_c, noise_angle_d, noise_angle_e, noise_angle_f;   // angles based on linear noise travel
float dir_c, dir_d, dir_e, dir_f;                                   // direction multiplicators

void setup() {

  Serial.begin(115200);                 // check serial monitor for current fps count
  // Initialize Matrix

  // Teensy users: make sure to use the hardware SPI pins 11 & 13
  // for best performance
  //FastLED.addLeds<APA102, 11, 13, BGR, DATA_RATE_MHZ(12)>(leds, NUM_LEDS); 
  // FastLED.addLeds<NEOPIXEL, 13>(leds, NUM_LEDS);   

  render_polar_lookup_table();          // precalculate all polar coordinates 
                                        // to improve the framerate
  render_vignette_table(9.5);           // the number is the desired radius in pixel
                                        // WIDTH/2 generates a circle

void loop() {

  // set speedratios for the offsets & oscillators
  spd = 0.05  ;
  c   = 0.013  ;
  d   = 0.017   ;
  e   = 0.2  ;
  f   = 0.007  ;

  calculate_oscillators();     // get linear offsets and oscillators going
  // ...and now let's generate a frame 

  for (x = 0; x < num_x; x++) {
    for (y = 0; y < num_y; y++) {

      // pick polar coordinates from look the up table 

      dist  = distance [x] [y];
      angle = theta    [y] [x];

      // Generation of one layer. Explore the parameters and what they do.
      scale_x  = 10000;                       // smaller value = zoom in, bigger structures, less detail
      scale_y  = 10000;                       // higher = zoom out, more pixelated, more detail
      z        = 0;                           // must be >= 0
      newangle = angle + angle_c;
      newdist  = dist;
      offset_x = 0;                        // must be >=0
      offset_y = 0;                        // must be >=0
      show1 = render_pixel();

      // Colormapping - Assign rendered values to colors 
      red   = show1;
      green = 0;
      blue  = 0;

      // Check the final results.
      // Discard faulty RGB values & write the valid results into the framebuffer.


  // Bring background layer to the front
  backgroundLayer.swapBuffers ();


  // check serial monitor for current performance data
  //EVERY_N_MILLIS(500) report_performance();

//-----------------------------------------------------------------------------------end main loop --------------------

void calculate_oscillators() {
  runtime = millis();                          // save elapsed ms since start up

  runtime = runtime * spd;                     // global anaimation speed

  linear_c = runtime * c;                      // some linear rising offsets 0 to max
  linear_d = runtime * d;
  linear_e = runtime * e;
  linear_f = runtime * f;

  angle_c = fmodf(linear_c, 2 * PI);           // some cyclic angle offsets  0 to 2*PI
  angle_d = fmodf(linear_d, 2 * PI);
  angle_e = fmodf(linear_e, 2 * PI);
  angle_f = fmodf(linear_f, 2 * PI);

  dir_c = sinf(angle_c);                       // some direction oscillators -1 to 1
  dir_d = sinf(angle_d);
  dir_e = sinf(angle_e);
  dir_f = sinf(angle_f);

  uint16_t noi;
  noi =  inoise16(10000 + linear_c * 100000);    // some noise controlled angular offsets
  noise_angle_c = map_float(noi, 0, 65535 , 0, 4*PI);
  noi =  inoise16(20000 + linear_d * 100000);
  noise_angle_d = map_float(noi, 0, 65535 , 0, 4*PI);
  noi =  inoise16(30000 + linear_e * 100000);
  noise_angle_e = map_float(noi, 0, 65535 , 0, 4*PI);
  noi =  inoise16(40000 + linear_f * 100000);
  noise_angle_f = map_float(noi, 0, 65535 , 0, 4*PI);

// given a static polar origin we can precalculate 
// all the (expensive) polar coordinates

void render_polar_lookup_table() {

  for (int xx = 0; xx < num_x; xx++) {
    for (int yy = 0; yy < num_y; yy++) {

        float dx = xx - center_x;
        float dy = yy - center_y;

      distance[xx] [yy] = hypotf(dx, dy);
      theta[xx] [yy]    = atan2f(dy, dx);

// calculate distance and angle of the point relative to
// the polar origin defined by center_x & center_y

void get_polar_values() {

  // calculate current cartesian distances (deltas) from polar origin point

  float dx = x - center_x;
  float dy = y - center_y;

  // calculate distance between current point & polar origin
  // (length of the origin vector, pythgorean theroem)
  // dist = sqrt((dx*dx)+(dy*dy));

  dist = hypotf(dx, dy);

  // calculate the angle
  // (where around the polar origin is the current point?)

  angle = atan2f(dy, dx);

  // done, that's all we need

// convert polar coordinates back to cartesian
// & render noise value there

float render_pixel() {

  // convert polar coordinates back to cartesian ones

  float newx = (offset_x + center_x - (cosf(newangle) * newdist)) * scale_x;
  float newy = (offset_y + center_y - (sinf(newangle) * newdist)) * scale_y;

  // render noisevalue at this new cartesian point

  uint16_t raw_noise_field_value = inoise16(newx, newy, z);

  // a lot is happening here, namely
  // A) enhance histogram (improve contrast) by setting the black and white point
  // B) scale the result to a 0-255 range
  // it's the contrast boosting & the "colormapping" (technically brightness mapping)

  if (raw_noise_field_value < low_limit)  raw_noise_field_value =  low_limit;
  if (raw_noise_field_value > high_limit) raw_noise_field_value = high_limit;

  float scaled_noise_value = map_float(raw_noise_field_value, low_limit, high_limit, 0, 255);

  return scaled_noise_value;

  // done, we've just rendered one color value for one single pixel

// float mapping maintaining 32 bit precision
// we keep values with high resolution for potential later usage

float map_float(float x, float in_min, float in_max, float out_min, float out_max) { 
  float result = (x-in_min) * (out_max-out_min) / (in_max-in_min) + out_min;
  if (result < out_min) result = out_min;
  if( result > out_max) result = out_max;

  return result; 

// Avoid any possible color flicker by forcing the raw RGB values to be 0-255.
// This enables to play freely with random equations for the colormapping
// without causing flicker by accidentally missing the valid target range.

void rgb_sanity_check() {

      // rescue data if possible: when negative return absolute value
      if (red < 0)     red = abs(red);
      if (green < 0) green = abs(green);
      if (blue < 0)   blue = abs(blue);
      // discard everything above the valid 0-255 range
      if (red   > 255)   red = 255;
      if (green > 255) green = 255;
      if (blue  > 255)  blue = 255;

// check result after colormapping and store the newly rendered rgb data

void write_pixel_to_framebuffer() {
      // the final color values shall not exceed 255 (to avoid flickering pixels caused by >255 = black...)
      // negative values * -1 


      // write the rendered pixel into the framebutter
      //leds[XY(x, y)] = finalcolor;

      backgroundLayer.drawPixel(x, y, {(uint8_t)red, (uint8_t)green, (uint8_t)blue});

// find the right led index

//uint16_t XY(uint8_t x, uint8_t y) {
//  if (y & 1)                             // check last bit
//    return (y + 1) * WIDTH - 1 - x;      // reverse every second line for a serpentine lled layout
//  else
//    return y * WIDTH + x;                // use this equation only for a line by line led layout
//}                                        // remove the previous 3 lines of code in this case

// make it look nicer - expand low brightness values and compress high brightness values,
// basically we perform gamma curve bending for all 3 color chanels,
// making more detail visible which otherwise tends to get lost in brightness

void adjust_gamma() {
  //for (uint16_t i = 0; i < NUM_LEDS; i++)
  //  leds[i].r = dim8_video(leds[i].r);
  //  leds[i].g = dim8_video(leds[i].g);
  //  leds[i].b = dim8_video(leds[i].b);

// precalculate a radial brightness mask

void render_vignette_table(float filter_radius) {

  for (int xx = 0; xx < num_x; xx++) {
    for (int yy = 0; yy < num_y; yy++) {

      vignette[xx] [yy] = (filter_radius - distance[xx] [yy]) / filter_radius; 
      if (vignette[xx] [yy] < 0) vignette[xx] [yy] = 0;  

// show current framerate and rendered pixels per second

void report_performance() {
  //int fps = FastLED.getFPS();                 // frames per second
  //int kpps = (fps * HEIGHT * WIDTH) / 1000;   // kilopixel per second

  //Serial.print(kpps); Serial.print(" kpps ... ");
  //Serial.print(fps); Serial.print(" fps @ ");
  //Serial.print(WIDTH*HEIGHT); Serial.println(" LEDs ... ");
Thanks for your time, Michael.

Float.h is not essenential, I temporaty used it do use predifened numers, like FLT_MAX. I assumed that it comes with Arduino / Teensyduino, at least I never installed it but have it anyway.

inoise16 is a 3d Simplex noise implementation that comes with FastLED. I am currently looking for something more performant to replace it with. Meanwhile this FastLED function can be used without using FastLED for anything else, it works not on the framebuffer, it just returns 16bit noisevalues.

adjust_gamma is not essential, can be commented out for now, but I will rework it.

The fps counting indeed uses a FastLED function - I'll replace it by a rewrite, too.

I'm in contact with Louis (SM autor) and the SM community. I will come back here when I can provide a SM-version of this code, should be plug & play by then.

Again, thanks for taking the time and pointing out the issues.
Try changing #include <FLOAT.h> to #include <float.h>.

The filename is all lowercase. Using uppercase probably works on Windows and most MacOS, but will fail with a case sensitive filesystem used most Linux systems or some MacOS.
FWIW, there appears to be two examples in the SmartMatrix library that use FastLed with Smartmatrix (FastLed_Functions and FastLed_Panel_Plus_Apa102). They run on my display once I select the proper shield and display type. I haven't looked into what they are doing.
@Richard: What kind of LEDs or interface do you use? Would be cool so see this running in large.

Have you seen this one?

Code here if you'd like to hypnotize yourself. :)
Last edited:
Just to report some progress, I improved the code (using structs and functions now), found some % performance improvenment and now I'm exploring what this Renderer (Shader? Technically it's a 5d coordinate mapper) is capable of.

Here some short impressions of the organic movements I get out of this thing.

I achieve this by using "Domain Warping" - basically shifting coordinates (3d cartesian and 2d polar) based on Perlin Noise. Here the concept is shown in a nice & interactive way.

Everything I show runs on 16x16 APA102 LEDs with only 8 bit color depth per color. I never got such smooth (dithered) results ever before. Precise calculations translate directly to image quality and temporal coherence of an animation.

I'm happy and grateful that a Teensy allows me to do such compute-intense animations on a microcontroller now.

Everything I showed so far are maximal 3 animation layers and run at +400 fps on a Teensy 3.6. Up to 10 layers still run at around 50 fps. I'll share more complex generative animations including code later when I polished them a bit.
Last edited:
A fellow LED enthusiast just finished his 48x 16x16 LED panel. I try to talk him into using a Teensy 4 but so far he sticks to an ESP32.

I'm confident with a T4 my code would run fluently.

Here is his short build video:

Just a short uptdate: I currently work on the SmartMatrix version of AnimARTrix.

Here you see a 12 layer animation, blended together using temporal dithering, running at 240 fps on 1024 LEDs. Please ignore the interference stripes, only the camera sees it.

Even at 600 MHz the Teensy 4.0 gets remarkably hot - I guess I should introduce some active cooling to not shorten it's lifespan too much.

I keep being impressed what this little processor is capable of. :eek:

edit: Oh, no embedded video preview for Youtube shorts?
The results of cartesian domain warping never fail to amaze me. In a polar coordinate system it produces even more interesting shapes and movements.

I shift the polar angle of every pixel based on dynamic Perlin noise data which causes this lovely unique look. The polar distance is untouched in this example.

This demo-animation is part of my current project which I called "AnimARTrix". The code for this animation will be part of the examples when I release it.

Right now I'm working on the SmartMatrix version, the FastLED version is pretty much done. I plan to release the whole thing within the next month, currently we're in the middle of beta-testing and I keep adding new features here and there.

The last add-on was color-selective dynamic brightening and darkening masks for layer merging inspired by and very similar to the Photoshop layer effects. So yeah, I'm making progress. :eek:


@RichardFerraro: "I would be happy to test on larger matrices (192x192) should you desire."
This would be very nice! Are you on GitHub? If so, please let me know your name and I'll give you access to the currently private repo.
On a Teensy 4.0 @600Mhz I currently get around 730.000 RGB pixels calculated per second (for a single) animation layer which would translate to 20 fps on 36k LEDs. It might require overclocking + cooling to get better results on your large setup.

@MichaelMeissner: "there appears to be two examples in the SmartMatrix library that use FastLed with Smartmatrix"
Yes, I followed this example and I haven't finally decided yet if I keep it that way or if I'm going to use the SM buffers directely which might be slighly more performant.
Also, if you'd like to test and play with the current SM version (so far mainly tested on 32x32) please drop me a note.
Last edited:
Another short update. Now I'm starting to build more complex animations.

Here my approach of a water "simulation".

And a hypnotic mandala animation - give it a minute or two to catch you. :cool:

Hi everyone! I made the repository public today. Consider it an early beta version. Everything I showed here you can find somewhere in the animation collection.

This is the SmartMatrix version. Tested on 32x32.

This is the FastLED version. Tested on 16x16.

I do not recommend to run the demo animations on a smaller resolution than they are tested on. There fits only a finite amount of detail on a given resolution, scaling down might cause "jumping pixels". If you want to try anyway, adjust the scale factors in the animation code. Scaling up to higher resolutions should work fine. You might want to adjust the size of radial_filter_radius when the animation uses it.
Last edited:
I intended to develop a user interface for controlling AnimARTix parameters. However, I got a bit carried away and ended up creating a simulator instead.

This simulator allowed me to delve into the concept of transitioning between different parameter sets. Initially, I constrained the parameter ranges to prevent visual blackout or flickering in edge cases. Subsequently, I randomly selected parameter sets and interpolated between them. Additionally, I incorporated some easeInOut functions for more interesting transitions. Finally, I introduced a second layer.

This appears to be a promising approach towards creating a generative animation synthesizer. Ultimately, the goal is to run this standalone on a Teensy + LED matrix, generating continually new patterns and animations.

This is work in progress, more parameters and more layers will be added.
Short update: Improved visual quality and overall look and feel. The little bar graph under the Shuffle button indicates the progress of the transition between parameter sets.

Now, a Full Auto mode is present, too, meaning to periodically trigger a reshuffling of all parameters.

I’m coming closer to the desired effect of the transitions looking better than the individual animations.