Search code examples
javaprocessingnested-loops

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?


Solution

  • 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.