diff --git a/.gitignore b/.gitignore index 1b57dab..bbe7f47 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ test/test_tuples test/test_canvas test/test_matrices test/test_transformations +test/test_intersections +test/test_rays *.ppm \ No newline at end of file diff --git a/intersections.h b/intersections.h new file mode 100644 index 0000000..37df9a1 --- /dev/null +++ b/intersections.h @@ -0,0 +1,76 @@ +#pragma once + +#include "spheres.h" +#include "rays.h" + +struct intersection { + float t; + struct sphere const * const object; +}; + +struct intersection intersection(float t, struct sphere const * const object) +{ + return (struct intersection){ t, object }; +} + +struct intersections { + int count; + struct intersection i[]; +}; + +struct intersections2 { + int count; + struct intersection i[2]; +}; + +struct intersections4 { + int count; + struct intersection i[4]; +}; + +struct intersections2 intersections2(struct intersection a, struct intersection b) +{ + return (struct intersections2){ 2, {a, b} }; +} + +struct intersections4 intersections4(struct intersection a, struct intersection b, struct intersection c, struct intersection d) +{ + return (struct intersections4){ 4, {a, b, c, d} }; +} + +inline static struct intersections2 intersect(struct sphere const * const s, struct ray r) +{ + struct mat4x4 m = mat4x4_inverse(&s->transform); + struct ray r2 = ray_transform(r, &m); + struct tuple sphere_to_ray = tuple_sub(r2.origin, point(0.0f, 0.0f, 0.0f)); + + float a = tuple_dot(r2.direction, r2.direction); + float b = 2 * tuple_dot(r2.direction, sphere_to_ray); + float c = tuple_dot(sphere_to_ray, sphere_to_ray) - 1; + + float discriminant = b * b - 4 * a * c; + + if (discriminant < 0) { + return (struct intersections2) { 0 }; + } else { + float root = sqrtf(discriminant); + float t1 = (-b - root) / (2 * a); + float t2 = (-b + root) / (2 * a); + + return + intersections2(intersection(t1, s), + intersection(t2, s)); + } +} + +inline static struct intersection * hit(struct intersections * xs) +{ + struct intersection * i = NULL; + for (int n = 0; n < xs->count; n++) { + if (xs->i[n].t >= 0) { + if (i == NULL || i->t > xs->i[n].t) + i = &xs->i[n]; + } + } + return i; +} diff --git a/rays.h b/rays.h new file mode 100644 index 0000000..7d814f2 --- /dev/null +++ b/rays.h @@ -0,0 +1,26 @@ +#pragma once + +#include "spheres.h" +#include "tuples.h" +#include "matrices.h" + +struct ray { + struct tuple origin; + struct tuple direction; +}; + +inline static struct ray ray(struct tuple origin, struct tuple direction) +{ + return (struct ray){origin, direction}; +} + +inline static struct tuple ray_position(struct ray ray, float t) +{ + return tuple_add(ray.origin, tuple_mul(ray.direction, t)); +} + +inline static struct ray ray_transform(struct ray r, struct mat4x4 const * const m) +{ + return ray(mat4x4_mul_t(m, &r.origin), + mat4x4_mul_t(m, &r.direction)); +} diff --git a/raytracer.c b/raytracer.c new file mode 100644 index 0000000..2692887 --- /dev/null +++ b/raytracer.c @@ -0,0 +1,41 @@ +#include "tuples.h" +#include "canvas.h" +#include "rays.h" +#include "spheres.h" +#include "intersections.h" +#include "transformations.h" + +int main() +{ + struct tuple ray_origin = point(0.0f, 0.0f, -5.0f); + + float wall_z = 10.0f; + float wall_size = 7.0f; + int canvas_pixels = 100; + float pixel_size = wall_size / (float)canvas_pixels; + float half = wall_size / 2.0f; + struct canvas c = canvas(canvas_pixels, canvas_pixels); + struct tuple red = color(1.0f, 0.0f, 0.0f); + struct sphere shape = sphere(); + struct mat4x4 shear = shearing(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); + struct mat4x4 scale = scaling(0.5f, 1.0f, 1.0f); + struct mat4x4 m = mat4x4_mul_m(&shear, &scale); + shape.transform = m; + + for (int y = 0; y < canvas_pixels; y++) { + float world_y = half - pixel_size * y; + for (int x = 0; x < canvas_pixels; x++) { + float world_x = -half + pixel_size * x; + + struct tuple position = point(world_x, world_y, wall_z); + struct ray r = ray(ray_origin, tuple_normalize(tuple_sub(position, ray_origin))); + struct intersections2 xs = intersect(&shape, r); + struct intersection * h = hit((struct intersections *)&xs); + if (h != NULL) { + canvas_write_pixel(c, x, y, red); + } + } + } + + canvas_to_ppm(c, "test.ppm"); +} diff --git a/spheres.h b/spheres.h new file mode 100644 index 0000000..6ee9dba --- /dev/null +++ b/spheres.h @@ -0,0 +1,14 @@ +#pragma once + +#include "matrices.h" + +struct sphere { + struct mat4x4 transform; +}; + +inline static struct sphere sphere() +{ + return (struct sphere){ + mat4x4_identity() + }; +} diff --git a/test/run.sh b/test/run.sh index b0c7a9d..8611f48 100644 --- a/test/run.sh +++ b/test/run.sh @@ -2,7 +2,7 @@ set -eux -for name in tuples canvas matrices transformations; do +for name in tuples canvas matrices transformations rays intersections spheres; do gcc -g -gdwarf-5 \ -Wall -Werror -Wfatal-errors \ -I. \ diff --git a/test/runner.h b/test/runner.h index 1986355..88630b1 100644 --- a/test/runner.h +++ b/test/runner.h @@ -2,18 +2,26 @@ typedef bool (*test_t)(const char ** scenario); +#define ANSI_RED "\x1b[31m" +#define ANSI_GREEN "\x1b[32m" +#define ANSI_RESET "\x1b[0m" + #define RUNNER(tests) \ int main() \ { \ int fail_count = 0; \ for (int i = 0; i < (sizeof (tests)) / (sizeof (test_t)); i++) { \ - const char * scenario = NULL; \ + const char * scenario = NULL; \ bool result = tests[i](&scenario); \ - const char * result_s = result ? "ok" : "fail"; \ + const char * result_s = result ? "ok" : ANSI_RED "fail" ANSI_RESET; \ fail_count += !result; \ fprintf(stderr, "%s: %s\n", scenario, result_s); \ } \ - fprintf(stderr, "\nfailed tests: %d\n", fail_count); \ + if (fail_count == 0) { \ + fprintf(stderr, ANSI_GREEN "failed tests: %d\n\n" ANSI_RESET, fail_count); \ + } else { \ + fprintf(stderr, ANSI_RED "failed tests: %d\n\n" ANSI_RESET, fail_count); \ + } \ \ return !(fail_count == 0); \ } diff --git a/test/test_intersections.c b/test/test_intersections.c new file mode 100644 index 0000000..84ed675 --- /dev/null +++ b/test/test_intersections.c @@ -0,0 +1,110 @@ +#include +#include + +#include "intersections.h" +#include "runner.h" + +static bool intersections_test_0(const char ** scenario) +{ + *scenario = "An intersection encapsulates t and object"; + + struct sphere s = sphere(); + struct intersection i = intersection(3.5f, &s); + + return + float_equal(i.t, 3.5f) && + i.object == &s; +} + +static bool intersections_test_1(const char ** scenario) +{ + *scenario = "Aggregating intersections2"; + + struct sphere s = sphere(); + struct intersection i1 = intersection(1.0f, &s); + struct intersection i2 = intersection(2.0f, &s); + struct intersections2 xs = intersections2(i1, i2); + + return + xs.count == 2 && + float_equal(xs.i[0].t, 1.0f) && + float_equal(xs.i[1].t, 2.0f); +} + +static bool intersections_test_2(const char ** scenario) +{ + *scenario = "The hit, when all intersections2 have positive t"; + + struct sphere s = sphere(); + struct intersection i1 = intersection(1.0f, &s); + struct intersection i2 = intersection(2.0f, &s); + struct intersections2 xs = intersections2(i2, i1); + + struct intersection * i = hit((struct intersections *)&xs); + + return + i != NULL && + i->t == i1.t && + i->object == i1.object; +} + +static bool intersections_test_3(const char ** scenario) +{ + *scenario = "The hit, when some intersections2 have negative t"; + + struct sphere s = sphere(); + struct intersection i1 = intersection(-1.0f, &s); + struct intersection i2 = intersection( 1.0f, &s); + struct intersections2 xs = intersections2(i2, i1); + + struct intersection * i = hit((struct intersections *)&xs); + + return + i != NULL && + i->t == i2.t && + i->object == i2.object; +} + +static bool intersections_test_4(const char ** scenario) +{ + *scenario = "The hit, when all intersections2 have negative t"; + + struct sphere s = sphere(); + struct intersection i1 = intersection(-2.0f, &s); + struct intersection i2 = intersection(-1.0f, &s); + struct intersections2 xs = intersections2(i2, i1); + + struct intersection * i = hit((struct intersections *)&xs); + + return i == NULL; +} + +static bool intersections_test_5(const char ** scenario) +{ + *scenario = "The hit is always the lowest nonnegative intersection"; + + struct sphere s = sphere(); + struct intersection i1 = intersection( 5.0f, &s); + struct intersection i2 = intersection( 7.0f, &s); + struct intersection i3 = intersection(-3.0f, &s); + struct intersection i4 = intersection( 2.0f, &s); + struct intersections4 xs = intersections4(i1, i2, i3, i4); + + struct intersection * i = hit((struct intersections *)&xs); + + return + i != NULL && + i->t == i4.t && + i->object == i4.object; +} + +test_t intersections_tests[] = { + intersections_test_0, + intersections_test_1, + intersections_test_2, + intersections_test_3, + intersections_test_4, + intersections_test_5, +}; + +RUNNER(intersections_tests); diff --git a/test/test_rays.c b/test/test_rays.c new file mode 100644 index 0000000..d861a42 --- /dev/null +++ b/test/test_rays.c @@ -0,0 +1,68 @@ +#include +#include + +#include "rays.h" +#include "runner.h" +#include "transformations.h" + +static bool rays_test_0(const char ** scenario) +{ + *scenario = "Creating and querying a ray"; + + struct tuple origin = point(1.0f, 2.0f, 3.0f); + struct tuple direction = vector(4.0f, 5.0f, 6.0f); + struct ray r = ray(origin, direction); + + return + tuple_equal(r.origin, origin) && + tuple_equal(r.direction, direction); +} + +static bool rays_test_1(const char ** scenario) +{ + *scenario = "Computing a point from a distance"; + + struct ray r = ray(point(2.0f, 3.0f, 4.0f), vector(1.0f, 0.0f, 0.0f)); + + return + tuple_equal(ray_position(r, 0.0f), point(2.0f, 3.0f, 4.0f)) && + tuple_equal(ray_position(r, 1.0f), point(3.0f, 3.0f, 4.0f)) && + tuple_equal(ray_position(r, -1.0f), point(1.0f, 3.0f, 4.0f)) && + tuple_equal(ray_position(r, 2.5f), point(4.5f, 3.0f, 4.0f)); +} + + +static bool rays_test_2(const char ** scenario) +{ + *scenario = "Translating a ray"; + + struct ray r = ray(point(1.0f, 2.0f, 3.0f), vector(0.0f, 1.0f, 0.0f)); + struct mat4x4 m = translation(3.0f, 4.0f, 5.0f); + struct ray r2 = ray_transform(r, &m); + + return + tuple_equal(r2.origin, point(4.0f, 6.0f, 8.0f)) && + tuple_equal(r2.direction, vector(0.0f, 1.0f, 0.0f)); +} + +static bool rays_test_3(const char ** scenario) +{ + *scenario = "Scaling a ray"; + + struct ray r = ray(point(1.0f, 2.0f, 3.0f), vector(0.0f, 1.0f, 0.0f)); + struct mat4x4 m = scaling(2.0f, 3.0f, 4.0f); + struct ray r2 = ray_transform(r, &m); + + return + tuple_equal(r2.origin, point(2.0f, 6.0f, 12.0f)) && + tuple_equal(r2.direction, vector(0.0f, 3.0f, 0.0f)); +} + +test_t rays_tests[] = { + rays_test_0, + rays_test_1, + rays_test_2, + rays_test_3, +}; + +RUNNER(rays_tests); diff --git a/test/test_spheres.c b/test/test_spheres.c new file mode 100644 index 0000000..3d1f954 --- /dev/null +++ b/test/test_spheres.c @@ -0,0 +1,153 @@ +#include +#include + +#include "rays.h" +#include "spheres.h" +#include "intersections.h" +#include "runner.h" +#include "matrices.h" +#include "transformations.h" + +static bool spheres_test_0(const char ** scenario) +{ + *scenario = "A ray intersects a sphere at two points"; + + struct ray r = ray(point(0.0f, 0.0f, -5.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + struct intersections2 xs = intersect(&s, r); + + return + xs.count == 2 && + float_equal(xs.i[0].t, 4.0f) && + float_equal(xs.i[1].t, 6.0f); +} + +static bool spheres_test_1(const char ** scenario) +{ + *scenario = "A ray intersects a sphere at a tangent"; + + struct ray r = ray(point(0.0f, 1.0f, -5.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + struct intersections2 xs = intersect(&s, r); + + return + xs.count == 2 && + float_equal(xs.i[0].t, 5.0f) && + float_equal(xs.i[1].t, 5.0f); +} + +static bool spheres_test_2(const char ** scenario) +{ + *scenario = "A ray misses a sphere"; + + struct ray r = ray(point(0.0f, 2.0f, -5.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + struct intersections2 xs = intersect(&s, r); + + return xs.count == 0; +} + +static bool spheres_test_3(const char ** scenario) +{ + *scenario = "A ray originates in a sphere"; + + struct ray r = ray(point(0.0f, 0.0f, 0.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + struct intersections2 xs = intersect(&s, r); + + return + xs.count == 2 && + float_equal(xs.i[0].t, -1.0f) && + float_equal(xs.i[1].t, 1.0f); +} + +static bool spheres_test_4(const char ** scenario) +{ + *scenario = "A sphere is behind a ray"; + + struct ray r = ray(point(0.0f, 0.0f, 5.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + struct intersections2 xs = intersect(&s, r); + + return + xs.count == 2 && + float_equal(xs.i[0].t, -6.0f) && + float_equal(xs.i[1].t, -4.0f); +} + +static bool spheres_test_5(const char ** scenario) +{ + *scenario = "Intersect sets the object on the intersection"; + + struct ray r = ray(point(0.0f, 0.0f, -5.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + struct intersections2 xs = intersect(&s, r); + + return + xs.count == 2 && + xs.i[0].object == &s && + xs.i[1].object == &s; +} + +static bool spheres_test_6(const char ** scenario) +{ + *scenario = "A sphere's default transformation"; + + struct sphere s = sphere(); + struct mat4x4 identity = mat4x4_identity(); + + return mat4x4_equal(&s.transform, &identity); +} + +static bool spheres_test_7(const char ** scenario) +{ + *scenario = "Changing a sphere's transformation"; + + struct sphere s = sphere(); + struct mat4x4 t = translation(2.0f, 3.0f, 4.0f); + s.transform = t; + + return mat4x4_equal(&s.transform, &t); +} + +static bool spheres_test_8(const char ** scenario) +{ + *scenario = "Intersecting a scaled sphere with a ray"; + + struct ray r = ray(point(0.0f, 0.0f, -5.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + s.transform = scaling(2.0f, 2.0f, 2.0f); + struct intersections2 xs = intersect(&s, r); + + return + xs.count == 2 && + float_equal(xs.i[0].t, 3.0f) && + float_equal(xs.i[1].t, 7.0f); +} + +static bool spheres_test_9(const char ** scenario) +{ + *scenario = "Intersecting a translated sphere with a ray"; + + struct ray r = ray(point(0.0f, 0.0f, 5.0f), vector(0.0f, 0.0f, 1.0f)); + struct sphere s = sphere(); + s.transform = translation(5.0f, 0.0f, 0.0f); + struct intersections2 xs = intersect(&s, r); + + return xs.count == 0; +} + +test_t spheres_tests[] = { + spheres_test_0, + spheres_test_1, + spheres_test_2, + spheres_test_3, + spheres_test_4, + spheres_test_5, + spheres_test_6, + spheres_test_7, + spheres_test_8, + spheres_test_9, +}; + +RUNNER(spheres_tests);