Skip to content

Instantly share code, notes, and snippets.

@StefanPetrick
Last active October 1, 2023 07:44
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save StefanPetrick/35ffd8467df22a77067545cfb889aa4f to your computer and use it in GitHub Desktop.
Save StefanPetrick/35ffd8467df22a77067545cfb889aa4f to your computer and use it in GitHub Desktop.
Code I wrote in the FastLED Podcast #3
// Circular Blobs
//
// VO.2 preview version
// by Stefan Petrick 2023
// This code is licenced under a
// Creative Commons Attribution
// License CC BY-NC 3.0
//
// In order to run this on your own setup you might want to check and change
// line 22 & 23 according to your matrix size and
// line 75 to suit your LED interface type.
//
// In case you want to run this code on a different LED driver library
// (like SmartMatrix, OctoWS2812, ESP32 16x parallel output) you will need to change
// line 52 to your own framebuffer and line 276+279 to your own setcolor function.
// In line 154 the framebuffer gets pushed to the LEDs.
// The whole report_performance function you can just comment out. It gets called
// in line 157.
//
// With this adaptions it should be easy to use this code with
// any given LED driver & interface you might prefer.
#include <FastLED.h>
#define WIDTH 16 // how many LEDs are in one row?
#define HEIGHT 16 // how many rows?
#define NUM_LEDS ((WIDTH) * (HEIGHT))
float runtime; // elapse ms since startup
float newdist, newangle; // parameters for image reconstruction from polar data
float z; // 3rd dimension for the 3d noise
float offset_x, offset_y, offset_z; // wanna shift the cartesians during runtime?
float scale_x, scale_y, scale_z; // cartesian scaling in 3 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 pnoise() function returns
// raw values ranging from -1 to +1. 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.
float low_limit = 0; // everything lower drawns in black
// higher numer = more black & more contrast present
float high_limit = 0.5; // 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)
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 spd; // can be used for global 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
// FastLED.addLeds<NEOPIXEL, 13>(leds, NUM_LEDS);
FastLED.addLeds<APA102, 11, 13, BGR, DATA_RATE_MHZ(12)>(leds, NUM_LEDS);
render_polar_lookup_table(); // precalculate all polar coordinates
// to improve the framerate
}
//*********************************************************************************************
void loop() {
// set speed ratios for the offsets & oscillators
spd = 0.001 ; // higher = faster
c = 0.05 ;
d = 0.07 ;
e = 0.09 ;
f = 0.01 ;
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++) {
dist = distance[x][y]; // pick precalculated polar data
angle = theta[x][y];
// define first animation layer
scale_x = 0.11; // smaller value = zoom in
scale_y = 0.1; // higher = zoom out
scale_z = 0.1;
newangle = angle + noise_angle_c + noise_angle_f;
newdist = dist;
offset_z = linear_c * 100;
z = -5 * sqrtf(dist) ;
show1 = render_pixel_faster();
// repeat for the 2nd layer, every parameter you don't change stays as it was set
// in the previous layer.
offset_z = linear_d * 100;
newangle = angle + noise_angle_d + noise_angle_f;
show2 = render_pixel_faster();
// 3d layer
offset_z = linear_e*100;
newangle = angle + noise_angle_e+ noise_angle_f;
show3 = render_pixel_faster();
// create some interference between the layers
show3 = show3-show2-show1;
if (show3 < 0) show3 = 0;
// Colormapping - Assign rendered values to colors
red = show1-show2/2;
if (red < 0) red=0;
green = (show1-show2)/2;
if (green < 0) green=0;
blue = show3-show1/2;
if (blue < 0) blue=0;
// Check the final results and store them.
// Discard faulty RGB values & write the remaining valid results into the framebuffer.
write_pixel_to_framebuffer();
}
}
// BRING IT ON! SHOW WHAT YOU GOT!
FastLED.show();
// check serial monitor for current performance data
EVERY_N_MILLIS(500) report_performance();
}
//*********************************************************************************************
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);
float n;
n = 1 + pnoise(linear_c , 10, 10); // some noise controlled angular offsets 0 to PI
noise_angle_c = n * PI;
n = 1 + pnoise(linear_d , 20, 20);
noise_angle_d = n * PI;
n = 1 + pnoise(linear_e , 30, 30);
noise_angle_e = n * PI;
n = 1 + pnoise(linear_f , 40, 40);
noise_angle_f = n * 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);
}
}
}
// Convert the polar 2 coordinates back to cartesian ones & also apply all 3d transitions.
// Calculate the noise value at this point after the 5 dimensional manipulation of
// the underlaying coordinates.
//
// Now I use a 32 bit float noise function which is more precise AND also more FPU friendly.
// This results in a better render qualitiy in edgecases AND also in a 15% better performance.
// Hurray!
float render_pixel_faster() {
// 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;
float newz = (offset_z + z) * scale_z;
// render noisevalue at this new cartesian point
float raw_noise_field_value = pnoise(newx, newy, newz);
// 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;
}
// 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 unsteady pixels caused by >255 = black...)
// negative values * -1
rgb_sanity_check();
// assign the final color of this one pixel
CRGB finalcolor = CRGB(red, green, blue);
// write the rendered pixel into the framebutter
leds[xy(x, y)] = finalcolor;
}
// 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 = fabsf(red);
if (green < 0) green = fabsf(green);
if (blue < 0) blue = fabsf(blue);
// discard everything above the valid 0-255 range
if (red > 255) red = 255;
if (green > 255) green = 255;
if (blue > 255) blue = 255;
}
// 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
/*
Ken Perlins improved noise - http://mrl.nyu.edu/~perlin/noise/
C-port: http://www.fundza.com/c4serious/noise/perlin/perlin.html
by Malcolm Kesson; arduino port by Peter Chiochetti, Sep 2007 :
- make permutation constant byte, obsoletes init(), lookup % 256
*/
static const byte p[] = { 151,160,137,91,90, 15,131, 13,201,95,96,
53,194,233, 7,225,140,36,103,30,69,142, 8,99,37,240,21,10,23,190, 6,
148,247,120,234,75, 0,26,197,62,94,252,219,203,117, 35,11,32,57,177,
33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,
48,27,166, 77,146,158,231,83,111,229,122, 60,211,133,230,220,105,92,
41,55,46,245,40,244,102,143,54,65,25,63,161, 1,216,80,73,209,76,132,
187,208, 89, 18,169,200,196,135,130,116,188,159, 86,164,100,109,198,
173,186, 3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,
212,207,206, 59,227, 47,16,58,17,182,189, 28,42,223,183,170,213,119,
248,152,2,44,154,163,70,221,153,101,155,167,43,172, 9,129,22,39,253,
19,98,108,110,79,113,224,232,178,185,112,104,218,246, 97,228,251,34,
242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,
150,254,138,236,205, 93,222,114, 67,29,24, 72,243,141,128,195,78,66,
215,61,156,180
};
float fade(float t){ return t * t * t * (t * (t * 6 - 15) + 10); }
float lerp(float t, float a, float b){ return a + t * (b - a); }
float grad(int hash, float x, float y, float z)
{
int h = hash & 15; /* CONVERT LO 4 BITS OF HASH CODE */
float u = h < 8 ? x : y, /* INTO 12 GRADIENT DIRECTIONS. */
v = h < 4 ? y : h==12||h==14 ? x : z;
return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
}
#define P(x) p[(x) & 255]
float pnoise(float x, float y, float z) {
int X = (int)floorf(x) & 255, /* FIND UNIT CUBE THAT */
Y = (int)floorf(y) & 255, /* CONTAINS POINT. */
Z = (int)floorf(z) & 255;
x -= floorf(x); /* FIND RELATIVE X,Y,Z */
y -= floorf(y); /* OF POINT IN CUBE. */
z -= floorf(z);
float u = fade(x), /* COMPUTE FADE CURVES */
v = fade(y), /* FOR EACH OF X,Y,Z. */
w = fade(z);
int A = P(X)+Y,
AA = P(A)+Z,
AB = P(A+1)+Z, /* HASH COORDINATES OF */
B = P(X+1)+Y,
BA = P(B)+Z,
BB = P(B+1)+Z; /* THE 8 CUBE CORNERS, */
return lerp(w,lerp(v,lerp(u, grad(P(AA ), x, y, z), /* AND ADD */
grad(P(BA ), x-1, y, z)), /* BLENDED */
lerp(u, grad(P(AB ), x, y-1, z), /* RESULTS */
grad(P(BB ), x-1, y-1, z))), /* FROM 8 */
lerp(v, lerp(u, grad(P(AA+1), x, y, z-1), /* CORNERS */
grad(P(BA+1), x-1, y, z-1)), /* OF CUBE */
lerp(u, grad(P(AB+1), x, y-1, z-1),
grad(P(BB+1), x-1, y-1, z-1))));
}
// Show the current framerate & rendered pixels per second in the serial monitor.
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 ");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment