Rotation functions?

Discussion in the Circle post lead to an issue with rotation. Given a formula like @zranger1 's that define a given shape based on shader style math, typically a rotation would be a separate function

@wizard, since you’ve put the map rotation functions into the firmware, is it possible to expose similar functions for a given (x,y[,z]) and given angle? So we could have pointRotateZ(x,y,angle) and get back an array of x and y?

We can do it in usercode but I suspect it’s the exact same math you’re doing already and faster.

ObInterestingMath: Let's remove Quaternions from every 3D Engine (An Interactive Introduction to Rotors from Geometric Algebra) - Marc ten Bosch
(Which removes quarterions entirely and just uses vectors to define rotation!)

I do plan on applying transforms with the map walking function. That means you could run through the map from multiple transforms.

But you are looking for coordinate transformation on arbitrary coordinates?

The math behind the pixel map coordinate transformations is a bit more involved and uses 4x4 transformation matrices. I found this article very helpful.

If you had to transform 3 points, I’d use the simple function.

1 Like

Agreed:

rotate point (px, py) around point (ox, oy) by angle theta:

p'x = cos(theta) * (px-ox) - sin(theta) * (py-oy) + ox

p'y = sin(theta) * (px-ox) + cos(theta) * (py-oy) + oy

That will work for our purposes, just calculate the rotated x+y values before using zranger’s functions should work, I think. And for a given rotation of a shape, you could precalculate the sine/cosine just once per shape. So pretty quick.

turns out to be even simpler for your code, @zranger1

shapetheta = theta + objects[i][6] // shape's angle, so each can be different
px = cos(shapetheta) * (x-objects[i][0]) - sin(shapetheta) * (y-objects[i][1])
py = sin(shapetheta) * (x-objects[i][0]) + cos(shapetheta) * (y-objects[i][1])
d = shapeSdf[objects[i][5]](px,py,objectSize);

You were already doing most of the origin relative math (e.g. x-objects[i][0]), so very easy to add.

Shape code, with shape rotation added
// adds per shape rotation 

// Global Variables
var maxObjects = 4;
var numObjects = 4;
export var objectSize = 0.21;
export var speed = 0.18;
var numShapes = 4;
var shapeSdf = array(numShapes)
var shapeCompare = array(2);
var filled = 1;
var lineWidth = 0.04;

var theta;

shapeCompare[0] = (f) => (abs(f) > lineWidth); // unfilled shapes
shapeCompare[1] = (f) => (f > lineWidth);      // filled shapes

shapeSdf[0] = circle;
shapeSdf[1] = square;
shapeSdf[2] = triangle;
shapeSdf[3] = hexagon;

// signed distance functions for various shapes, adapted for 2D. 
// Math from https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
function circle(x,y,r) {
  return hypot(x,y) - r;
}

function square(x,y,size) {
  dx = abs(x) - size;  d1 = max(dx,0);
  dy = abs(y) - size;  d2 = max(dy,0);
	return min(max(dx, dy), 0.0) + hypot(d1,d2);
}

function triangle(x,y,r) {
	return max((abs(x) * 0.866025) - (y * 0.5), y) - r / 2;
}

function hexagon(x,y,r){
     x = abs(x); y = abs(y);
     return  max((x * 0.5 + y * 0.866025),x) - r;
}

// array of object vectors
var objects = array(maxObjects);

// UI
export function sliderSize(v) {
  objectSize = 0.4 * v;
}

export function sliderSpeed(v) {
  speed = v;
}

export function sliderFilled(v) {
  filled = (v >= 0.5);
}

// allocate memory for object vectors
function createObjects() {
  for (var i = 0; i < maxObjects; i++) {  
    objects[i] = array(8);
  }
}

// create object vector with a random position, direction, speed, color
function initObjects() {
  var hue = random(1);
  for (var i = 0; i < numObjects; i++) {
    var b = objects[i];  
    
    b[0] = random(1);     // x pos
    b[1] = random(1);     // y pos

    b[2] = random(0.2);  // x velocity
    b[3] = random(0.2);  // y velocity

    b[4] = hue;           // color
    b[5] = i % numShapes; // shape
    b[6] = random(1)*PI2  // rotation angle
    hue += 0.619033
  }
}

// move objects and bounce them off "walls"
function bounce() {
  for (var i = 0; i < numObjects; i++) {
    var b = objects[i];
    
// move object
    b[0] += b[2] * speed;
    b[1] += b[3] * speed;

// bounce off walls by flipping vector element sign when we hit.
    if (b[0] < 0) { b[0] = 0; b[2] = -b[2]; continue; } 
    if (b[1] < 0) { b[1] = 0; b[3] = -b[3]; continue; }

    if (b[0] > 1) { b[0] = 1; b[2] = -b[2]; continue; }
    if (b[1] > 1) { b[1] = 1; b[3] = -b[3]; continue; }
  }
}

createObjects();
initObjects();

export function beforeRender(delta) {
  bounce();
  theta = PI2 * time(0.1);

// uncomment the block below to rotate entire scene around its
// center
/*

  resetTransform();
  translate(-0.5,-0.5);  
  rotate(theta);  
  translate(0.5,0.5);
*/  
}

export function render2D(index,x,y) {
  var d;
  var v = 0;

  for (var i = 0; i < numObjects; i++) {
    shapetheta = theta + objects[i][6]
    px = cos(shapetheta) * (x-objects[i][0]) - sin(shapetheta) * (y-objects[i][1])
    py = sin(shapetheta) * (x-objects[i][0]) + cos(shapetheta) * (y-objects[i][1])
    d = shapeSdf[objects[i][5]](px,py,objectSize);
    if (shapeCompare[filled](d)) continue;
    
    v = 1-(d/0.04);
    s = 1.5-abs(d)/objectSize
    h = objects[i][4]-d;      
    break;
  }
     
  hsv(h, s, v*v*v)
}

added I see your concern about speed vs number of objects

I tweaked the above to cache the sin/cos in the shape pre render, and then just pull it once.

With 8 squares, depending on size/fill, it goes as slow as 12fps or so… probably more way to optimize it, but not bad at all.

Added:
if (abs(sx) > objectSize * 1.1 || abs(sy) > objectSize * 1.1) continue;
more than doubled the framerate by avoiding the check if the point is well outside of the size, which cuts down on scanning every pixel for each shape, even if the shape is well away from the pixel, so there is no way it’s close enough. 25-28fps (varies based on size/filled/etc)

8 squares rotating for FPS purposes, some caching done
// adds per shape rotation 


// Global Variables
var maxObjects = 8;
var numObjects = 8;
export var objectSize = 0.21;
export var speed = 0.18;
var numShapes = 4;
var shapeSdf = array(numShapes)
var shapeCompare = array(2);
var filled = 1;
var lineWidth = 0.04;

var theta;

shapeCompare[0] = (f) => (abs(f) > lineWidth); // unfilled shapes
shapeCompare[1] = (f) => (f > lineWidth);      // filled shapes

shapeSdf[0] = circle;
shapeSdf[1] = square;
shapeSdf[2] = triangle;
shapeSdf[3] = hexagon;

// signed distance functions for various shapes, adapted for 2D. 
// Math from https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
function circle(x,y,r) {
  return hypot(x,y) - r;
}

function square(x,y,size) {
  dx = abs(x) - size;  d1 = max(dx,0);
  dy = abs(y) - size;  d2 = max(dy,0);
	return min(max(dx, dy), 0.0) + hypot(d1,d2);
}

function triangle(x,y,r) {
	return max((abs(x) * 0.866025) - (y * 0.5), y) - r / 2;
}

function hexagon(x,y,r){
     x = abs(x); y = abs(y);
     return  max((x * 0.5 + y * 0.866025),x) - r;
}

// array of object vectors
var objects = array(maxObjects);

// UI
export function sliderSize(v) {
  objectSize = 0.4 * v;
}

