This project felt like jumping into the deep end as far as rendering goes. I was inspired by Dan Shiffman's 4D Tesseract challenge, although I wanted to make the gif my own. Until now I hadn't really given any thought to there being dimensions beyond the ones we can comfortably perceive and how they work, and thinking about higher dimensions led me down a rabbit hole of awesome math and logic content by channels like Numberphile and 3B1B. Dan Shiffman's challenge in particular was in Java and was a lot more conducive to 3d with its P3D renderer, but I think I found a suitable workaround by drawing quads instead of individual points using WEBGL. I was also treating the easing function (doubleExpSig) as an after thought, but once I actually used it to control what I see as distance along the 4th dimension I was surprised by what a huge role it played in the aesthetic of the loop. I can't imagine pulling off a smooth and natural motion without it. That being said however, the gif doesn't convincingly feel "4D" and I was to revisit it once I have more time.
I didn't end up sketching very much for this assignment, but here's a few things I did that helped me keep track of what I was doing
I realized pretty early into the project that hardcoding in each of the 16 4d vertices was time-consuming and I often drew the quads in the wrong order. I decided to make use of modulus and int division to set the points in the right order.
This is me marking the points needed to make up each face. This is maybe a quarter of what I ended up needing and I think I could have spent more time on this planning phase.
// This is a template for creating a looping animation in p5.js (JavaScript). // When you press the 'F' key, this program will export a series of images into // your default Downloads folder. These can then be made into an animated gif. // This code is known to work with p5.js version 0.6.0 // Prof. Golan Levin, 28 January 2018 // INSTRUCTIONS FOR EXPORTING FRAMES (from which to make a GIF): // 1. Run a local server, using instructions from here: // https://github.com/processing/p5.js/wiki/Local-server // 2. Set the bEnableExport variable to true. // 3. Set the myNickname variable to your name. // 4. Run the program from Chrome, press 'f'. // Look in your 'Downloads' folder for the generated frames. // 5. Note: Retina screens may export frames at twice the resolution. //=================================================== // User-modifiable global variables. var myNickname = "rigatoni"; var nFramesInLoop = 60; var bEnableExport = true; // Other global variables you don't need to touch. var nElapsedFrames; var bRecording; var theCanvas; //=================================================== function setup() { theCanvas = createCanvas(720, 720, WEBGL); bRecording = false; nElapsedFrames = 0; } //=================================================== function keyTyped() { if (bEnableExport) { if ((key === 'f') || (key === 'F')) { bRecording = true; nElapsedFrames = 0; } } } //=================================================== function draw() { // Compute a percentage (0...1) representing where we are in the loop. var percentCompleteFraction = 0; if (bRecording) { percentCompleteFraction = float(nElapsedFrames) / float(nFramesInLoop); } else { percentCompleteFraction = float(frameCount % nFramesInLoop) / float(nFramesInLoop); } // Render the design, based on that percentage. // This function renderMyDesign() is the one for you to change. renderMyDesign (percentCompleteFraction); // If we're recording the output, save the frame to a file. // Note that the output images may be 2x large if you have a Retina mac. // You can compile these frames into an animated GIF using a tool like: if (bRecording && bEnableExport) { var frameOutputFilename = myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png"; print("Saving output image: " + frameOutputFilename); saveCanvas(theCanvas, frameOutputFilename, 'png'); nElapsedFrames++; if (nElapsedFrames >= nFramesInLoop) { bRecording = false; } } } //=================================================== function renderMyDesign (percent) { background(180); var cube = new Hypercube(500, percent) rotateY(percent*PI) cube.Draw() } function Hypercube(size, margin) { this.points = [] margin -= 1 margin = doubleExponentialSigmoid(margin) for(var i=0; i<16; i++) { var j = i var w = floor(j/8)*margin j=j%8 var stereo = 1/(2-w) var z = floor(j/4)*stereo-(0.5*stereo) j=j%4 var y = floor(j/2)*stereo-(0.5*stereo) j=j%2 var x = floor(j/1)*stereo-(0.5*stereo) this.points[i] = new P4(x*size, y*size, z*size, 0) } this.Draw = function() { fill(225,15) var front = new Face(this.points[4], this.points[5], this.points[6], this.points[7]) var back = new Face(this.points[0], this.points[1], this.points[2], this.points[3]) var left = new Face(this.points[0], this.points[2], this.points[4], this.points[6]) var right = new Face(this.points[1], this.points[3], this.points[5], this.points[7]) var top = new Face(this.points[2], this.points[3], this.points[6], this.points[7]) var bottom = new Face(this.points[0], this.points[1], this.points[4], this.points[5]) var sFront = new Face(this.points[12], this.points[13], this.points[14], this.points[15]) var sBack = new Face(this.points[8], this.points[9], this.points[10], this.points[11]) var sLeft = new Face(this.points[8], this.points[10], this.points[12], this.points[14]) var sRight = new Face(this.points[9], this.points[11], this.points[13], this.points[15]) var sTop = new Face(this.points[10], this.points[11], this.points[14], this.points[15]) var sBottom = new Face(this.points[8], this.points[9], this.points[12], this.points[13]) var pfront = new Face(this.points[4], this.points[5], this.points[12], this.points[13]) var pback = new Face(this.points[0], this.points[1], this.points[8], this.points[9]) var pleft = new Face(this.points[0], this.points[2], this.points[8], this.points[10]) var pright = new Face(this.points[1], this.points[3], this.points[9], this.points[11]) var ptop = new Face(this.points[2], this.points[3], this.points[10], this.points[11]) var pbottom = new Face(this.points[0], this.points[4], this.points[8], this.points[12]) var psFront = new Face(this.points[1], this.points[5], this.points[9], this.points[13]) var psBack = new Face(this.points[5], this.points[7], this.points[13], this.points[15]) var psLeft = new Face(this.points[3], this.points[7], this.points[11], this.points[15]) var psRight = new Face(this.points[2], this.points[6], this.points[10], this.points[14]) var psTop = new Face(this.points[6], this.points[7], this.points[14], this.points[15]) var psBottom = new Face(this.points[4], this.points[6], this.points[12], this.points[14]) front.Draw() back.Draw() left.Draw() right.Draw() sFront.Draw() sBack.Draw() sLeft.Draw() sRight.Draw() pfront.Draw() pback.Draw() pleft.Draw() pright.Draw() psFront.Draw() psBack.Draw() psLeft.Draw() psRight.Draw() } } function Face(p1, p2, p3, p4) { var distance = 200 this.p1 = p1 this.p2 = p2 this.p3 = p3 this.p4 = p4 this.Draw = function() { beginShape() vertex(this.p1.x,this.p1.y,this.p1.z) vertex(this.p2.x,this.p2.y,this.p2.z) vertex(this.p4.x,this.p4.y,this.p4.z) vertex(this.p3.x,this.p3.y,this.p3.z) endShape(CLOSE) } this.Print = function() { this.p1.Print() this.p2.Print() this.p3.Print() this.p4.Print() } } function P4(x,y,z,w) { this.x = x this.y = y this.z = z this.w = w this.Print = function() { print(this.x, this.y, this.z, this.w) } this.ScalarMult = function(multiplier) { this.x*=multiplier this.y*=multiplier this.z*=multiplier this.w*=multiplier } } // Symmetric double-element sigmoid function ('_a' is the slope) // See https://github.com/IDMNYU/p5.js-func/blob/master/lib/p5.func.js // From: https://idmnyu.github.io/p5.js-func/ //=================================================== function doubleExponentialSigmoid (_x, _a){ if(!_a) _a = 0.75; // default var min_param_a = 0.0 + Number.EPSILON; var max_param_a = 1.0 - Number.EPSILON; _a = constrain(_a, min_param_a, max_param_a); _a = 1-_a; var _y = 0; if (_x<=0.5){ _y = (pow(2.0*_x, 1.0/_a))/2.0; } else { _y = 1.0 - (pow(2.0*(1.0-_x), 1.0/_a))/2.0; } return(_y); } |