Skip to content
Advertisement

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

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?

Advertisement

Answer

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.

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement