Assignment 2: Loops

For this assignment, we were asked to use Loops to create a simple work of art. A few years ago, I came across one video of Lex Fridman on my YouTube recommendation on “Donut-shaped C code that generates a 3D spinning donut” . It is a video featuring Andy Sloane’s work on the 3D Spinning donut. Since then, this video has been on my watch later playlist. The blog post from Andy Sloane himself on this work can be found here. I always wanted to recreate this. On my first reading of the blog post, I understood the math easily, but back then, I did not have the courage to take on the coding portion.

After going through the listed examples for this assignment, it was clear to me that Andy’s work is also a work of art. So, I quickly decided to recreate that work in p5.js. After looking into the source code, I quickly realized the fearsome code is nothing but a combination of lots of loops.

In this artwork, the 3D effects are created using the mathematical process called z-buffering. This involves creating a 2D array called a z-buffer, which stores the distance to the nearest object for each pixel on the screen. When the code renders an object, it calculates the distance to the object for each pixel and compares it to the value stored in the z-buffer for that pixel. If the object is closer than the object currently stored in the z-buffer, the code updates the z-buffer with the new distance and draws the object on the screen. This process is repeated for each object in the scene, and the result is a 2D image that appears to be 3D. The detailed mathematical explanation can be found on Sloane’s blog here.

I could not directly use the source code provided by Andy Sloane. His art work was based on C code, which did not have functions like draw() in p5.js. So, although I took the mathematical grounding from his shared source code, I had to rewrite the algorithm to make this work in p5.js.

// Function to draw the rotating donut
function canvasframe() {
  // Increment angles for rotation effect
  A += 0.07;
  B += 0.03;

  // Calculating the cosines and sines of A, B
  let cA = cos(A), sA = sin(A),
      cB = cos(B), sB = sin(B);
  
  // parameters to scale and move the donut
  let scale = 400; // variable to scale up or down the donut
  let xOffset = width / 2;
  let yOffset = height / 2;
  let K2 = 10; // perspective adjust
  
  // Draw the donut
  for (let j = 0; j < TWO_PI; j += 0.3) {
    let ct = cos(j), st = sin(j);
    for (let i = 0; i < TWO_PI; i += 0.1) {
      let sp = sin(i), cp = cos(i);
      let ox = 2 + 1 * ct, 
          oy = 1 * st;

      // transform the 3D points to 2D using rotation and perspective
      let x = ox * (cB * cp + sA * sB * sp) - oy * cA * sB;
      let y = ox * (sB * cp - sA * cB * sp) + oy * cA * cB;
      let ooz = 1 / (K2 + cA * ox * sp + sA * oy); // One over Z (Depth)
      let xp = xOffset + scale * ooz * x; // translate and scale to fit the canvas
      let yp = yOffset - scale * ooz * y; // translate and scale to fit the canvas
      let L = 0.7 * (cp * ct * sB - cA * ct * sp - sA * st + cB * (cA * st - ct * sA * sp)); // calculate brightness
      if (L > 0) {
        fill(255 * L); // Set the fill color based on the brightness
        rect(xp, yp, 2, 2); //draw the point with a slightly larger size for better visibility
      }
    }
  }
}

I successfully recreated the donut’s 3D effect. However, I could not finish up the ASCII donut. So, in future I would love to finish up the work and replicate the entire art work.

Leave a Reply