Looking Outwards 05 – 3D Computer Graphics

One of my favorite musical artists, Carpenter Brut, released a music video about 4 years ago for his song “Turbo Killer”. The basic premise of the video was a world where machines were living (and hence called “blood machines”) and due to their power were highly coveted and fought over. The video was directed and written by Seth Ickerman, a Paris-based CGI artist whose goal was to capture the over-the-top 80s feel of Carpenter Brut’s heavy synth music. 

The video features a myriad of special effects that synergize to create this 80s atmosphere, of which 3D graphics plays a major role. Most of the video was comprised of CGI in some form, with only close-up shots of real actors shot live and superimposed onto a CGI set. Some of the CGI models used in the video are detailed to an extreme amount, leading me to believe they were not all modeled manually by hand. Generative design in certain scenes and models seem to help Ickerman model faster as well as depicting a more organic feel to these machines.

Project 05 – Wallpaper

Too much Among Us was played in the making of this wallpaper.

Cyan sus.

sketch
/*
 *Eric Zhao
 *ezhao2@andrew.cmu.edu
 *
 *Draws a wallpaper consisting of a "3D lattice"
 *and Among Us sprites.
 */

var w = 50; //pillar length and height
var s = 10; //pillar width
var pillarHue = 101;
var pillarLight = 75;
var pillarDark = 25;

function setup() {
    colorMode(HSB);
    createCanvas(600, 600);
    background(16, 34, 93);

}

function draw() {
    //draws the lattice of pillars
    for(let i = 0; i < 12; i++){
        for(let j = 0; j < 12; j++){
            if((i+j) % 2 == 0){
                pillar2(i*w, j*w, s, w, pillarHue, pillarLight, pillarDark);
            } else{
                pillar(i*w, j*w, s, w, pillarHue, pillarLight, pillarDark);
            }
        }
    }
    //draws the Among Us sprites on rows with odd numbers of spaces
    for(let i = 0; i < height; i += 2*w){
        push();
        translate(0, i);
        for(let j = 0; j < width-w; j += 2*w){
            push();
            translate(j + w/2, w/2);
            scale(0.45);
            amongUs();
            pop();
        }
        pop();
    }
    translate(w, w);
    //draws the Among Us sprites on rows with even numbers of spaces
    for(let i = 0; i < height-2*w; i += 2*w){
        push();
        translate(0, i);
        for(let j = 0; j < width-2*w; j += 2*w){
            push();
            translate(j + w/2, w/2);
            scale(0.45);
            amongUs();
            pop();
        }
        pop();
    }
    //shh, here's the impostor!
    translate(width/3+15, height/3-2);
    scale(0.2);
    knife();
    noLoop();
}

function pillar(x, y, s, w, hue, light, shadow){
    //pillar function drawing a lattice element top left to bottom right
    fill(hue, 40, shadow);
    quad(x, y, x+s, y, x+w, y+w-s, w+x, w+y);
    fill(hue, 40, light);
    quad(x, y, w+x, w+y, w-s+x, w+y, x, s+y);

}

function pillar2(x, y, s, w, hue, light, shadow){
    //pillar function drawing a lattice element bot left to top right
    fill(hue, 40, shadow);
    quad(x, y+w, x+w, y, x+w, y+s, x+s, y+w);
    fill(hue, 40, light);
    quad(x, y+w, x, y+w-s, x+w-s, y, x+w, y);
}

function amongUs(){
    //creates an Among Us sprite lookalike
    push();
    noStroke();
    fill(50);
    ellipse(53, 94, 70, 15); //shadow
    backpack();
    body();
    visor();
    pop();
}
function body(){
    //Among Us sprite body
    //body base color
    noStroke();
    fill(187, 100, 76);
    beginShape();
    curveVertex(59, 76);
    curveVertex(59, 76);
    curveVertex(62, 88);
    curveVertex(75, 88);
    curveVertex(78, 74);
    curveVertex(81, 55);
    curveVertex(79, 38);
    curveVertex(69, 7);
    curveVertex(38, 7);
    curveVertex(28, 48);
    curveVertex(32, 91);
    curveVertex(50, 91);
    curveVertex(52, 76);
    curveVertex(59, 76);
    curveVertex(59, 76);
    endShape();

    //body highlight color
    fill(172, 56, 80);
    beginShape();
    curveVertex(45, 62);
    curveVertex(45, 62);
    curveVertex(70, 63);
    curveVertex(79, 38);
    curveVertex(69, 7);
    curveVertex(45, 4);
    curveVertex(36, 32);
    curveVertex(39, 54);
    curveVertex(45, 62);
    curveVertex(45, 62);
    endShape();

    //outline
    stroke(0);
    strokeWeight(6);
    noFill();
    beginShape();
    curveVertex(59, 76);
    curveVertex(59, 76);
    curveVertex(62, 88);
    curveVertex(75, 88);
    curveVertex(78, 74);
    curveVertex(81, 55);
    curveVertex(79, 38);
    curveVertex(69, 7);
    curveVertex(38, 7);
    curveVertex(28, 48);
    curveVertex(32, 91);
    curveVertex(50, 91);
    curveVertex(52, 76);
    curveVertex(59, 76);
    curveVertex(59, 76);
    endShape();
}

function backpack(){
    //Among Us backpack (body colored)
    noStroke();
    //backpack base color
    fill(187, 100, 76);
    beginShape();
    curveVertex(33, 27);
    curveVertex(33, 27);
    curveVertex(19, 31);
    curveVertex(17, 69);
    curveVertex(30, 72);
    curveVertex(30, 72);
    endShape();

    //backpack highlight
    fill(172, 56, 80);
    beginShape();
    curveVertex(33, 27);
    curveVertex(33, 27);
    curveVertex(19, 28);
    curveVertex(18, 39);
    curveVertex(33, 36);
    curveVertex(33, 36);
    endShape();

    strokeWeight(6);
    noFill();

    //outline
    stroke(0);
    strokeWeight(6);
    noFill();
    beginShape();
    curveVertex(33, 27);
    curveVertex(33, 27);
    curveVertex(19, 31);
    curveVertex(17, 69);
    curveVertex(30, 72);
    curveVertex(30, 72);
    endShape();
}
function visor(){
    //Among Us visor section
    strokeWeight(6);
    noStroke();
    fill(193, 38, 43);
    ellipse(64, 28, 35, 25);
    fill(196, 36, 87);
    ellipse(67, 24, 25, 18);
    fill(0, 0, 100);
    ellipse(68, 24, 15, 5);
    //outline
    stroke(0);
    noFill();
    ellipse(64, 28, 35, 25);
}

function knife() {
    //for the killer only...
    strokeWeight(2);
    fill(0, 0, 37);
    rect(0, 0, 15, 30);
    fill(11, 67, 51);
    rect(-10, 30, 35, 15);
    fill(0, 0, 50);
    triangle(-5, 45, 7.5, 100, 20, 45);
    noStroke();
    fill(0, 0, 85);
    triangle(7.5, 45, 7.5, 100, -5, 45);
    noFill();
    stroke(0);
    triangle(-5, 45, 7.5, 100, 20, 45);
    noLoop();
}

Project 04 – String Art

sketch
/*
 * Eric Zhao
 * ezhao2@andrew.cmu.edu
 *
 * Project 4: String Art
 * A dynamic string art with four separate string shapes
 * that changes based on mouse position.
 */

var angle = 0;
var posX = 0;
var posY = 0;
var cursorX;
var cursorY;
var loopCount = 0;
var numLines = 30;
function setup() {
    createCanvas(400, 400);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);
}

function draw(){
    stroke(255);
    strokeWeight(0.125);
    background(0);
    translate(width/2, height/2);

    push()
    cursorX = map(min(mouseX, width), -200, 200, 0, 400);
    for(let a = 0; a <= 360; a += 1){
        //rotates the canvas an increment of radians and adjusts canvas
        rotate(5);
        push();
        scale(4);
        posY = -a;
        line(-cursorX, posY, cursorX, cursorX);
        //draws lines normal to point offset after rotation
        pop();
    }
    pop();

    stroke(0, 255, 255);
    strokeWeight(0.75);


    //bottom right cyan string corner
    line(0, height/2, width/2, height/2);
    line(width/2, 0, width/2, height/2);
    numLines = int(map(constrain(mouseX, 0, 400), 0, 400, 0, 30));
    var dx1 = (0-width/2)/numLines;
    var dy1 = (height/2-height/2)/numLines;
    var dx2 = (width/2-width/2)/numLines;
    var dy2 = (height/2-0)/numLines;
    var x1 = 0;
    var y1 = height/2;
    var x2 = width/2;
    var y2 = height/2;
    for (var i = 0; i <= numLines; i += 1) {
        line(x1, y1, x2, y2);
        x1 -= dx1;
        y1 -= dy1;
        x2 -= dx2;
        y2 -= dy2;
    }

    //bottom left string corner
    line(0, height/2, -width/2, height/2);
    line(-width/2, 0, -width/2, height/2);
    dx1 = (0 + width/2)/numLines;
    dy1 = (height/2-height/2)/numLines;
    dx2 = (-width/2 - 0)/numLines;
    dy2 = (height/2-0)/numLines;
    x1 = 0;
    y1 = height/2;
    x2 = -width/2;
    y2 = height/2;
    for (var i = 0; i <= numLines; i += 1) {
        line(x1, y1, x2, y2);
        x1 -= dx1;
        y1 -= dy1;
        x2 -= dx2;
        y2 -= dy2;
    }
    rotate(PI); //rotates canvas to draw top two string corners

    //top left string part
    line(0, height/2, width/2, height/2);
    line(width/2, 0, width/2, height/2);
    numLines = int(map(constrain(mouseX, 0, 400), 0, 400, 0, 30));
    dx1 = (0-width/2)/numLines;
    dy1 = (height/2-height/2)/numLines;
    dx2 = (width/2-width/2)/numLines;
    dy2 = (height/2-0)/numLines;
    x1 = 0;
    y1 = height/2;
    x2 = width/2;
    y2 = height/2;
    for (var i = 0; i <= numLines; i += 1) {
        line(x1, y1, x2, y2);
        x1 -= dx1;
        y1 -= dy1;
        x2 -= dx2;
        y2 -= dy2;
    }
    //top right string part
    line(0, height/2, -width/2, height/2);
    line(-width/2, 0, -width/2, height/2);
    dx1 = (0 + width/2)/numLines;
    dy1 = (height/2-height/2)/numLines;
    dx2 = (-width/2 - 0)/numLines;
    dy2 = (height/2-0)/numLines;
    x1 = 0;
    y1 = height/2;
    x2 = -width/2;
    y2 = height/2;
    for (var i = 0; i <= numLines; i += 1) {
        line(x1, y1, x2, y2);
        x1 -= dx1;
        y1 -= dy1;
        x2 -= dx2;
        y2 -= dy2;
    }
}

I got the initial idea for this composition when trying to make the spiral example from a class lecture and accidentally rotating the canvas in radians instead of degrees.

LO 04 – Sound Art

Sample visualizations that Milkdrop is capable of.

Milkdrop is a visualizing plugin created by Google employee Ryan Geiss for Winamp, a media player for Windows systems. Milkdrop turns input audio such as songs or movies into abstract, constantly-changing psychedelic compositions. Since its release in 2001, Geiss has added extra features into the plugin, such as pixel shaders that allow for more complex and layered visualization presets.

Milkdrop takes the wavelength of an audio file playing in Winamp and analyzes it to create a visualization based on a preset, which is like a template for the program to convert the wavelength into a visual. Milkdrop uses a grid system on screen in which it calculates a pixel’s value based on a preset and interpolates the other pixels based on these values. In other words, it will calculate the values of about 32 x 24 evenly spaced points on the screen and averages the others based on the calculated values.

I was drawn to Milkdrop from how different it looked from other audio visualizers and how unlimited the potential for creating visualizations based in the program could be. I also found it interesting that Geiss’s skillset translated over to his work at Google, where he currently works on AI software for Pixel phones to help it take better photos.

LO 3 – Computational Fabrication

The BAC Mono is a British-built, street legal racecar designed to deliver the most pure driving experience possible. The Mono’s secret to fun lies in its weight: at only 1250 pounds, the car behaves like a scalpel, being able to turn precisely and quickly due to having little inertia.

For its 2020 refresh, BAC had to come up with creative methods to shave mass of this already featherweight machine. Partnering with Autodesk, the engineers at BAC used Fusion 360 to generate a lighter wheel. Compared to the outgoing design, this wheel saved 2.6 pounds. While this may not seem like much on paper, removing unsprung weight from the spinning wheels of a car translates tenfold towards performance. In other words, the new design actually saves about 26 pounds per wheel when the car is in motion!

To me, the majority of generative design is very obvious – the algorithms used to generate these designs have a distinct, hollowed out, weblike aesthetic. Although this is pleasing in some applications, they may not look ideal in others. In the case of the BAC Mono, maintaining the general 5 spoke design of the outgoing wheel was a priority. The end result speaks for itself – a wheel that looks virtually unchanged on the surface but is much improved underneath.

Project 03 – Dynamic Drawing

sketch
/*
 * Eric Zhao
 * ezhao2@andrew.cmu.edu
 *
 * Dynamic Drawing: This program creates an array of squares
 * and circles on the canvas. The circles light up and grow
 * in size the closer the mouse is to them, while the squares
 * rotate based on the mouse's position relative to the center
 * of the canvas. Number of circles and squares per row can be
 * changed and the program is scalable with canvas sizes.
 */


var circleDia = 0;
var mouseDist = 0;
var cursorX = 0; 
var cursorY = 0; 
var distanceMult = 0; //circle diameter multiplier
var circleCount = 15; //circle and square count
var tanRotation = 0; //square rotation state
function setup() {
    createCanvas(400, 400);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);
    colorMode(HSB, 360, 100, 100, 100)
    rectMode(CENTER);
}

function draw() {
    push();
    noStroke();
    circleDia = width/circleCount;
    cursorX = constrain(mouseX, 0, width);
    cursorY = constrain(mouseY, 0, height);
    //constrains mouseX and mouseY to canvas size
    background(0);
    //the following are two for loops that draw a grid of circles with
    //[circleCount] number of circles in each row and column
    for(let y = circleDia/2; y <= 1 + height-circleDia/2; y+= circleDia){
        for(let x = circleDia/2; x <= 1 + width-circleDia/2; x+= circleDia){
            mouseDist = constrain(dist(x, y, cursorX, cursorY), 1, width);
            distanceMult = mouseDist/circleCount;
            //used later to modify circle size based on mouse position
            tanRotation = atan2(cursorY - height/2, cursorX - width/2);
            //stores rotation of mouse before pushing rectangle
            push();
            translate(x, y);
            rotate(tanRotation);
            fill(15,25);
            rect(0, 0, 50, 50);
            pop();
            //draws rotated rectangles in same positions as the circle grid
            if(dist(x, y, width/2, height/2) < height/2){
            //constrains array of circles within a larger circular area
                fill(180+degrees(atan2(cursorY - height/2, cursorX - width/2)),
                65, 512/distanceMult); 
                /*adjusts HSB values based on proximity of mouse to circle and 
                coordinates of mouse relative to center of canvas*/
                circle(x, y, circleDia-distanceMult);
            }    
        }
    }
    pop();
}

This project started with the idea of having an array of circles that grew and shrank based on how close they were to the mouse. After I made that basic program work, I started experimenting with having the mouse position change the colors of the array and using other shapes as a filter on top of the circles.

Project 02 – Variable Portrait

This is a random portrait generator that creates random robot faces! The hardest part of this was writing a loop that could let each mouse click cause the program to cycle through several faces at a time.

sketch
/*
* Eric Zhao
* ezhao2@andrew.cmu.edu
* Section D
*
* Random Face Generator. Randomizes face shape, eye color and size, nose shape,
* and presence of antenna. Operates on click and rolls through several faces 
* per click.
*/

let w = 0; //face width
let h = 0; //face height
let topRadius = 0; //face curvature
let botRadius = 0; //face curvature
let eyeRadius = 0; //eye size
let index = 15; //loop index
let noseCircle = 0; //controls shape of nose
let faceColor = 0; //grayscale face color value
let eyeR = 0; //eye Red value
let eyeG = 0; //eye Green value
let eyeB = 0; //eye Blue value

function setup() {
    createCanvas(600, 600);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);
    frameRate(30);
    rectMode(CENTER);
}

function draw() { 
    /* This draw command runs when the mouse is clicked, generating 15 portraits and stopping
    on the last one. Given that each face needs random values, this function sets the variables 
    instead of the mousePressed function. */
    //setting variables randomly
    w = random(width/4, width/2); //next 4 lines generate random rectangle dimensions
    h = random(w/2, w*1.5);
    topRadius = random(0, width/8); 
    botRadius = random(width/20, width/3);
    eyeRadius = random(w/8, h/4);
    noseCircle = int(random(int(0), int(2)));
    faceColor = random (100, 200);
    eyeR = random(30, 256);
    eyeG = random(30, 256);
    eyeB = random(30, 256);
    //setting fills and strokes
    background(185, 240, 185);
    noStroke();
    fill(faceColor);
    rect(width/2, height/2, w, h, topRadius, topRadius, botRadius, botRadius); //draws rectangle
    fill(eyeR, eyeG, eyeB)
    ellipse((width/2) - (w/5), (height/2) - (h/5), eyeRadius, eyeRadius); //left eye
    ellipse((width/2) + (w/5), (height/2) - (h/5), eyeRadius, eyeRadius); //right eye
    noFill();
    strokeWeight(5);
    stroke(220);
    if((eyeR + eyeG + eyeB) / 3 > 128) { //if the eye color is above a certain brightness, darken stroke
        stroke(50)
    }
    arc((width/2) - (w/5), (height/2) - (h/5), eyeRadius * 0.8, eyeRadius * 0.8,
        PI + QUARTER_PI, QUARTER_PI); //left reflection
    arc((width/2) + (w/5), (height/2) - (h/5), eyeRadius * 0.8, eyeRadius * 0.8,
        PI + QUARTER_PI, QUARTER_PI); //right reflection
    stroke(220);
    if (noseCircle === 1) { //this loop controls for nose shape and antenna
        //draws a round nose and face profile 
        arc((width/2), (height/2) + (eyeRadius * 0.8) / 2, eyeRadius * 0.8, eyeRadius * 0.8, 
        HALF_PI, PI + HALF_PI);
        line (width/2, (height/2) - (h/2), width/2, height/2);
        line (width/2, (height/2) + (eyeRadius * 0.8), width/2, (height/2 + h/2));
        stroke(faceColor); 
        line (width/2, (height/2) - (h/2), (width/2) + (w/4), (height/2) - (h/1.5)); //draws antenna stem
        noStroke();
        fill (255, 66, 66);
        ellipse((width/2) + (w/4), (height/2) - (h/1.5), eyeRadius * 0.5, eyeRadius * 0.5); //draws antenna circle
    } else {
        //draws a triangle nose / face profile
        line (width/2, (height/2) - (h/2), width/2, height/2 - eyeRadius);
        line (width/2, height/2 - eyeRadius, (width/2) - (w/8), (height/2) + (h/4));
        line ((width/2) - (w/8), (height/2) + (h/4), width/2, (height/2) + (h/4));
        line (width/2, (height/2) + (h/4), width/2, (height/2 + h/2));        
    }

    print(index.toString()); //prints loop index to console
    index++;

    if (index > 9) { //repeats the draw command 15 times and stops the loop afterwards
        noLoop();
    }

}

function mousePressed(){
    index = 0;
    loop();
}
Sketches to figure out positioning of face elements.

Looking Outward 02 – Generative Art

Memo Aktens – “Gloomy Sunday”, 2017.

In the biography of his website, Memo Aktens writes that his “biggest inspiration is trying to understand the world around me.” Aktens’s unique perspective is reflected in his series “Learning to See”, involving him taking ordinary household objects like towels and cables and generating them into beautiful, lifelike landscapes. Seeing a video of the algorithm in action, it amazes me just how seamlessly it works and the level of abstraction it can produce from a simple arrangement of objects. He seems to be able to take this basic concept and extend it to different mediums, such as running a similar program to transform a video of a dancing saxophonist  into a moving rock formation by the beach. I am not too sure how Akten’s algorithm works, but he mentions that this series works utilizes machine learning and that he trained this model with images of four categories: ocean & waves, clouds & sky, fire, flowers, and images from the Hubble Space Telescope.

Memo Aktens – “#EpicGanGuy2019”, 2019.

LO 1 – Inspiration

A photo taken by me in the infinite crystal universe around 2016.

About 4 years ago, my family and I visited a TeamLab exhibit in Menlo Park, California. TeamLab are a group known for interactive spaces, sculptures, and exhibits, many of which immerse gallery visitors into an ethereal dimension.

The TeamLab exhibition in Pace Gallery featured unique exhibits such as a “flower room” where flowers were projected onto the floor with ceiling projectors, and every now and then some would disappear and pop up elsewhere. We ventured into a room filled with strands of crystal lights hanging straight down from the ceiling, meant to portray an infinite-looking space. Although this was one of the more well known exhibits of TeamLab, it wasn’t among my favorites.

A small exhibit meant for little kids actually intrigued me the most. Kids colored in templates of boats, buses, and cars and then fed them through a scanner. A giant projected screen, moments later, showed the kids’ drawings being transformed into digital models of cars bumbling about in a busy city.

There is very little information about the processes the members of TeamLab use to create these augmented-reality works, but their website mentions their interdisciplinary skills including programmers, CG animators, and mathematicians. Yayoi Kusama, a contemporary Japanese sculptor and artist, creates work similar to that of TeamLab. The work they produce has serious potential for the future, especially if virtual or augmented reality continue to grow in popularity and accessibility.

Project 01 – Self Portrait

sketch
function setup() {
    createCanvas(400, 400);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);
}

function draw() {
	background(158, 231, 234);
	//noStroke();
	stroke(25);
	strokeWeight(3);
	fill(221, 190, 140);
	circle(285, 210, 50); //r ear
	rect(110, 125, 180, 200, 20, 20, 80, 80); //face
	fill(230, 210, 203);
	circle(160, 195, 50);// l eye
	circle(240, 195, 50);//r eye
	fill(96, 81, 76);
	rect(110, 65, 165, 60, 60, 10, 0, 0); //main hair
	rect(280, 130, 15, 80, 0, 0, 15, 0); //lower hair
	fill(221, 155, 140);
	arc(200, 230, 30, 30, 1.57, -1.57); //nose
	line(180, 270, 220, 270); //mouth
	noStroke();
	fill(255, 255, 255);
	arc(160, 195, 40, 40, -2.356, 0.785);//left eye reflection
	arc(240, 195, 40, 40, -2.356, 0.785);//right eye reflection
	//line(200, 0, 200, 400);
	//line(0, 200, 400, 200);

}