Phenomenal & Enigmatic, part 4 of 4

#javascript #demoscene #learning #reverse-engineering

Written by Anders Marzi Tornblad

This is part 4 of the Phenomenal & Enigmatic series. If you haven't read the first part, here it is: Phenomenal & Enigmatic, part 1 of 4

Screenshot from the 2013 JavaScript Canvas demo Phenomenal and Enigmatic. Spinning 3D cube with 2D and 3D animations on each surface.I remember seeing the "TV Cube" part of Enigma for the first time, and not really being able to figure out how it was made. I was only 17 at the time, didn't have much experience with vector and matrix math. I didn't have a clue how to do proper backface culling, so when I made demos in the 1990s, my occational 3D graphics demos were never really good enough. So the thought of making 2D and 3D objects appear on the surfaces of another 3D object was definitely beyound my understanding of 3D math.

Once again, I am aware that a better way of doing this is probably by manipulating a transformation matrix to rotate, translate and project coordinates from different branches of a hierarchical coordinate system. But I ignored that and rolled it by hand.

Star field

The stars on the front of the cube might look as if there is some depth, but that is just an illusion. Each star has a (x,y) position ,and a third constant, that I call z, that governs speed along the X axis, as well as the alpha component of its color. The lower the speed, the dimmer the light. When observed, it gives the impression of a 3D space, but it's really just a form of parallax scrolling.

Pseudo code

// Star field
for (var star, i = 0; star = stars[i++];) {
    // Move the star a bit to the "right"
    star.x += (star.z * star.z * speedConstant);
    
    // Limit x to (-1 .. 1)
    if (star.x > 1) star.x -= 2;
    
    // Left out: Project the star's coordinates to screen coordinates
    var screenCoords = ( /* left out */ );
    
    // Draw the star, using Z to determine alpha and size
    context.fillStyle = "rgba(255,255,255," + (star.z * star.z).toFixed(3) + ")";
    context.fillRect(screenCoords.x, screenCoords.2, star.z * 2, star.z * 2);
}

Hidden line vector

Back in the 1990s, I could never do a proper hidden line vector, because I didn't know how to properly cull back-facing polygons. For the Phenomenal & Enigmatic "TV Cube" part, I arranged all polygons in the hidden line pyramid, so that when facing the camera, each polygon is to be drawn clockwise. That way, I could use a very simple algorithm to determine each polygon's visibility.

I found one really efficient algorithm for this on StackOverflow, and I learned that since all five polygons are convex (triangles cannot be concave, and the only quadrangle is a square) it's enough to only check the first three coordinates, even for the square.

Rotating the pyramid in 3D space was done in exactly the same way as rotating coordinates in the intro part of the demo, and after all coordinates are roteated, I used the polygon winding order algorithm to perform the backface culling. then drew all polygon outlines. Voilà, a hidden line vector.

Pseudo code

///Hidden line vector
// Points
var points = [
    { x : -40, y : -40, z :  70 }, // Four corners at the bottom
    { x :  40, y : -40, z :  70 },
    { x :  40, y :  40, z :  70 },
    { x : -40, y :  40, z :  70 },
    { x :   0, y :   0, z : -70 }  // And finally the top
];

// Each polygon is just an array of point indices
var polygons = [
    [0, 4, 3], // Four triangle sides
    [1, 4, 0],
    [2, 4, 1],
    [3, 4, 2],
    [3, 2, 1, 0] // And a quadrangle bottom
];

// First rotate the points in space and project to screen coordinates
var screenCoords = [];

for (var point, i = 0; point = points[i++];) {
    screenCoords.push(rotateAndProject(point)); // rotateAndProject is left out
}

// Then go through each polygon and draw those facing forward
for (var polygon, i = 0; polygon = polygons[i++];) {
    var edgeSum = 0;
    for (var j = 0; j < 3; ++j) {
        var pointIndex = polygon[j];
        var pointIndex2 = polygon[(j + 1) % 3];
        
        var point = screenCoords[pointIndex];
        var point2 = screenCoords[pointIndex2];
        
        edgeSum += (point2.x - point.x) * (point2.y + point.y);
    }
    
    if (edgeSum < 0) {
        // This polygon is facing the camera
        // Left out: Draw the polygon using screenCoords, context.moveTo and context.lineTo
    }
}

Plane vector

The plane vector part is much simpler than both the star field and the hidden line vector. I rotate a plane around its center, and then use the code already in place to project it to screen coordinates.

Projection

The function responsible for translating coordinates in the 3D space to screen coordinates is not very complex, since it is almost the exact same thing as for the intro part of the demo. Also, to determine which faces of the cube are facing the camera, I used the same backface culling algorithm as for the hidden line vector. I was really pleased with the end result.

You can try this solution at at atornblad.github.io/enigmatic. The latest version of the code is always available in the GitHub repository.

Articles in this series: