WLED pattern porting?

The same pattern can look very different with a different pallette. Obvious example: Fire is all red/orange/yellows… Change that to a blue/green/violet and it’s a very different look.

I see palettes as a valid way to say “use these colors”, which we don’t otherwise have now.

Well, I’d still prefer to let the user select one or two base colors with UI sliders, then generate the rest computationally as part of the pattern. But having said that, here’s a palette-stored-in-map proof of concept, just as something to think about.

The main simplifying assumption this makes is that because we’re theoretically running ported WLED patterns, we’re driving a 1D strip, as most stock WLED patterns do. That leaves the whole 3D map available to us as storage. To try this out, install the test mapper, and run the pattern. The test palette isn’t especially inspired, but it is um, quite visible!

Here’s the test mapper, with a 5 element RGB palette:

function (pixelCount) {
  paletteSize = 5;

  // construct palette here.  First entry is palette size,
  // repeated in x,y,z.  The next entries are the
  // palette RGB colors, followed by low and high
  // sentinel values to control normalization.
  var map = [
    [5,5,5],
    [216,0,0],
    [0,131,84],
    [238,75,106],
    [0,59,200],
    [15,113,115],
    [-1,-1,-1],
    [256,256,256]
  ]
  
// fill the rest of the map with zeros
  for (i = paletteSize + 3; i < pixelCount; i++) {
    map.push([0, 0, 0])
  }
  return map
}

And here’s the demo pattern:

// This is a sneaky way of storing a palette in 3D map data
// for use in 1D patterns

var MAX_PALETTE_LENGTH = 6;
var paletteLength = 0;
var paletteR = array(MAX_PALETTE_LENGTH);
var paletteG = array(MAX_PALETTE_LENGTH);
var paletteB = array(MAX_PALETTE_LENGTH);

var drawFrame = renderGetPalette;
var paletteRetrieved = 0;

// use this renderer on the first frame to retrieve the palette from the
// 3D map data
function renderGetPalette(index,x,y,z) {
  // de-normalize palette data ((x * range) - low Value)
  x = (x * 257)-1;
  y = (y * 257)-1;
  z = (z * 257)-1;  

  // palette length is duplicated in first x,y,z
  // entries, so if these are all identical, it's a good
  // bet we've got a palette instead of a "real" map.
  if (index == 0) {
    if ((x == y) && (y == z)) paletteLength = floor(x);
  } 
  else if (index <= paletteLength) {
    var n = index - 1;
    paletteR[n] = floor(x + 0.5) / 255;
    paletteG[n] = floor(y + 0.5) / 255;
    paletteB[n] = floor(z + 0.5) / 255;
  }
  paletteRetrieved = 1;
}

// once the palette is retrieved, use this render to draw
// onto our 1D strip.
function renderRunPattern(index,x,y,z) {
  render(index);
}

export function beforeRender(delta) {
  if (paletteRetrieved) drawFrame = renderRunPattern;
}

export function render3D(index,x,y,z) {
  drawFrame(index,x,y,z);
}

// proof-of-concept -- just divides strips into equal segments
// with a palette color in each segment!
export function render(index) {
 var n = (index/pixelCount) * paletteLength; 
 rgb(paletteR[n],paletteG[n],paletteB[n]);
}
1 Like

While I get your idea… Changing a map is non trivial…

I really dislike the idea. Still mulling alternatives.

True enough!

For now, if anybody’s crazy enough to do something like this, it could easily be generalized a bit to store several palettes, which patterns could select via slider. It’s in no way an ideal solution, but it does show one way to share global (constant) data between patterns.

1 Like

Yeah, we’re in a bit of bootstrap problem:

We don’t have pallettes, so the need for adding pallette support to the API/firmware isn’t needed, which would make it much easier.

Honestly, a global variable API (someway to stash multi pattern variables) is really the missing bit. Pallette storage is just one possible use.

I took a shot at porting Distortion Waves last night. First I did a more-or-less straight transliteration of the FastLED code (though I got rid of the gamma LUT and replaced the cosine LUT with a PB wave function) and that was OK, though there were lots of artefacts in the middle which I think were due to numeric overflows.

Then I started hacking away to make it more PB-like. I’ve now got it very simple and PB-friendly, but in the process of scaling from FastLED’s 0…255 intensities and Soulmate’s 1-20 coordinates to PB’s 0…1 world I lost track of what the scaling coefficients should be at each stage, so it’s now 95% correct but 100% wrong (i.e. the code looks good but the results don’t resemble the original).

I’m tired of looking at it, and feeling distinctly inadequate after seeing @zranger1’s Great Metaballs of Fire, so I’ll post it here as a starting point if someone with fresh eyes wants to carry it forward…

//  Cobbled together from the original at: https://editor.soulmatelights.com/gallery/1089-distorsion-waves

//  simple replacement for LUT
function cos_wave(proportion) { return 1-wave(proportion+0.25); }
function beatsin(bpm) { return wave(time(0.91552734375/bpm)); }

// adjustments
timeBase = 0.1;
speed = 5;
w = 2;

export function beforeRender(delta) {
  a1=time(timeBase); a2=time(2*timeBase); a3=time(3*timeBase);
  cx1 = beatsin(10-speed); cy1 = beatsin(12-speed); 
  cx2 = beatsin(13-speed); cy2 = beatsin(15-speed);
  cx3 = beatsin(17-speed); cy3 = beatsin(14-speed);
}

export function render2D(index, x, y) {
/*
  byte rdistort = cos_wave[   (cos_wave[((x << 3) + a1) & 255]    + cos_wave[   ((y << 3) - a2) & 255]    + a3     ) & 255   ] >> 1;
  byte gdistort = cos_wave[   (cos_wave[((x << 3) - a2) & 255]    + cos_wave[   ((y << 3) + a3) & 255]    + a1 + 32) & 255   ] >> 1;
  byte bdistort = cos_wave[   (cos_wave[((x << 3) + a3) & 255]    + cos_wave[   ((y << 3) - a1) & 255]    + a2 + 64) & 255   ] >> 1;
*/
  coeff1 = 0.06; 
  r1 = coeff1*cos_wave(x+a1); 
  g1 = coeff1*cos_wave(x-a2); 
  b1 = coeff1*cos_wave(x+a3);
  
  coeff2 = 1; 
  r2 = coeff2*cos_wave(y-a2); 
  g2 = coeff2*cos_wave(y+a1); 
  b2 = coeff2*cos_wave(y-a2);
  
  coeff3 = 1; 
  rdistort = coeff3*cos_wave(r1+r2+a3); 
  gdistort = coeff3*cos_wave(g1+g2+a1+1/8);  
  bdistort = coeff3*cos_wave(b1+b2+a2+1/4); 

/*
  byte valueR = rdistort + w * (a1 - (((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1)) >> 7));
  byte valueG = gdistort + w * (a2 - (((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2)) >> 7));
  byte valueB = bdistort + w * (a3 - (((xoffs - cx3) * (xoffs - cx3) + (yoffs - cy3) * (yoffs - cy3)) >> 7));
*/
  dx1 = x-cx1; dy1 = y-cy1; dx2 = x-cx2; dy2 = y-cy2; dx3 = x-cx3; dy3 = y-cy3;
  r = cos_wave(rdistort + w*(a1-(dx1*dx1 + dy1*dy1)));
  g = cos_wave(gdistort + w*(a2-(dx2*dx2 + dy2*dy2)));
  b = cos_wave(bdistort + w*(a3-(dx3*dx3 + dy3*dy3)));

  rgb(r*r, g*g, b*b); 
}
2 Likes

@Pixie – you had this! The only thing missing is that WLED’s coordinates come in as integer pixel numbers and Pixelblazes are normalized and scaled. To get the numbers back to something like the proper scaling for this pattern, try adding the line:

scale(0.5,0.25)

somewhere in the initialization section. (YMMV on the specific values – this looked good to me, and seems to correct for the aspect ratio of the rectangular display he’s running it on in the video.)

1 Like

I don’t know how I (re)missed the Palette Utility in the pattern collection.

Discussion here (which I even participated in)

I think next step is to try porting a WLED pattern that assumes palette usage and see how awkward it is… I still think maps aren’t an answer, but I’m not sure we have a good answer yet. If we had includes (for example), we could have a palette include with desired palettes.

Nice work!

I will play with this after dinner, and I’m already envisioning replacing some of the hard coded values with sliders to make it more adjustable. But glad @zranger1 noticed the easy fix.

You aren’t alone. I was porting some generic JS stuff and couldn’t figure out why my matrix displayed half of my pattern fine, and the other half all screwy… I was doing the canvas as an array, and referencing it in render, and dammit, what was I doing wrong? Oh right, my matrix is zigzag and so using a bare index? Half of it will be reversed. Or do it the right way and use x, y, which for some silly reason I wasn’t doing)

Sometimes you just need to put it aside and come back fresh, or get new eyeballs.

I have at least one awesome pattern on hold, waiting for me to come back to it with fresh energy and figure out what I wasn’t doing right.

I think it’s been raised elsewhere in the feature enhancements thread, but we really need a render1D(index, X) that respects the pixelmap; otherwise the wiring of many 2D and 3D objects really messes up 1D patterns.