export function sliderSpeed(v) {
  speed = v;
}

export function sliderFilled(v) {
  filled = (v >= 0.5);
}

// allocate memory for object vectors
function createObjects() {
  for (var i = 0; i < maxObjects; i++) {  
    objects[i] = array(9);
  }
}

// create object vector with a random position, direction, speed, color
function initObjects() {
  var hue = random(1);
  for (var i = 0; i < numObjects; i++) {
    var b = objects[i];  
    
    b[0] = random(1);     // x pos
    b[1] = random(1);     // y pos

    b[2] = random(0.2);  // x velocity
    b[3] = random(0.2);  // y velocity

    b[4] = hue;           // color
    b[5] = 1 // shape
    b[6] = random(1)*PI2  // rotation
    b[7] = sin(b[6])
    b[8] = cos(b[6])
    hue += 0.619033
  }
}

// move objects and bounce them off "walls"
function bounce() {
  for (var i = 0; i < numObjects; i++) {
    var b = objects[i];
    
// move object
    b[0] += b[2] * speed;
    b[1] += b[3] * speed;

// bounce off walls by flipping vector element sign when we hit.
    if (b[0] < 0) { b[0] = 0; b[2] = -b[2]; continue; } 
    if (b[1] < 0) { b[1] = 0; b[3] = -b[3]; continue; }

    if (b[0] > 1) { b[0] = 1; b[2] = -b[2]; continue; }
    if (b[1] > 1) { b[1] = 1; b[3] = -b[3]; continue; }
  }
}

createObjects();
initObjects();

export function beforeRender(delta) {
  bounce();
  theta = PI2 * time(0.1);
  for (var i = 0; i < numObjects; i++) {
    shapetheta = theta + objects[i][6]
    objects[i][7] = sin(shapetheta)
    objects[i][8] = cos(shapetheta)
  }
// uncomment the block below to rotate entire scene around its
// center
/*

  resetTransform();
  translate(-0.5,-0.5);  
  rotate(theta);  
  translate(0.5,0.5);
*/  
}

export function render2D(index,x,y) {
  var d;
  var v = 0;
  for (var i = 0; i < numObjects; i++) {
    sx = x-objects[i][0]
    sy = y-objects[i][1]
    if (abs(sx) > objectSize * 1.1 || abs(sy) > objectSize * 1.1) continue;
    sinshape = objects[i][7]
    cosshape = objects[i][8]
    px = cosshape * sx - sinshape * sy
    py = sinshape * sx + cosshape * sy
    d = shapeSdf[objects[i][5]](px,py,objectSize);
    if (shapeCompare[filled](d)) continue;
    
    v = 1-(d/0.04);
    s = 1.5-abs(d)/objectSize
    h = objects[i][4]-d;      
    break;
  }
     
  hsv(h, s, v*v*v)
}

Follow up, cause my GF came home and immediately proclaimed the running pattern was ‘80s kid show’, so I guess I’d better share the code, and add it to the pattern collectionn.

80s kid show
// 80s kid show, by Scruffynerf, with huge chunks of code by @zranger
// v1.0ish, but certainly hackable for more fun.  8-2021

// Global Variables
var maxObjects = 16;
var objectProperties = 12
export var numObjects = 10;
export var objectSize = 0.21;
export var speed = 0.18;
export var filled = 1;
export var lineWidth = 0.04;
export var bounds = 1;
export var spin = 1;
export var flat = 1;
export var whichShapes = 2;
var old = 2
var theta;
var numShapes = 5;
var shapeSdf = array(numShapes)
var shapeCompare = array(2);
shapeCompare[0] = (f) => (abs(f) > lineWidth); // unfilled shapes
shapeCompare[1] = (f) => (f > lineWidth);      // filled shapes

shapeSdf[0] = circle;
shapeSdf[1] = square;
shapeSdf[2] = triangle;
shapeSdf[3] = hexagon;
shapeSdf[4] = hexstar;

