Drawing a Rotating Cube in C: A Beginner-Friendly Guide

3 minute read Published: 2024-06-12

github repo

Have you ever wondered how to create simple animations on your computer screen? In this post, we'll walk through how to draw a rotating cube using the C programming language. Don't worry if you're not familiar with coding or computer science – we'll break it down step-by-step.

Introduction

This project will involve drawing a 3D cube on a 2D terminal screen and making it rotate to create an animation effect. We'll use some math and programming concepts, but we'll explain them in simple terms.

Ingredients: The Building Blocks

Including Essential Libraries

First, we include some standard libraries that provide tools for our code:

#include <stdio.h>    // For standard input and output functions
#include <string.h>   // For manipulating text strings
#include <unistd.h>   // For creating delays
#include <math.h>     // For mathematical functions like cosine and sine
#include <stdlib.h>   // For general functions like absolute value

Setting Up the Canvas

We define the size of our canvas (screen):

#define W 80          // Width of the canvas
#define H 22          // Height of the canvas

Defining 3D Points and Projections

We'll use macros (short snippets of code) to handle 3D points and project them onto a 2D screen:

#define A(x, y, z, a, b, c) float a = x, b = y, c = z
#define M(a, b, x, y, z) float f = 2 / (z + 5); a = 40 + 15 * x * f; b = 10 + 15 * y * f
#define P(i, j) (m[i][0] * x + m[i][1] * y + m[i][2] * z)
#define R(a, b, c, d) float c = cos(a), d = sin(a); m[0][0] = 1; m[0][1] = 0; m[0][2] = 0; m[1][0] = 0; m[1][1] = c; m[1][2] = -d; m[2][0] = 0; m[2][1] = d; m[2][2] = c

Step-by-Step Animation Creation

Initial Setup

We start by defining some variables and initializing our canvas:

int main() {
    float m[3][3], t = 0; // Rotation matrix and time variable
    int x, y, z, i, j, k; // Loop counters and temporary variables
    char c[W * H]; // Screen buffer
    float vertices[8][3], transformed[8][2]; // Arrays for 3D and 2D points
    int faces[6][4] = {
        {0, 1, 3, 2},
        {4, 5, 7, 6},
        {0, 1, 5, 4},
        {2, 3, 7, 6},
        {0, 2, 6, 4},
        {1, 3,7, 5}
    }; // Definitions of cube faces
    char shades[] = " .:-=+*#%@"; // Shading characters

    memset(c, ' ', sizeof(c)); // Initialize the screen buffer with spaces

Main Loop: Drawing Frames

We continuously draw new frames to create the animation:

    for (;;) {
        t += 0.05; // Increment time
        R(t, 1, c1, s1); // Set up the rotation matrix

Calculate 3D to 2D Projection

For each corner of the cube, we calculate its new position after rotating and project it onto the 2D screen:

        for (i = 0; i < 8; i++) {
            A((i & 1) * 2 - 1, ((i >> 1) & 1) * 2 - 1, ((i >> 2) & 1) * 2 - 1, x, y, z);
            float px = P(0, 1), py = P(1, 1), pz = P(2, 1);
            vertices[i][0] = px;
            vertices[i][1] = py;
            vertices[i][2] = pz;
            float a, b;
            M(a, b, px, py, pz);
            transformed[i][0] = a;
            transformed[i][1] = b;
        }

Draw Cube Faces with Shading

We calculate the shading for each face and draw the edges:

        for (i = 0; i < 6; i++) {
            float normal_x = 0, normal_y = 0, normal_z = 0;
            for (j = 0; j < 4; j++) {
                int next = (j + 1) % 4;
                normal_x += (vertices[faces[i][j]][1] - vertices[faces[i][next]][1]) * (vertices[faces[i][j]][2] + vertices[faces[i][next]][2]);
                normal_y += (vertices[faces[i][j]][2] - vertices[faces[i][next]][2]) * (vertices[faces[i][j]][0] + vertices[faces[i][next]][0]);
                normal_z += (vertices[faces[i][j]][0] - vertices[faces[i][next]][0]) * (vertices[faces[i][j]][1] + vertices[faces[i][next]][1]);
            }
            float shade = normal_z / sqrt(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z);
            char shade_char = shades[(int)((shade + 1) * 4.5)];

            for (j = 0; j < 4; j++) {
                int next = (j + 1) % 4;
                int x0 = (int)transformed[faces[i][j]][0], y0 = (int)transformed[faces[i][j]][1];
                int x1 = (int)transformed[faces[i][next]][0], y1 = (int)transformed[faces[i][next]][1];
                int dx = abs(x1 - x0), dy = abs(y1 - y0);
                int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
                int err = (dx > dy ? dx : -dy) / 2, e2;
                while (1) {
                    if (x0 >= 0 && x0 < W && y0 >= 0 && y0 < H) c[y0 * W + x0] = shade_char;
                    if (x0 == x1 && y0 == y1) break;
                    e2 = err;
                    if (e2 > -dx) { err -= dy; x0 += sx; }
                    if (e2 < dy) { err += dx; y0 += sy; }
                }
            }
        }

Display the Frame

We print the current frame and set up for the next:

        for (i = 0; i < H; i++) {
            for (j = 0; j < W; j++) putchar(c[i * W + j]);
            putchar('\n');
        }

        usleep(100000); // Wait for 100 milliseconds
        printf("\x1b[%dA", H); // Move the cursor back to the top of the screen
        memset(c, ' ', sizeof(c)); // Clear the screen buffer
    }
}

Understanding the Math

3D to 2D Projection

To convert a 3D point to a 2D point:

f = 2 / (z + 5)
a = 40 + 15 * x * f
b = 10 + 15 * y * f

Here, (x, y, z) are the 3D coordinates, and (a, b) are the 2D coordinates on the screen.

Rotation Matrix

For rotating around the z-axis:

[ cos(θ) -sin(θ) 0 ]
[ sin(θ)  cos(θ) 0 ]
[    0       0    1 ]

This matrix multiplies with the coordinates to get the new rotated coordinates.

Normal Vector

The normal vector is a vector that is perpendicular to a surface. It helps in determining the shading:

Normal = (v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x)

Here, v1 and v2 are vectors on the face of

the cube.

Conclusion

By following these steps, you can create a simple but fascinating rotating cube animation on your terminal. This project is a great way to get started with graphics programming and understand how 3D objects can be represented on 2D screens. Happy coding!

Full Code Example


#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <math.h>
#include <stdlib.h> // For abs function

#define W 80
#define H 22
#define A(x, y, z, a, b, c) float a = x, b = y, c = z
#define M(a, b, x, y, z) float f = 2 / (z + 5); a = 40 + 15 * x * f; b = 10 + 15 * y * f
#define P(i, j) (m[i][0] * x + m[i][1] * y + m[i][2] * z)
#define R(a, b, c, d) float c = cos(a), d = sin(a); m[0][0] = 1; m[0][1] = 0; m[0][2] = 0; m[1][0] = 0; m[1][1] = c; m[1][2] = -d; m[2][0] = 0; m[2][1] = d; m[2][2] = c

int main() {
    float m[3][3], t = 0;
    int x, y, z, i, j, k;
    char c[W * H];
    float vertices[8][3], transformed[8][2];
    int faces[6][4] = {
        {0, 1, 3, 2},
        {4, 5, 7, 6},
        {0, 1, 5, 4},
        {2, 3, 7, 6},
        {0, 2, 6, 4},
        {1, 3, 7, 5}
    };
    char shades[] = " .:-=+*#%@";

    memset(c, ' ', sizeof(c));

    for (;;) {
        t += 0.05;
        R(t, 1, c1, s1);

        for (i = 0; i < 8; i++) {
            A((i & 1) * 2 - 1, ((i >> 1) & 1) * 2 - 1, ((i >> 2) & 1) * 2 - 1, x, y, z);
            float px = P(0, 1), py = P(1, 1), pz = P(2, 1);
            vertices[i][0] = px;
            vertices[i][1] = py;
            vertices[i][2] = pz;
            float a, b;
            M(a, b, px, py, pz);
            transformed[i][0] = a;
            transformed[i][1] = b;
        }

        for (i = 0; i < 6; i++) {
            float normal_x = 0, normal_y = 0, normal_z = 0;
            for (j = 0; j < 4; j++) {
                int next = (j + 1) % 4;
                normal_x += (vertices[faces[i][j]][1] - vertices[faces[i][next]][1]) * (vertices[faces[i][j]][2] + vertices[faces[i][next]][2]);
                normal_y += (vertices[faces[i][j]][2] - vertices[faces[i][next]][2]) * (vertices[faces[i][j]][0] + vertices[faces[i][next]][0]);
                normal_z += (vertices[faces[i][j]][0] - vertices[faces[i][next]][0]) * (vertices[faces[i][j]][1] + vertices[faces[i][next]][1]);
            }
            float shade = normal_z / sqrt(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z);
            char shade_char = shades[(int)((shade + 1) * 4.5)];

            for (j = 0; j < 4; j++) {
                int next = (j + 1) % 4;
                int x0 = (int)transformed[faces[i][j]][0], y0 = (int)transformed[faces[i][j]][1];
                int x1 = (int)transformed[faces[i][next]][0], y1 = (int)transformed[faces[i][next]][1];
                int dx = abs(x1 - x0), dy = abs(y1 - y0);
                int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
                int err = (dx > dy ? dx : -dy) / 2, e2;
                while (1) {
                    if (x0 >= 0 && x0 < W && y0 >= 0 && y0 < H) c[y0 * W + x0] = shade_char;
                    if (x0 == x1 && y0 == y1) break;
                    e2 = err;
                    if (e2 > -dx) { err -= dy; x0 += sx; }
                    if (e2 < dy) { err += dx; y0 += sy; }
                }
            }
        }

        for (i = 0; i < H; i++) {
            for (j = 0; j < W; j++) putchar(c[i * W + j]);
            putchar('\n');
        }

        usleep(100000);
        printf("\x1b[%dA", H);
        memset(c, ' ', sizeof(c));
    }
}