I’ve started pasting this into all the 1D patterns so they’ll look the same across all my 2D objects:

export function render1D(index, x) {
  render(floor(x*pixelCount)); // call the original renderer with a corrected index
}

var matrixWidth = sqrt(pixelCount), matrixHeight = pixelCount / matrixWidth;
export function render2D(index, x, y) {
  row = floor(x*matrixHeight);
  column = floor(y*matrixWidth);
  offset = row * matrixHeight + column;
  render1D(index, offset/pixelCount);
}
1 Like

I found a few differences between the original code formulas and yours, minor but significant in effect, like using a2 instead of a3 (I think that was it)

Here’s a slider heavy version, intended to let someone play with all of the variables until they find an effect they like…

Some changes, including…

  • Using wave(time()) rather than time() to avoid what I found were distracting ‘flash changes’ when something went from 1 to 0… also without millis(), we really don’t have a long ‘t’ to replace it. @wizard, consider this a plea for some way to emulate that, but we also don’t really have anything longer than a 16.16 number, which doesn’t really help us here. millis() is used in the original (and many other patterns) as the driving force, since it’ll grow to 4,294,967,295 (32 unsigned) before it rolls over.

  • Log scale sliders, so picking values below and above 1 was easier.

  • Decoupled the time bases… that’s another change from the original code… in Original, time then time/2 then time/3, but in your code above, you grew the time base by x2 and x3

Distortion Waves TNG, with sliders upon sliders
// original code by ldirko_Yaroslaw Turbin 17-06-2021
// https://twitter.com/ldir_ko        https://vk.com/ldirko
// https://www.youtube.com/c/ldirldir https://www.reddit.com/user/ldirko/  

// original code: https://editor.soulmatelights.com/gallery/1089-distorsion-waves
// and https://wokwi.com/arduino/projects/301639284294681097

// modified/recoded by @pixie at https://forum.electromage.com/t/wled-pattern-porting/1295/22
// then re-modified quite a bit by Scruffynerf

//  simple replacement for original LUTs
function cos_wave(proportion) { return 1-wave(proportion+0.25); }
function beatsin(bpm) { return wave(time(0.91552734375/bpm)); }

// adjustments - all sliders now
export var timeBase1 = 0.2;
export var timeBase2 = 0.1;
export var timeBase3 = 0.66;
export var speed = 4;
export var w = .05;
export var scalefactor = .5;
export var coeff1,coeff2,coeff3

export function sliderSpeed(v){
  speed = v*9.9
}

var minv = log(.1);
var maxv = log(3);

export function sliderScale(v){
  scalefactor = exp(minv + (maxv-minv)*v);
}

var minv2 = log(.01);
export function sliderWiggle(v){
  w = exp(minv2 + (maxv-minv2)*v);
}

var maxv2 = log(30);

export function sliderTime1(v){
  timeBase1 = exp(minv2 + (maxv2-minv2)*v) +.0001
}
export function sliderTime2(v){
  timeBase2 = exp(minv2 + (maxv2-minv2)*v) +.0001
}
export function sliderTime3(v){
  timeBase3 = exp(minv2 + (maxv2-minv2)*v) +.0001
}

coeff1 = 1; 
coeff2 = 1; 
coeff3 = 1; 

export function sliderC1(v){
  coeff1 = exp(minv + (maxv-minv)*v);
}
export function sliderC2(v){
  coeff2 = exp(minv + (maxv-minv)*v);
}
export function sliderC3(v){
  coeff3 = exp(minv + (maxv-minv)*v);
}

export function beforeRender(delta) {
  resetTransform()
  //translate(-.5, -.5)
  scale(scalefactor,scalefactor)
  
  a1=wave(time(timeBase1*2))*2-1;
  a2=wave(time(timeBase2*2))*2-1; 
  a3=wave(time(timeBase3*2))*2-1;

  cx1 = beatsin(10-speed)
  cx2 = beatsin(13-speed)
  cx3 = beatsin(17-speed)
  cy1 = beatsin(12-speed)
  cy2 = beatsin(15-speed)
  cy3 = beatsin(14-speed)
}

// debugging uncomment - 
// export var _r,_g,_b,cx1,cx2,cx3,cy1,cy2,cy3,dx1,dx2,dx3,dy1,dy2,dy3