// signed distance functions for various shapes, adapted for 2D. 
// Math from https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
function circle(x,y,r) {
  return hypot(x,y) - r;
}

function square(x,y,size) {
  dx = abs(x) - size;  d1 = max(dx,0);
  dy = abs(y) - size;  d2 = max(dy,0);
	return min(max(dx, dy), 0.0) + hypot(d1,d2);
}

function triangle(x,y,r) {
	return (max((abs(x) * 0.866025) - (y * 0.5), y) - r / 2);
}

function hexagon(x,y,r){
     x = abs(x); y = abs(y);
     return  max((x * 0.5 + y * 0.866025),x) - r;
}

function hexstar(x,y,r) {
  x = abs(x); y = abs(y); 
  // the vector of scary constants is just a grab bag of precalculated values.
  // 1.73205 is the square root of 3.  0.866025 is half that
  // these constants are used in a lot of hexagonal sdf things
  // because on a hex with flat to flat radius of 0.5, the
  // distance between the points is 0.5/sqrt(3)
  dot = 2 * min(-0.5*x + 0.866025 * y,0);
  x -= dot * -0.5; y -= dot * 0.866025;
  
  dot = 2 * min(0.866025*x + -0.5 * y,0);
  x -= dot * 0.866025; y -= dot * -0.5;
  
  x -= clamp(x, r * 0.5773502692, r * 1.7320508076);
  y -= r;
  result = hypot(x,y);
  // TODO - could be more efficient here.  I know I've got a fast bit masking
  // signum function around somewhere.
  return (y > 0) ? result : -result;
}


// array of object vectors
var objects = array(maxObjects);

// UI
export function sliderFloaters(v) {
  numObjects = floor(maxObjects * v);
}

export function sliderType(v) {
  old = whichShapes;
  whichShapes = floor((numShapes) * v);
  if (old != whichShapes){ initObjects(); }
}
export function sliderSize(v) {
  objectSize = 0.4 * v;
}

export function sliderSpeed(v) {
  speed = v;
}

export function sliderFilled(v) {
  filled = (v >= 0.5);
}

export function sliderLine(v) {
  lineWidth = 0.04 + 0.05* v
}

export function sliderCutoff(v) {
  bounds = 1 + v
}

export function sliderSpin(v) {
  spin = (v >= 0.5);
}

export function sliderFlat(v) {
  flat = (v <= 0.5);
}

// allocate memory for object vectors
function createObjects() {
  for (var i = 0; i < maxObjects; i++) {  
    objects[i] = array(objectProperties);
  }
}

// create object vector with a random position, direction, speed, color, etc
function initObjects() {
  var hue = random(1);
  for (var i = 0; i < maxObjects; i++) {
    var b = objects[i];  
    
    b[0] = random(1);     // x pos
    b[1] = random(1);     // y pos

    b[2] = random(0.2);  // x velocity
    b[3] = random(0.2);  // y velocity

    b[4] = hue;           // color
    
    if (whichShapes == numShapes){
      b[5] = floor(random(numShapes)) // random shape mix
    } else {
      b[5] = whichShapes // specific shape
    }

    b[6] = random(1)*PI2  // rotation
    b[7] = sin(b[6])
    b[8] = cos(b[6])

    b[9] = random(2)+1 // object variation in size
    b[10] = random(1) //  timing of growth cycle
    b[11] = 1 // current size caching so we save the final size per render
    hue += 0.4
  }
}

// move objects and bounce them off "walls"
function bounce() {
  for (var i = 0; i < numObjects; i++) {
    var b = objects[i];
    
// move object
    b[0] += b[2] * speed;
    b[1] += b[3] * speed;

// bounce off walls by flipping vector element sign when we hit.
    if (b[0] < 0) { b[0] = 0; b[2] = -b[2]; continue; } 
    if (b[1] < 0) { b[1] = 0; b[3] = -b[3]; continue; }
    if (b[0] > 1) { b[0] = 1; b[2] = -b[2]; continue; }
    if (b[1] > 1) { b[1] = 1; b[3] = -b[3]; continue; }
  }
}

