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
main()
,char cmd;
is uninitialised, then tested to be'q'
... One day, this might bite you... Considerdo/while
instead ofwhile
as the loop in order to "fall out the bottom" rather than off the top of the loop... \$\endgroup\$