cube

A rotating 3D wireframe cube rendered in the terminal
git clone https://github.com/michaelneuper/cube.git
README | Files | Log | Refs | LICENSE

cube/src/cube.c (3889B)


/**
 * @file    cube.c
 * @author  Michael Neuper (michael@michaelneuper.com)
 *
 * Implements the cube logic: vertex and edge initialisation,
 * perspective projection, rotation, and ncurses rendering.
 */

#include <math.h>
#include <ncurses.h>
#include <stdlib.h>

#include "cube.h"
#include "debug.h"
#include "ncdraw.h"

static const double cam_distance = 50.0;
static const double zoom         = 500.0;
static const double scale_x      = 2.0;
static const double scale_y      = 1.0;

static inline double trans_x(double x) {
    return x + getmaxx(stdscr) / 2.0;
}
static inline double trans_y(double y) {
    return y + getmaxy(stdscr) / 2.0;
}

/**
 * Represents a projected point in 2D screen-space.
 */
typedef struct {
    double x, y;
} Point2D;

/** Converts a projected 2D point to an ncurses screen pixel. */
static Pixel point_to_pixel(Point2D p);
/** Applies perspective projection to a 3D point, returning its 2D screen
 * position. */
static Point2D project_point(Point3D p);
/** Rotates a 3D point around the given axis by angle radians. */
static Point3D rotate_point(Point3D p, Axis3D axis, double angle);

Cube *create_cube() {
    Cube *cube = malloc(sizeof(Cube));

    if (!cube) return NULL;

    // clang-format off
    *cube = (Cube){
        .vertices = {
                { -1, -1, -1 },  //     1-------3
                { -1, -1,  1 },  //    /|      /|
                { -1,  1, -1 },  //   / |     / |
                { -1,  1,  1 },  //  5-------7  |
                {  1, -1, -1 },  //  |  0----|--2
                {  1, -1,  1 },  //  | /     | /
                {  1,  1, -1 },  //  |/      |/
                {  1,  1,  1 },  //  4-------6
            },
        .edges = {
                { 0, 1 }, { 0, 2 }, { 0, 4 },
                { 1, 3 }, { 1, 5 },
                { 2, 3 }, { 2, 6 },
                { 3, 7 }, // top line
                { 4, 5 },
                          { 4, 6 },
                { 5, 7 },
                { 6, 7 },
            },
    };
    // clang-format on

    return cube;
}

void destroy_cube(Cube *cube) {
    free(cube);
    endwin();
}

void rotate_cube(Cube *cube, Axis3D axis, double angle) {
    ASSERT_NOT_NULL(cube);

    for (size_t i = 0; i < CUBE_VERTICES; i++) {
        cube->vertices[i] = rotate_point(cube->vertices[i], axis, angle);
    }
}

void draw_cube(const Cube *cube) {
    ASSERT_NOT_NULL(cube);

    for (size_t i = 0; i < CUBE_EDGES; i++) {
        Edge e = cube->edges[i];

        Point2D p1 = project_point(cube->vertices[e.v1]);
        Point2D p2 = project_point(cube->vertices[e.v2]);

        draw_line(point_to_pixel(p1), point_to_pixel(p2));
    }
}

static Pixel point_to_pixel(Point2D p) {
    float x = trans_x(scale_x * p.x);
    float y = trans_y(scale_y * -p.y);

    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (x >= getmaxx(stdscr)) x = getmaxx(stdscr) - 1;
    if (y >= getmaxy(stdscr)) y = getmaxy(stdscr) - 1;

    return (Pixel){
        .row = y,
        .col = x,
    };
}

static Point2D project_point(Point3D p) {
    double depth = p.z + cam_distance;

    return (Point2D){
        .x = zoom * p.x / depth,
        .y = zoom * p.y / depth,
    };
}

static Point3D rotate_point(Point3D p, Axis3D axis, double angle) {
    switch (axis) {
        case X:
            return (Point3D){
                .x = p.x,
                .y = p.y * cos(angle) - p.z * sin(angle),
                .z = p.y * sin(angle) + p.z * cos(angle),
            };

        case Y:
            return (Point3D){
                .x = p.x * cos(angle) + p.z * sin(angle),
                .y = p.y,
                .z = -p.x * sin(angle) + p.z * cos(angle),
            };

        case Z:
            return (Point3D){
                .x = p.x * cos(angle) - p.y * sin(angle),
                .y = p.x * sin(angle) + p.y * cos(angle),
                .z = p.z,
            };
    }
}