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
- A: Defines a 3D point.
- M: Projects a 3D point onto the 2D screen.
- P: Multiplies coordinates by a rotation matrix.
- R: Sets up a rotation matrix for rotating the cube.
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));
}
}