10
\$\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\$
1
  • 1
    \$\begingroup\$ Trivial: In main(), char cmd; is uninitialised, then tested to be 'q'... One day, this might bite you... Consider do/while instead of while as the loop in order to "fall out the bottom" rather than off the top of the loop... \$\endgroup\$
    – Fe2O3
    Commented Jul 5 at 23:01

2 Answers 2

5
\$\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
5
\$\begingroup\$

float vs. double

  • Avoid mixing types of functions for no gain.
// float length(struct vector3d v) { return sqrt(dot(v, v));} // Why call a double function?
   float length(struct vector3d v) { return sqrtf(dot(v, v));}
// Same for `sin()`, versus `sinf()`, etc.
  • double is closer to the default floating point type than float. Consider re-coding using double and additional functions for float if desired.

You may alternatively look to <tgmath.h>.

  • Use float constants rather than double when the code is primarily float.
// #define DELTA 0.0001
#define DELTA 0.0001f

Code directly

Rather than add function overhead:

struct vector3d vsub(struct vector3d v, struct vector3d w) {
    // return vadd(v, scale(-1, w));
    return V3(v.x - w.x, v.y - w.y, v.z - w.z);
}
\$\endgroup\$

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