export function render2D(index, x, y) {
  r1 = coeff1*cos_wave(x+a1); 
  g1 = coeff1*cos_wave(x-a2); 
  b1 = coeff1*cos_wave(x+a3);
  
  r2 = coeff2*cos_wave(y-a2); 
  g2 = coeff2*cos_wave(y+a3); 
  b2 = coeff2*cos_wave(y-a1);
  
  rdistort = coeff3*cos_wave(r1+r2+a3); 
  gdistort = coeff3*cos_wave(g1+g2+a1+1/8);  
  bdistort = coeff3*cos_wave(b1+b2+a2+1/4); 

  dx1 = (x-cx1)*(x-cx1);
  dx2 = (x-cx2)*(x-cx2);
  dx3 = (x-cx3)*(x-cx3);
  dy1 = (y-cy1)*(y-cy1);
  dy2 = (y-cy2)*(y-cy2);
  dy3 = (y-cy3)*(y-cy3);

  _r = rdistort + w*(a1-(dx1 + dy1))
  _g = gdistort + w*(a2-(dx2 + dy2))
  _b = bdistort + w*(a3-(dx3 + dy3))
  rgb(_r*_r, _g*_g, _b*_b);
}
1 Like

For a substitute tick count, here’s what I use when porting shader things:

var timebase = 0;

export function beforeRender(delta) {
  timebase = (timebase + delta/1000) % 3600
}

This gives a fractional second counter that rolls over once an hour. Long enough to avoid most visual artifacts, short enough to give you a little room to scale it and not run into integer scaling issues.

2 Likes

I just came across Aurora, and the video of the many patterns it did/does:

Kudos to @JasonCoon and @embedded-creations on all that work.

I’ve been following links and realizing that bitrot is starting to eat older led libraries/demos/etc (led effects by ratkins is long gone, for example, unless I can find a backup repo) so porting is also a form of preservation and carrying algos forward to the future.

Also a port of it to neomatrix:

I think a lot of these still don’t exist for PB yet, so they go on the (growing) porting pile.

3 Likes

Here’s another port in case it’s useful:

Also some more links with the latest details on actually compiling Aurora for SmartMatrix Library (it’s not straightforward as the latest code is on a branch):

3 Likes

If anyone asks, “Why Pixelblaze?” this is one really good answer…

I was wandering through my pattern library, and stopped to look at the code for @pixie and @scruffynerf’s Distortion Waves port. Just casually wondered, “What would this look like with radial coordinates?”

One line of code:
tmp = atan2(y,x); y = hypot(x,y); x = tmp;
added at the front of render - and a few seconds of parameter tweaking later, I had my answer. It’s… really cool! (I’ve attached source below. Absolutely worth running to have a look! )

Yes, you could do this sort of playing and exploration on a compiled platform, but there are a lot more obstacles in the way. Pixelblaze is really excellent at providing the shortest path between crazy idea and actual blinking LEDs!

WLED Distortion Waves - Radial Version
// original code by ldirko_Yaroslaw Turbin 17-06-2021
// https://twitter.com/ldir_ko        https://vk.com/ldirko
// https://www.youtube.com/c/ldirldir https://www.reddit.com/user/ldirko/  

// original code: https://editor.soulmatelights.com/gallery/1089-distorsion-waves
// and https://wokwi.com/arduino/projects/301639284294681097

// modified/recoded by @pixie at https://forum.electromage.com/t/wled-pattern-porting/1295/22
// then re-modified quite a bit by Scruffynerf

//  simple replacement for original LUTs
function cos_wave(proportion) { return 1-wave(proportion+0.25); }
function beatsin(bpm) { return wave(time(0.91552734375/bpm)); }

// adjustments - all sliders now
export var timeBase1 = 0.06228;
export var timeBase2 = 0.04;
export var timeBase3 = 0.057;
export var speed = 9.2;
export var w = .015;
export var scalefactor = 1;
export var coeff1,coeff2,coeff3

var minv = log(.1);
var maxv = log(3);
var maxv2 = log(30);
coeff1 = .21; 
coeff2 = .6626; 
coeff3 = .8245;
var minv2 = log(.01);

/*  uncomment to re-enable UI
export function sliderSpeed(v){
  speed = v*9.9
}

var minv2 = log(.01);
export function sliderWiggle(v){
  w = exp(minv2 + (maxv-minv2)*v);
}

export function sliderTime1(v){
  timeBase1 = exp(minv2 + (maxv2-minv2)*v) +.0001
}
export function sliderTime2(v){
  timeBase2 = exp(minv2 + (maxv2-minv2)*v) +.0001
}
export function sliderTime3(v){
  timeBase3 = exp(minv2 + (maxv2-minv2)*v) +.0001
}

export function sliderC1(v){
  coeff1 = exp(minv + (maxv-minv)*v);
}
export function sliderC2(v){
  coeff2 = exp(minv + (maxv-minv)*v);
}
export function sliderC3(v){
  coeff3 = exp(minv + (maxv-minv)*v);
}
*/

