7
\$\begingroup\$

This is my attempt at creating a ray marcher for the command line for fun. I have some prior experience with C programming, although I'm far from being an expert, and little to no experience with computer graphics (so the renderer is probably going to be a mess πŸ˜…).

Any feedback on the implementation of the renderer or more general coding advice would be greatly appreciated :]

Link to the repo: https://gitlab.com/Konben/cli-ray-marching

The Code

vector.h

/*
 * FILENAME: vector.h
 *
 * DESCRIPTION: Defines some vector maths utility functions.
 */
#ifndef VECTOR_H
#define VECTOR_H

/* 3-dimensional cartesian vector with coordinates (x, y, z). */
struct vector3d
{
    float x, y, z;
};

#define V3(X, Y, Z) (struct vector3d) {.x = X, .y = Y, .z = Z}

struct vector3d vadd(struct vector3d v, struct vector3d w);

struct vector3d vsub(struct vector3d v, struct vector3d w);

struct vector3d scale(float scalar, struct vector3d v);

float length(struct vector3d v);

struct vector3d normalise(struct vector3d v);

float dot(struct vector3d v, struct vector3d w);

struct vector3d rotate(struct vector3d v, float x_rot, float y_rot, float z_rot);

#endif

vector.c

/*
 * FILENAME: vector.c
 *
 * DESCRIPTION: Implements the vector maths utility functions defined in vector.h.
 */
#include "vector.h"
#include <math.h>

struct vector3d vadd(struct vector3d v, struct vector3d w)
{
    return V3(v.x + w.x, v.y + w.y, v.z + w.z);
}

struct vector3d vsub(struct vector3d v, struct vector3d w)
{
    return vadd(v, scale(-1, w));
}

struct vector3d scale(float scalar, struct vector3d v)
{
    return V3(scalar*v.x, scalar*v.y, scalar*v.z);
}

float length(struct vector3d v)
{
    return sqrt(dot(v, v));
}

struct vector3d normalise(struct vector3d v)
{
    return scale(1.0/length(v), v);

}

float dot(struct vector3d v, struct vector3d w)
{
    return v.x*w.x + v.y*w.y + v.z*w.z;
}

struct vector3d rotate_x(struct vector3d v, float rot)
{
    return V3(
            v.x,
            cos(rot)*v.y - sin(rot)*v.z,
            sin(rot)*v.y + cos(rot)*v.z
        );
}

struct vector3d rotate_y(struct vector3d v, float rot)
{
    return V3(
            cos(rot)*v.x + sin(rot)*v.z,
            v.y,
            -sin(rot)*v.x + cos(rot)*v.z
        );
}

struct vector3d rotate_z(struct vector3d v, float rot)
{
    return V3(
            cos(rot)*v.x - sin(rot)*v.y,
            sin(rot)*v.x + cos(rot)*v.y,
            v.z
        );
}

struct vector3d rotate(struct vector3d v, float x_rot, float y_rot, float z_rot)
{
    // Not a very efficient way to do it,
    // but the generalised matrix was a bit too unwieldy.
    return rotate_z(rotate_y(rotate_x(v, x_rot), y_rot), z_rot);
}

render.h

/*
 * FILENAME: render.h
 *
 * DESCRIPTION: Defines the ray marcher.
 */
#ifndef MARCHING_H
#define MARCHING_H

#include "vector.h"

/* Signed distance functions */
typedef float (*sdf)(struct vector3d pos);

/*
 * Draws object to stdout using the ray marching algorithm.
 * The view frustum is originaly placed at (0, 0, 0) oriented in the Z-direction before any transformations are applied.
 *
 * @param object Object to be rendered as a signed distance function.
 * @param light_position Vector indicating the position of a light source.
 * @param view_position Vector indicating the position of the view frustum.
 * @param view_rotation_x Rotation of the view frustum in the x-axis.
 * @param view_rotation_y Rotation of the view frustum in the y-axis.
 * @param near_distance Distance to the near plane of the view frustum.
 * @param far_distance Distance to the far plane of the view frustum.
 * @param near_width Width of the view frustums near plane.
 * @param near_height Height of the view frustums near plane.
 * @param resolution Number of pixels (2 char long strings) displayed per unit of length.
 */
void render(
        sdf object,
        struct vector3d light_position,
        struct vector3d view_position,
        float view_rotation_x, float view_rotation_y,
        float near_distance, float far_distance,
        float near_width, float near_height,
        unsigned int resolution
    );

#endif

render.c

/*
 * FILENAME: render.c
 *
 * DESCRIPTION: Implements the ray marcher defined in render.h.
 */
#include "render.h"
#include <stdio.h>
#include <math.h>
#include <stdbool.h>

#define DELTA 0.0001 // Basically zero

char const *light_levels[] = {
    "..",
    "..",
    "β–‘β–‘",
    "β–‘β–‘",
    "β–‘β–‘",
    "β–’β–’",
    "β–’β–’",
    "β–’β–’",
    "β–“β–“",
    "β–“β–“",
    "β–ˆβ–ˆ",
};

struct vector3d cast(struct vector3d ray_base, struct vector3d ray_direction, sdf dist, float render_distance, bool *hit)
{
    struct vector3d ray = V3(0, 0, 0);
    float distance;

    do
    {
        distance = dist(vadd(ray_base, ray));

        if (length(ray) > render_distance)
        {
            *hit = false;
            return V3(0, 0, 0);
        }
        
        ray = vadd(ray, scale(distance, ray_direction));
    } while (distance > DELTA);

    *hit = true;
    return ray;
}

struct vector3d normal_at(struct vector3d v, sdf dist)
{
    return normalise(V3(
        dist(V3(v.x + DELTA, v.y, v.z)) - dist(V3(v.x - DELTA, v.y, v.z)),
        dist(V3(v.x, v.y + DELTA, v.z)) - dist(V3(v.x, v.y - DELTA, v.z)),
        dist(V3(v.x, v.y, v.z + DELTA)) - dist(V3(v.x, v.y, v.z - DELTA))
        ));
}

void render(
        sdf object,
        struct vector3d light_position,
        struct vector3d view_position,
        float view_rotation_x, float view_rotation_y,
        float near_distance, float far_distance,
        float near_width, float near_height,
        unsigned int resolution
    )
{
    struct vector3d near_tl = vsub(V3(0, 0, near_distance), V3(near_width/2, near_height/2, 0));
    float offset = 1.0/resolution;

    for (int y = 0; y < near_height*resolution; ++y)
    {
        struct vector3d base = vadd(near_tl, V3(offset/2, y*offset + offset/2, 0));

        for (int x = 0; x < near_width*resolution; ++x)
        {
            // Compute distance to far plane (max render distance).
            float render_distance = far_distance*length(base)/base.z;

            // Transform ray according to the view frustums position & rotation.
            struct vector3d ray_base = vadd(view_position, base);
            struct vector3d ray_direction = rotate(normalise(base), view_rotation_x, view_rotation_y, 0);

            // Cast ray
            bool hit;
            struct vector3d ray = cast(ray_base, ray_direction, object, render_distance, &hit);

            if (hit)
            {
                struct vector3d object_surface = vadd(ray_base, ray);

                // Compute light level in range 0-10 using surface normal.
                struct vector3d norm = normal_at(object_surface, object);
                struct vector3d light_direction = normalise(vsub(light_position, object_surface));
                int level = round(dot(light_direction, norm)*10);

                if (level >= 0)
                    fputs(light_levels[level], stdout);
                else
                    fputs("  ", stdout);
            }
            else 
            {
                fputs("  ", stdout);
            }
            
            base = vadd(base, V3(offset, 0, 0));
        }

        putchar('\n');
    }
}

main.c

/*
 * FILE: main.c
 *
 * DESCRIPTION: A simple ray-marcher for the terminal.
 */
#include "render.h"
#include "vector.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <termios.h>
#include <unistd.h>

// ANSI escape code to save & restore cursor position.
#define ANSI_SAVE_CURSOR "\033[s"
#define ANSI_RESTORE_CURSOR "\033[u"

#define ZOOM_SPEED 1.0
#define ROTATION_SPEED 0.1

// Signed distance function of a sphere at coord (0, 0, 0) and radius 1.
float sphere_sdf(struct vector3d pos);