createObjects();
initObjects();

function swaptwo(){
  s1 = floor(random(numObjects))
  s2 = floor(random(numObjects))
  for (i = 0; i < objectProperties; i++){
    swap = objects[s1][i]
    objects[s1][i] = objects[s2][i]
    objects[s2][i] = swap
  }
}

export function beforeRender(delta) {
  bounce();
  theta = PI2 * time(.1);
  for (var i = 0; i < numObjects; i++) {
    shapetheta = theta + objects[i][6]
    objects[i][7] = sin(shapetheta)
    objects[i][8] = cos(shapetheta)
    objects[i][11] = objects[i][9] * objectSize * (.5+wave(time(objects[i][10])))
  }

  // swap 2 floaters randomly once in a random while, so the same one isn't always on top 
  if (random(1) > .98){
    swaptwo();
  }
  
  if (spin){
    resetTransform();
    translate(-0.5,-0.5);  
    rotate(theta);  
    translate(0.5,0.5);
  }
}


export function render2D(index,x,y) {
  var d,h,s,v;
  for (var i = 0; i < numObjects; i++) {
    shapesize = objects[i][11]
    sx = x-objects[i][0]
    sy = y-objects[i][1]
    if (abs(sx) > shapesize * bounds || abs(sy) > shapesize * bounds ) continue;
    sinshape = objects[i][7]
    cosshape = objects[i][8]
    px = cosshape * sx - sinshape * sy
    py = sinshape * sx + cosshape * sy
    d = shapeSdf[objects[i][5]](px,py,shapesize);
    if (shapeCompare[filled](d)) continue;
    
    if (flat){ 
      v = 1; 
      s = 1;
    } else {
      v = 1-(1.3*d/lineWidth)
      s = 1.5-abs(d)/shapesize*bounds
    }
    h = objects[i][4];      
    break;
  }
  hsv(h, s, v*v)
}

Some notes before I forget:
The Shape slider has a trick to cause a reinit if you slide it. Save the old value before you set the new, and if it’s different after, then the slider was moved, so reinitialize the shapes.

Lots of sliders that are really switches, but no UI yet so it’s a bit messy.
Only 5 shape types so far, more would be nice, maxing the slider makes a random mix instead of one type.
The cutoff adjusts how far the shape looks out for points to include, so if you adjust it, you can make more lumpy shapes (blobs)
More floaters, and less cutoff, is slower but hopefully not too slow
Linewidth is adjustable for some thicker lines.
Flat is more “basic” h,1,1 coloring, while non-flat is more edgy/fill looking. The colors tend to group (adding .4 so it’ll roll every 5 shapes) which I liked the look of, and the color scheme tends to be a nice contrasty mix that way, even though it’s picking a random starting color.

I could add more and more to this… it’s got so much we can do.

1 Like

Nice, @scruffynerf! You caught the good optimizations – the early out when pixels are completely outside an object’s bounding box is excellent! I like the 80s TV look too!

Here’s the final “good” Hexstar function, along with the relatively fast signum function I finally managed to find - swap it in if you like.

One of the oddities of the ported shader functions, including the n-gon one, is that they don’t produce shapes of even remotely equal size. ( I don’t think anybody in the shaderverse ever tested quite this way. :slight_smile: )

Sometimes the size parameter means the distance to a flat side, sometimes to a point. I’ve tried to standardize a little as I go. This version produces a hex star the same size as everything else…

function signum(a) {
  return (a > 0) - (a < 0)
}

function hexStar(x,y,r) {
  // rescale to pointy parts of star
  x = abs(x*1.73205); y = abs(y*1.73205); 
  dot = 2 * min(-0.5*x + 0.866025 * y,0);
  x -= dot * -0.5; y -= dot * 0.866025;
  
  dot = 2 * min(0.866025*x + -0.5 * y,0);
  x -= dot * 0.866025; y -= dot * -0.5;
  
  x -= clamp(x, r * 0.57735, r * 1.73205);
  y -= r;
  return signum(y) * hypot(x,y) / 1.73205;
}
2 Likes

