How to roll up a 2D Grid in P3D with Processing

Viewed 76

I have built a two-dimensional grid of rectangles with a nested loop. Now I want to "roll up" this grid in three-dimensional space, or in other words "form a cylinder" or a "column". With the movement of my mouse pointer. Up to the "roll up" I get everything programmed as desired - but then my mathematics fails.

float size;
float pixel = 75;

void setup() {
  size(1920, 1080, P3D);
  frameRate(30);
  size = width/pixel;
  rectMode(CENTER);
  noStroke();
}

    void draw() {
      background(0);
      rotateX(radians(45));
      translate(pixel*size/2, -pixel*size, -pixel*size);
      translate(-pixel*size/2, -pixel*size/2, -pixel*size/4);
      pushMatrix();
      for (int x = 0; x < pixel; x++) {
        for (int y = 0; y < pixel; y++) {
          pushMatrix();
          float sin = sin(radians(x * 10)) * mouseX;
          float cos = cos(radians(x * 10)) * mouseX;
          translate(x*size, y*size);
          rotate(radians(45));
          fill(255);
          rect(sin, cos, size/5, size/5);
          popMatrix();
        }
      }
      popMatrix();
    }

Instead of a roll up, the grid twists twice.... I thought I could achieve the "roll up" by concatenating sin(); and cos(); - similar to this example:

float sin, cos;
void setup() {
  size(900, 900);
  background(0);
}
void draw() {
  translate(width/2, height/2);
  for (int i = 0; i < 200; i++) {
    sin = sin(radians(frameCount + i *10)) * 400;
    cos = cos(radians(frameCount + i *10)) * 400;
    ellipse(sin, cos, 10, 10);
  }
}

What is the best way to achieve this roll up?

1 Answers

You are on the right track using the polar to cartesian coordinate system transformation formula.

There are multiple ways to solve this. Here's an idea, starting in 2D first: unrolling a circle to a line. I don't know the 100% mathematically correct way of doing this and I hope someone else posts this. I can however post a hopepfully convincing enough estimation using these "ingredients":

  • The length of the circle (circumference) is 2πR
  • Processing's lerp() linearly interpolates between two values (first two arguments of the function) by a percentage (expressed a value between 0.0 and 1.0 (called a normalized value) -> 0 = 0% = start value, 0.5 = 50% = half-way between stard and end value, 1.0 = 100% = at end value)
  • Processing provides a PVector class which is both handy to encapsulate 2D/3D point properties (.x, .y, .z), but also provides a lerp() method which is a nice shorthand to avoid manually lerping 3 times (once for each dimension (x, y, z))

Here's a basic commented sketch to illustrate the above:

// total number of points
int numPoints = 24;
// circle radius
float radius = 50;
// circle length (circumference) = 2πR
float circleLength = TWO_PI * radius;
// spacing between each point for the length of the circle
float lengthIncrement = circleLength / numPoints;
// how many radians should each point on a circle increment by
float angleIncrement = TWO_PI / numPoints;

// cache for points on a the circle
PVector[] pointsCircle = new PVector[numPoints];
// cache for points on a line the lenght of the circle
PVector[] pointsLine   = new PVector[numPoints];

void setup(){
  size(300, 300);
  noStroke();
  // cache: pre-compute start(circle) and end(line) points
  for(int i = 0 ; i < numPoints; i++){
    // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom
    float angle = (angleIncrement * i) + HALF_PI;
    pointsCircle[i] = new PVector(cos(angle) * radius, sin(angle) * radius);
    // compute positions on a line, offsetting by half: avoids most self-intersections when animating
    pointsLine[i] = new PVector(lengthIncrement * i - (circleLength * 0.5), 0);
  }
}

void draw(){
  background(0);
  translate(width * 0.5, height * 0.5);
  // map interpolation amount to mouse X position
  float interpolationAmount = (float)mouseX / width;
  // for each point
  for(int i = 0 ; i < numPoints; i++){
    // compute the interpolated position
    PVector pointAnimated = PVector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount);
    // optional: visualise the first point as the darkest and last point as the brightest
    fill(map(i, 0, numPoints -1, 64, 255));
    // render the point as a circle
    circle(pointAnimated.x, pointAnimated.y, 9);
  }
}

24 points on a circle that linearly interpolate to a line using the mouse X position

The same logic can be applied in 3D with an extra loop to repeat circles/lines to appear as a cylinder/grid:

// total number of points
int numPointsX = 24;
// circle radius
float radius = 50;
// circle length (circumference) = 2πR
float circleLength = TWO_PI * radius;
// spacing between each point for the length of the circle
float lengthIncrement = circleLength / numPointsX;
// how many radians should each point on a circle increment by
float angleIncrement = TWO_PI / numPointsX;

// cache for points on a the circle
PVector[] pointsCircle = new PVector[numPointsX];
// cache for points on a line the lenght of the circle
PVector[] pointsLine   = new PVector[numPointsX];
// number of points on Z axis 
int numPointsZ = 24;

void setup(){
  size(300, 300, P3D);
  // render circles as thick points
  noFill();
  strokeWeight(9);
  // cache: pre-compute start(circle) and end(line) points
  for(int i = 0 ; i < numPointsX; i++){
    // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom
    float angle = (angleIncrement * i) + HALF_PI;
    pointsCircle[i] = new PVector(cos(angle) * radius, sin(angle) * radius);
    // compute positions on a line, offsetting by half: avoids most self-intersections when animating
    pointsLine[i] = new PVector(lengthIncrement * i - (circleLength * 0.5), 0);
  }
}

void draw(){
  background(0);
  
  translate(width * 0.5, height * 0.5, 0);
  rotateY(map(mouseX, 0, width, -PI, PI));
  rotateX(map(mouseY, 0, height, PI, -PI));
  
  float interpolationAmount = (float)mouseX / width;
  // render the grid (circular or rectangular)
  beginShape(POINTS);
  for(int j = 0 ; j < numPointsZ; j++){
    // offset by half the size to pivot from center
    float z = (circleLength * 0.5) - (lengthIncrement * j);
    
    for(int i = 0 ; i < numPointsX; i++){
      // compute the interpolated position
      PVector pointAnimated = PVector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount);
      // render point
      stroke(map(i, 0, numPointsX -1, 64, 255));
      vertex(pointAnimated.x, pointAnimated.y, z);
    }
  }
  endShape();
}

multiple circles stacked in 3D appear a cylinder that unrolls to a plane when the mouse X position moves left to right

The above code would've worked without PVector, but it would be more verbose. The other thing to keep in mind is that a the static PVector.lerp() method will generate a new PVector instance per call: this is ok for small demo such as this, but caching a bunch of PVectors to lerp() should waste less memory.

For the sake of completeness here are interactive versions your can run right here via p5.js:

// total number of points
let numPoints = 24;
// circle radius
let radius = 50;
// circle length (circumference) = 2πR
let circleLength;
// spacing between each point for the length of the circle
let lengthIncrement;
// how many radians should each point on a circle increment by
let angleIncrement;

// cache for points on a the circle
let pointsCircle = new Array(numPoints);
// cache for points on a line the lenght of the circle
let pointsLine   = new Array(numPoints);

function setup(){
  createCanvas(300, 300);
  noStroke();
  // ensure TWO_PI is defined before assignment
  circleLength = TWO_PI * radius;
  lengthIncrement = circleLength / numPoints;
  angleIncrement = TWO_PI / numPoints;
  // cache: pre-compute start(circle) and end(line) points
  for(let i = 0 ; i < numPoints; i++){
    // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom
    let angle = (angleIncrement * i) + HALF_PI;
    pointsCircle[i] = createVector(cos(angle) * radius, sin(angle) * radius);
    // compute positions on a line, offsetting by half: avoids most self-intersections when animating
    pointsLine[i] = createVector(lengthIncrement * i - (circleLength * 0.5), 0);
  }
}