//  resetTransform()
  translate(-.5, -.5)
  scale(scalefactor,scalefactor)

export function beforeRender(delta) {

  a1=wave(time(timeBase1*2))*2-1;
  a2=wave(time(timeBase2*2))*2-1; 
  a3=wave(time(timeBase3*2))*2-1;

  cx1 = beatsin(10-speed)
  cx2 = beatsin(13-speed)
  cx3 = beatsin(17-speed)
  cy1 = beatsin(12-speed)
  cy2 = beatsin(15-speed)
  cy3 = beatsin(14-speed)
}

// debugging uncomment - 
// export var _r,_g,_b,cx1,cx2,cx3,cy1,cy2,cy3,dx1,dx2,dx3,dy1,dy2,dy3

export function render2D(index, x, y) {
  tmp = atan2(y,x); y = hypot(x,y); x = tmp;
  r1 = coeff1*cos_wave(x+a1); 
  g1 = coeff1*cos_wave(x-a2); 
  b1 = coeff1*cos_wave(x+a3);
  
  r2 = coeff2*cos_wave(y-a2); 
  g2 = coeff2*cos_wave(y+a3); 
  b2 = coeff2*cos_wave(y-a1);
  
  rdistort = coeff3*cos_wave(r1+r2+a3); 
  gdistort = coeff3*cos_wave(g1+g2+a1+1/8);  
  bdistort = coeff3*cos_wave(b1+b2+a2+1/4); 

  dx1 = (x-cx1)*(x-cx1);
  dx2 = (x-cx2)*(x-cx2);
  dx3 = (x-cx3)*(x-cx3);
  dy1 = (y-cy1)*(y-cy1);
  dy2 = (y-cy2)*(y-cy2);
  dy3 = (y-cy3)*(y-cy3);

  _r = rdistort + w*(a1-(dx1 + dy1))
  _g = gdistort + w*(a2-(dx2 + dy2))
  _b = bdistort + w*(a3-(dx3 + dy3))
  rgb(_r*_r, _g*_g, _b*_b);
}
1 Like

actually @zranger1, it’s funny you mention this, cause… aircoookie of WLED is doing a survey, specifically asking about what you’d like to see in a “Live effect editor”

As you may or may not know, I am currently working on my Bachelor’s thesis, and you can help!
The topic is related to WLED and I am conducting some research to assess how much you would like a specific new feature.

It would delight me if you can spare 5 minutes to answer the survey:
https://forms.gle/bHxcg4N5BkUf2rhY6
This survey will run for 14 days.

Thank you very much and have a nice week!

1 Like

Ha! I filled it out – it’ll be interesting to see what he comes up with. Wonder how many votes, “Give us the option of switching between polar and cartesian coordinates”, will get.

1 Like

Now that we’ll have array literals, a few things become easier. Palettes for one thing.
@jeff 's palette utility used some nifty functions to ‘set3’ or ‘set8’ to populate arrays.
Now that it’ll be much easier to just put the array in directly, the code can be cleaner.
I need to revisit it, and see how easy it would be use it for ‘quick’ porting purposes from fastled/wled.

1 Like

Ok, I’ve got all of the main wled palettes ported now. Code shortly. I want to do a lot of cleanup on it. I’ve taken @pixie 's code, and added 8bit packing, so each palette is now essentially a one dimensional array of single 16.16 numbers, stored as small as is possible, meaning you can pack quite a bit in there.
The library will include all of the wled palettes but limiting your own code to a handful you that like will be pretty easy.

It’s not perfect yet… still some bugs to find.
for example:

wledpalette[0] = [
  p( 0, 194, 1, 1),
  p( 94, 1, 29, 18),
  p(132, 57, 131, 28),
  p(255, 113, 1, 1)
];

when I watch that, I get:
wledpalette[0] = [1.006866, 24093.07, -31612.89, -254.9944]
but that’s not quite the same pattern, missing some blue. I suspect it’s 16.15 issue, (but I’m storing red not blue in the last bits… ) but… we’ll see.

1 Like

TIL: WLED has a fixed FPS (frames per second) of 42.

So given how simple some of the patterns are, some sort of FPS limiting will likely be needed, as I’m pretty sure they’d run faster than 42 FPS on a PB.

I did just outline a way to do “delays” in PB, but I suspect matching a FPS like this is a bit more complex. It would involve looking at the Delta and ensure it stayed in the right range (23.8095238 milliseconds per render - so between 23 and 24 millis of Delta) and then do something to either speed up (skip a frame?) or slow down (stay static for a frame?). Ideas?