And here’s a port of iq’s n-sided regular polygon signed distance function. Original GLSL source is below. Have fun drawing! I can sense a Github page collecting these things in the near future…

function signum(a) {
  return (a > 0) - (a < 0)
}

// draw regular polygon of height 'r' with 'sides' sides.
function nSidedPolygon(x,y,r,sides) {
  var x1,y1,bn,he;  
  var an = PI2/sides;
  
// size algorithm needs a little adjustment for triangles.  I 
// commented this out, since there's already a faster stand-alone
// triangle function.  Just uncomment if you want it!  
//  r -= (sides == 3) * r / 3;
  he = r * tan(0.5*an);

// swap x and y coordinates and flip signs as needed 
// (if you want to reverse the y orientation, use 'x = -y')  
  tmp = -x; x = y; y = tmp;
  
// calculate angle needed to rotate to first sector  
  var bn = an * floor((atan2(y,x) + 0.5*an)/an);
  
// when porting GLSL matrix operations remember that
// matrices are initialized in column major order.  This
// has given me more than one headache since directX
// does it the other way...
  c = cos(bn); s = sin(bn);
  x1 = (c * x) + (s * y);
  y1 = (c * y) - (s * x);
  
  return hypot(x1 - r, y1 - clamp(y1,-he,he)) * signum(x1 - r)
}

// iq’s GLSL code, from Shader - Shadertoy BETA

#define N 7

// signed distance to a regular n-gon
float sdNGon( in vec2 p, in float r )
{
    // these 2 lines can be precomputed
    float an = 6.2831853/float(N);
    float he = r*tan(0.5*an);
    
    // rotate to first sector
    p = -p.yx; // if you want the corner to be up
    float bn = an*floor((atan(p.y,p.x)+0.5*an)/an);    
    vec2  cs = vec2(cos(bn),sin(bn));
    p = mat2(cs.x,-cs.y,cs.y,cs.x)*p;

    // side of polygon
    return length(p-vec2(r,clamp(p.y,-he,he)))*sign(p.x-r);
}

1 Like

The version of n-poly that also does stars is pretty clean too. Easy enough to make that one do double duty (ie star or normal poly)

I admit now I want all sorts of other shapes.
Hearts, Spades, Clubs, Diamonds, etc etc.

(Not that I expect you to do all the porting, of course… )

Tomorrow, I’ll start a github repository that has a markdown page where we can keep these things and I’ll port more of 'em as I can. Work is unfortunately interfering with my recreational programming this week, as it sometimes does.

1 Like

Sounds good. If we port all of the IQ examples, and the ones in PixelSpirit, that’s the majority of SDFs I’ve found. Absolutely, the building blocks of making whatever shapes we want, scalable, and usable both in render() as live math on the fly and usable for buffered creation too.

I’m imagining a random image creator similar to the way the Deck builds up, but making random combos… Lots of options like symmetry, rings, and more. But let’s get a good SDF collection first.

Added:

Imagine an led display like this:

Link to SDF reference below:

It’s dead simple right now, but it’s a place to start – Send me code here or via pr on github, and we can grow it pretty quickly. I’ll make a separate thread for it when we’re a bit farther along.

2D SDF Reference
Toolkit Main Page

1 Like

Yeah, we’re way off topic now

In additional to IQ’s own stuff on SDF combinations.

^ Good discussion that shows why this is good for lowres LEDs.

Some good visualized bits:

More good code:
http://mercury.sexy/hg_sdf/

And

Also related, and curious where PB fits in this…

As Patricio of BookOfShaders/PixelSpiritDeck/Lygia is taking old monitors, adding a Pi, and making pseudo LED displays

It’s all converging.

Audio +shaders:

And a video Playlist of their progress:

And a p5.js animated port of the PSDeck, again, totally imagining a led version

And another audio reactive one potentially

Adapted to more interactive/reactive items here: