New Pattern: Geometry Morphing Demo 2D

Just uploaded “Geometry Morphing Demo 2D” to the library (source code below, too) - the latest version of the distance field drawing pattern started in the “Circles” thread.

It shows a quick, easy way to draw certain kinds of geometry, and subject it to all kinds of strange and interesting manipulations.

This particular version is a basic demo just to give the flavor: It draws a single geometric shape, rotated using Pixelblaze’s new map transform functions, and smoothly transforms it into to the next type of shape on its list. From here, many things are possible!

Geometry Morphing Demo 2D
/* Geometry Morphing Demo 2D

 Smooth transitions between animated geometric shapes.  
 
 This pattern shows how to draw and dynamically modify geometric
 objects using a pixel shader and signed distance functions.  Since this 
 method scales automatically with the number of available processors, it 
 is most often used on GPU-based systems.  It's also very well suited to
 the Pixelblaze's architecture.
 
 For more information, see:
   Basic tutorial on this style of rendering:
     https://www.shadertoy.com/view/Xl2XWt
   Distance functions for many 2D shapes:
     https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm

 MIT License
 Take this code and use it to make cool things!
 
 Version  Author        Date      
 1.0.0    ZRanger1 08/09/2021
*/ 

// UI control variables
export var objectSize = 0.4;
export var lineWidth = 0.05;
var filled = 1;

// shape function selection 
var numShapes = 6;
var shapeSdf = array(numShapes)
var shapeCompare = array(2);

// animation control
var shape = 0;
var nextShape = 1;
var hue = 0;
var morphClock = 0;
var wait = 0; 
var lerpPct = 0;

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

shapeSdf[0] = circle;
shapeSdf[1] = cross;
shapeSdf[2] = hexStar;
shapeSdf[3] = square;
shapeSdf[4] = triangle;
shapeSdf[5] = hexagon;

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

// 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) {
  // 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;
}

// interior distance on this is slightly weird. Still
// looking for a reasonable fix.
function cross(x,y,size) {
  x = abs(x); y = abs(y);
  
  if (y > x) { tmp = x; x = y; y = tmp; }
  qx = x - size; qy = y - size / 5;
  k = max(qy,qx);
  if (k > 0) {
    wx = max(qx,0); wy = max(qy,0);
    return hypot(wx,wy);
  } else {
    wx = max(size - x,0); wy = max(-k,0);
    return -hypot(wx,wy);
  }
}

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

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

export function sliderLineWidth(v){
  lineWidth = 0.25 * v * v;
}

export function beforeRender(delta) {
  morphClock += delta

// morph to a new shape every other second...
  if (morphClock > 1000) {
    if (!wait) {
      shape = nextShape;                      // set to next shape
      nextShape = (nextShape+1) % numShapes;  
    }
    morphClock = 0;    
    wait = !wait;
  }

  lerpPct = morphClock / 1000;

// rotate entire scene
  theta = PI2 * time(0.1);
  resetTransform();
  translate(-0.5,-0.5);  
  rotate(theta);  
}

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

  // draw one our shapes, interpolating between two SDFs when switching shapes
  if (wait) {
    d = shapeSdf[shape](x,y,objectSize);
  } else {
    d = shapeSdf[shape](x,y,objectSize) * (1-lerpPct) + shapeSdf[nextShape](x,y,objectSize) * lerpPct;
  }
  
  // fill or just draw boundary based on UI seting.  
  if (!shapeCompare[filled](d)) {;
    v = 1-(d/lineWidth);
    s = 1.5-abs(d)/objectSize
    h = d + time(0.1);      
  }

  hsv(h, s, v*v)
}
1 Like

Roiling in my brain: Rotation and N-sided polygoning (so a generic function rather than square and hex, to do pentagon, etc). Square back to rectangular.

1 Like

That’d be cool! I started out in that direction, and found that anything more than 6 sides looks awfully circular on my low-res matrix, so I bailed and just chose a few shapes to start with. It’d be good to be able to change the number of sides at will though.

Rotation is pretty easy from a programming standpoint, and is a great example of the tradeoffs between drawing methods. To rotate an individual animated SDF object, for each pixel in the scene you have to:

  • translate the whole coordinate space so the object’s center is at the origin
  • rotate the point around the z axis
  • evaluate the distance function at the rotated point
  • transform back to the original coordinate space so you can check the next object in the list

If you precompute sin() and cos(), this is all pretty fast, but still, it’s a lot of pixels. It’ll bog down with more than two or three independently rotating objects.

If you wanted to rotate a lot of independent objects, the traditional vertex list/scan conversion method is the way to go because it limits the number of points you have to transform.

Nah, you’ve over complicated it.

As I pulled out in Rotation functions?, you just need two lines.

If you modify the function to be
f(x,y,size,theta aka angle of rotation),
You still have to compute the sin/cos of the same angle repeatedly. So cache those two sin(theta) and cos(theta) in the shape array?

Make a SDFrotate(shapearray pointer, theta)
And add precomputes there.

Added: turns out to be even easier, see code below. No changes to SDF functions needed. I added a theta to the shape array details, so each is unique…

(Or elsewhere). Or maybe do just compute it if that’s faster than pulling from an array or whatever you else use to store it. Whatever’s the least work.

You don’t need to recalc it back.

You are looking thru all points and deciding for each point, “are you a member of shape N”?

Feeding in those X,Y coords to each shape function… You are passing px,py,size…ox,oy are the shapes center which you have. Either store theta of the shape, or sin+cos, and you have it all.
Now you have to just add the point rotation lines above to your SDF which gives you new X,Ys relative to center, and then you run the rest of your SDF. And normal there after.
No need to reverse at all. You’ve not changed a thing permanently.

done: Rotation functions? - #4 by Scruffynerf

1 Like

Rotation code linked above.

That’s IQ’s generic version, and simpler than others out there.

This is why I prefer doing everything in complex coordinates:
Rotation is just multiplication by cos(θ) + i·sin(θ)

1 Like