Superformula - a new way to make moving shapes

In my quest to glean fun stuff from Shadertoy, as @zranger1 and I attempt to find SDFs we can port to PB, I discovered this:

which is based on this:

Basically, it uses polar coordinates (radius/angle) with a variety of arguments (6-7), and makes all sorts of nifty shapes. So a morphing walk between items looks good, and whether you make random changes, or tie any of those parameters into time (so changing over time), or frequency buckets (changing to music), etc, it’ll make a cool looking pattern. It’s colorless by default, but that could be related to a parameter, or angle, or radius (or more than one of those), to generate something more visually interesting.

I haven’t written up the code for this yet, but wanted to record this, so I do come back and write it. A music response visual is my first desire, way more interesting than just a simple spectrometer, and depending on how it’s paying attention to sound (volume, buckets, major frequency, etc) should be a dancing shape that is entirely repeatable given the same input.

1 Like

Very cool. I read the wiki on it. I’m no mathematician, but it was clear to me that it’s a very special formula. Using it for a music reactive program sounds really intriguing. Can’t wait to see what you come up with.

1 Like

I’m still sort of buried at work, but had a little time this afternoon, so I did a quick port, plus a little extra color. Here it is:
(Edit - optimizations suggested by @scruffynerf)

// From https://www.shadertoy.com/view/llsyz8
// Yet another implementation of the supershape / superformula, 2017 by JT.
// cycling through the examples given in the wikipedia-article.
// Ref: https://en.wikipedia.org/wiki/Superformula
// Pixelblaze port 2021 ZRanger1

var shape = array(24);
shape[0] = vec4(3, 5, 18, 18);
shape[1] = vec4(6, 20, 7, 18);
shape[2] = vec4(4, 2, 4, 13);
shape[3] = vec4(7, 3, 4, 17);
    
shape[4] = vec4(7, 3, 6, 6);
shape[5] = vec4(3, 3, 14, 2);
shape[6] = vec4(19, 9, 14, 11);
shape[7] = vec4(12, 15, 20, 3);
    
shape[8] = vec4(8, 1, 1, 8);
shape[9] = vec4(8, 1, 5, 8);
shape[10] = vec4(8, 3, 4, 3);
shape[11] = vec4(12, 15, 20, 3);
    
shape[12] = vec4(5, 2, 6, 6);
shape[13] = vec4(6, 1, 1, 6);
shape[14] = vec4(6, 1, 7, 8);
shape[15] = vec4(7, 2, 8, 4);
    
shape[16] = vec4(3, 2, 8, 3);
shape[17] = vec4(3, 6, 6, 6);
shape[18] = vec4(4, 1, 7, 8);
shape[19] = vec4(7, 2, 8, 4);
    
shape[20] = vec4(2, 2, 2, 2);
shape[21] = vec4(2, 1, 1, 1);
shape[22] = vec4(2, 1, 4, 8);
shape[23] = vec4(3, 2, 5, 7);

// place coordinate origin at zero;
translate(-0.5,-0.5);
scale(0.5,0.5);

var timebase = 0;

// variables used for rendering
var i,i2,stepVal;
var sx,sy,sz,sw;

// fake 4-element vector initializer
// (Yes, I know I could use the new literal array intializers,
// but not everybody has upgraded yet, so...)
function vec4(x,y,z,w) {
  var v = array(4);
  v[0] = x; v[1] = y; v[2] = z; v[3] = w;
  return v;
  
  // the new, simple way! Use this if you've got 3.20 or newer firmware 
  // return [x,y,z,w];
}

// linear interpolation between start and end using val to weight between them.
function mix(start,end,val) {
  return start * (1-val) + end * val;
}

//Threshold function with a smooth transition.  Interpolates with a sigmoidal
//curve 0 and 1 when l < v < h. 
function smoothstep(l,h,v) {
    var t = clamp((v - l) / (h - l), 0.0, 1.0);
    return t * t * (3.0 - 2.0 * t);
}

// the actual superformula function
function superformula(phi,a,b,m, n1, n2, n3) {
    var tmp = m * phi / 4;
    var vx = abs(cos(tmp)/a);  var vy = abs(sin(tmp)/b);
    return pow(pow(vx, n2) + pow(vy, n3), -1/n1);
}

export function beforeRender(delta) {
  timebase = (timebase + delta / 1000) % 1000;
  
  // calculate indices for current and next shapes
  i = floor(mod(timebase,24));
  i2 = (i+1) % 24;  
  
  // how far into the morph we are at this time
  stepVal = smoothstep(0.2,0.8,frac(timebase)); 
  
  // interpolate parameters to morph between shapes
  sx = mix(shape[i][0],shape[i2][0],stepVal);
  sy = mix(shape[i][1],shape[i2][1],stepVal);
  sz = mix(shape[i][2],shape[i2][2],stepVal);
  sw = mix(shape[i][3],shape[i2][3],stepVal);    
}

export function render2D(index,x,y) {
  
  var r = superformula(atan2(y,x),1,1,sx,sy,sz,sw);
  r = smoothstep(0,1,r*0.2 - hypot(x,y));
  hsv(timebase+r, 1, r)
}
2 Likes

Astounding especially in HDR! :heart::heart::heart:

I had played a bit with this myself, but got some weird bugs I still needed to track down, so kudos to @zranger1 for the working code.

Caveats:

  • missing a and b arguments, but easy enough to add in
  • way extra work done that can be moved:
// interpolate parameters to morph between shapes
  sx = mix(shape[i][0],shape[i2][0],stepVal);
  sy = mix(shape[i][1],shape[i2][1],stepVal);
  sz = mix(shape[i][2],shape[i2][2],stepVal);
  sw = mix(shape[i][3],shape[i2][3],stepVal);

belongs in beforeRender, not render (none of those values change per pixel, so repeated recalcs every pixel are wasted cpu)

working on a randomizing one, and then will (re)work on a musical one.

The code above has been edited w/@scruffynerf’s suggestions, including adding the a,b parameters back in. The shader example actually didn’t need them because those values were always 1 in the wikipedia shape data they were using.

It’s currently running at about 42 fps on my 16x16 matrix, which should leave plenty of headroom for music reactive code!

2 Likes