function draw(){
  background(0);
  translate(width * 0.5, height * 0.5);
  // map interpolation amount to mouse X position
  let interpolationAmount = constrain(mouseX, 0, width) / width;
  // for each point
  for(let i = 0 ; i < numPoints; i++){
    // compute the interpolated position
    let pointAnimated = p5.Vector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount);
    // optional: visualise the first point as the darkest and last point as the brightest
    fill(map(i, 0, numPoints -1, 64, 255));
    // render the point as a circle
    circle(pointAnimated.x, pointAnimated.y, 9);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

// total number of points
let numPointsX = 24;
// circle radius
let radius = 50;
// circle length (circumference) = 2πR
let circleLength;
// spacing between each point for the length of the circle
let lengthIncrement;
// how many radians should each point on a circle increment by
let angleIncrement;

// cache for points on a the circle
let pointsCircle = new Array(numPointsX);
// cache for points on a line the lenght of the circle
let pointsLine   = new Array(numPointsX);
// number of points on Z axis 
let numPointsZ = 24;

function setup(){
  createCanvas(600, 600, WEBGL);
  // ensure TWO_PI is defined before assignment
  circleLength = TWO_PI * radius;
  lengthIncrement = circleLength / numPointsX;
  angleIncrement = TWO_PI / numPointsX;
  // render circles as thick points
  noFill();
  strokeWeight(9);
  stroke(255);
  // cache: pre-compute start(circle) and end(line) points
  for(let i = 0 ; i < numPointsX; i++){
    // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom
    let angle = (angleIncrement * i) + HALF_PI;
    pointsCircle[i] = createVector(cos(angle) * radius, sin(angle) * radius);
    // compute positions on a line, offsetting by half: avoids most self-intersections when animating
    pointsLine[i] = createVector(lengthIncrement * i - (circleLength * 0.5), 0);
  }
}

function draw(){
  background(0);
  orbitControl();
  rotateX(HALF_PI);
  let interpolationAmount = constrain(mouseX, 0, width) / width;
  // render the grid (circular or rectangular)
  beginShape(POINTS);
  for(let j = 0 ; j < numPointsZ; j++){
    // offset by half the size to pivot from center
    let z = (circleLength * 0.5) - (lengthIncrement * j);
    
    for(let i = 0 ; i < numPointsX; i++){
      // compute the interpolated position
      let pointAnimated = p5.Vector.lerp(pointsCircle[i], pointsLine[i], interpolationAmount);
      // render point
      vertex(pointAnimated.x, pointAnimated.y, z);
    }
  }
  endShape();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

Update

As previously mentioned, you can work without PVector in this simple case:

// total number of points
int numPointsX = 24;
// circle radius
float radius = 50;
// circle length (circumference) = 2πR
float circleLength = TWO_PI * radius;
// spacing between each point for the length of the circle
float lengthIncrement = circleLength / numPointsX;
// how many radians should each point on a circle increment by
float angleIncrement = TWO_PI / numPointsX;

// cache for points on a the circle
float[][] pointsCircle = new float[numPointsX][2];
// cache for points on a line the lenght of the circle
float[][] pointsLine   = new float[numPointsX][2];
// number of points on Z axis 
int numPointsZ = 24;

void setup(){
  size(300, 300, P3D);
  // render circles as thick points
  noFill();
  strokeWeight(9);
  // cache: pre-compute start(circle) and end(line) points
  for(int i = 0 ; i < numPointsX; i++){
    // compute the angle using the increment but also offset by 90 degrees so 1st point is at bottom
    float angle = (angleIncrement * i) + HALF_PI;
    pointsCircle[i] = new float[]{cos(angle) * radius, sin(angle) * radius};
    // compute positions on a line, offsetting by half: avoids most self-intersections when animating
    pointsLine[i] = new float[]{lengthIncrement * i - (circleLength * 0.5), 0};
  }
}

void draw(){
  background(0);
  
  translate(width * 0.5, height * 0.5, 0);
  rotateY(map(mouseX, 0, width, -PI, PI));
  rotateX(map(mouseY, 0, height, PI, -PI));
  
  float interpolationAmount = (float)mouseX / width;
  // render the grid (circular or rectangular)
  beginShape(POINTS);
  for(int j = 0 ; j < numPointsZ; j++){
    // offset by half the size to pivot from center
    float z = (circleLength * 0.5) - (lengthIncrement * j);
    
    for(int i = 0 ; i < numPointsX; i++){
      // compute the interpolated position
      float pointAnimatedX = lerp(pointsCircle[i][0], pointsLine[i][0], interpolationAmount);
      float pointAnimatedY = lerp(pointsCircle[i][1], pointsLine[i][1], interpolationAmount);
      // render point
      stroke(map(i, 0, numPointsX -1, 64, 255));
      vertex(pointAnimatedX, pointAnimatedY, z);
    }
  }
  endShape();
}

Personally, I find the PVector version slightly more readable.

Related