int main(void)
{
    struct termios old_term, new_term;
    tcgetattr(STDIN_FILENO, &old_term);

    new_term = old_term;
    new_term.c_lflag &= ~ICANON;
    new_term.c_lflag &= ~ECHO;

    tcsetattr(STDIN_FILENO, TCSANOW, &new_term);

    puts("move up/left/down/right: w/a/s/d, zoom in/out: e/r, quit: q");

    float distance = 5;
    float rotation_x = 0;
    float rotation_y = 0;

    fputs(ANSI_SAVE_CURSOR, stdout);
    
    char cmd;
    while(cmd != 'q') {
        fputs(ANSI_RESTORE_CURSOR, stdout);

        render(
            sphere_sdf,
            V3(-5, -5, -5),
            rotate(scale(distance, V3(0, 0, -1)), rotation_x, rotation_y, 0),
            rotation_x, rotation_y,
            1, 32,
            2, 1,
            30
        );

        cmd = getchar();
        switch (cmd)
        {
        case 'w':
            rotation_x -= ROTATION_SPEED;
            break;
        case 'a':
            rotation_y += ROTATION_SPEED;
            break;
        case 's':
            rotation_x += ROTATION_SPEED;
            break;
        case 'd':
            rotation_y -= ROTATION_SPEED;
            break;
        case 'e':
            if (distance > ZOOM_SPEED)
            {
                distance -= 1;
            }
            break;
        case 'r':
            distance += 1;
            break;
        }
    }

    tcsetattr(STDIN_FILENO, TCSANOW, &old_term);
    return EXIT_SUCCESS;
}

float sphere_sdf(struct vector3d pos)
{
    float circle_radius = 1.0;

    return length(pos) - circle_radius;
}

Project Layout

.
β”œβ”€β”€ Makefile
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ main.c
β”‚Β Β  β”œβ”€β”€ render.c
β”‚Β Β  β”œβ”€β”€ render.h
β”‚Β Β  β”œβ”€β”€ vector.c
β”‚Β Β  └── vector.h
└── UNLICENSE

Example output

enter image description here

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

Prefer static inline functions to macros:

#define V3(X, Y, Z) (struct vector3d) {.x = X, .y = Y, .z = Z}

is better as:

#if defined(__GNUC__) || defined(__clang__)) || defined(__INTEL_LLVM_COMPILER)
    #define ATTRIB_CONST         __attribute__((const)
    #define ATTRIB_INLINE        __attribute__((always_inline))
#else
    #define ATTRIB_CONST 
    #define ATTRIB_INLINE
#endif

ATTRIB_CONST ATTRIB_INLINE static inline V3(float x, float y, float z) 
{
    return (struct vector3d) {x, y, z};
}

You'd likely find the const attribute useful for other functions too, if only for GCC, Clang, and ICC.

Use a consistent naming approach:

vadd(struct vector3d v, struct vector3d w);

struct vector3d vsub(struct vector3d v, struct vector3d w);

struct vector3d scale(float scalar, struct vector3d v);

Some identifiers are prefixed with a v, some are not. Consider using a common prefix for all identifiers (yes, this is C's way of doing namespaces):

vadd  ===> v3_add/vadd/v3add
vsub  ===> v3_sub/vsub/v3sub/
scale ===> v3_scale/vscale/v3scale
...

Define a type alias to type less:

#if 0
struct vector3d
{
    float x, y, z;
};
#else
typedef struct vector3d
{
    float x, y, z;
} vector3d;
#endif

Now we can write vector3d everywhere we were using struct vector3d.

The functions that are internal to a translation unit should be declared as having internal linkage:

rotate_x(), rotate_y(), and rotate_z() should be declared with the static keyword.

And light_levels should also be static in "render.c".

Do not ignore the return value of library functions:

tcgetattr() and tcsetattr() both have a return value. From the man page:

Upon successful completion, 0 shall be returned. Otherwise, -1 shall be returned and errno set to indicate the error.

Break main() into smaller functions:

    tcgetattr(STDIN_FILENO, &old_term);

    new_term = old_term;
    new_term.c_lflag &= ~ICANON;
    new_term.c_lflag &= ~ECHO;

    tcsetattr(STDIN_FILENO, TCSANOW, &new_term);

can be refactored into its own subroutine set_raw_mode(), or the like. Similarly, the code that is resetting the terminal back to cooked mode can be refactored into another subroutine.

The code responsible for taking a character as input and then processing it with a switch statement can also be moved to another subroutine.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks a lot for your thorough answer! One thing that confuses me though, is that I thought that aliasing structs is usually discouraged. Is this wrong/outdated information? \$\endgroup\$
    – Knogger
    Commented May 5 at 8:38
  • \$\begingroup\$ @Knogger I am aware that the Linux kernel coding style warns against it, but see: software.codidact.com/posts/290881 \$\endgroup\$
    – Harith
    Commented May 5 at 8:51
  • \$\begingroup\$ Thanks for the link, that makes a lot of sense to me. \$\endgroup\$
    – Knogger
    Commented May 5 at 9:11

Your Answer

By clicking β€œPost Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.