7 Commits

Author SHA1 Message Date
Godzil
2ea4abdce7 Boundingboxes should be ready.
Next step (later) would be to properly use them other than group to lower the number of intersection calculation per ray.
2020-02-25 18:03:12 +00:00
Godzil
831a096281 Continue working on bounding boxes. 2020-02-25 09:20:38 +00:00
Godzil
3011544e8f Started working on boundingboxes. 2020-02-24 18:03:25 +00:00
Godzil
d1965caf8d Add an option to run coverage locally 2020-02-24 17:26:36 +00:00
Godzil
7bbe5e843b Group should work now. 2020-02-24 17:25:54 +00:00
Godzil
7c794f0496 Working on groups 2020-02-24 09:25:52 +00:00
Godzil
80f59efa43 Add another hardcoded scene. Also made a test file for hw3 that should cover all the commands. 2020-02-23 19:37:47 +00:00
29 changed files with 1043 additions and 17 deletions

View File

@@ -10,12 +10,32 @@ option(COVERALLS "Generate coveralls data" OFF)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/external/coveralls-cmake/cmake)
option(PACKAGE_TESTS "Build the tests" ON)
option(ENABLE_COVERAGE "Build for code coverage" OFF)
if (ENABLE_COVERAGE AND COVERALLS)
message(FATAL_ERROR "You can't enable both ENABLE_COVERAGE and COVERALLS at the same time")
endif()
if (COVERALLS)
include(Coveralls)
coveralls_turn_on_coverage()
endif()
if (ENABLE_COVERAGE)
if("${CMAKE_C_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang" OR
"${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
message("Building with LLVM Code Coverage Tools")
set(CMAKE_CXX_FLAGS "-fprofile-instr-generate -fcoverage-mapping")
elseif(CMAKE_COMPILER_IS_GNUCXX)
message("Building with lcov Code Coverage Tools")
set(CMAKE_CXX_FLAGS "--coverage")
else()
message(FATAL_ERROR "Compiler ${CMAKE_C_COMPILER_ID} is not supported for code coverage")
endif()
endif()
# LodePNG don't make a .a or .so, so let's build a library here
add_library(LodePNG STATIC)

View File

@@ -0,0 +1,106 @@
/*
* DoRayMe - a quick and dirty Raytracer
* Bounding box header
*
* Created by Manoël Trapier
* Copyright (c) 2020 986-Studio.
*
*/
#ifndef DORAYME_BOUNDINGBOX_H
#define DORAYME_BOUNDINGBOX_H
struct BoundingBox
{
private:
bool isReset;
public:
Tuple min;
Tuple max;
BoundingBox() : min(INFINITY, INFINITY, INFINITY, 1.0), max(-INFINITY, -INFINITY, -INFINITY, 1.0), isReset(true) { };
BoundingBox(Tuple min, Tuple max) : min(min), max(max), isReset(false) { };
void operator|(const BoundingBox &b) {
isReset = false;
if (this->min.x > b.min.x) { this->min.x = b.min.x; }
if (this->min.y > b.min.y) { this->min.y = b.min.y; }
if (this->min.z > b.min.z) { this->min.z = b.min.z; }
if (this->max.x < b.max.x) { this->max.x = b.max.x; }
if (this->max.y < b.max.y) { this->max.y = b.max.y; }
if (this->max.z < b.max.z) { this->max.z = b.max.z; }
}
bool haveFiniteBounds() { return this->min.isRepresentable() && this->max.isRepresentable(); };
bool fitsIn(const BoundingBox &other) {
bool fits = true;
if (this->min.x > other.min.x) { fits = false; }
if (this->min.y > other.min.y) { fits = false; }
if (this->min.z > other.min.z) { fits = false; }
if (this->max.x < other.max.x) { fits = false; }
if (this->max.y < other.max.y) { fits = false; }
if (this->max.z < other.max.z) { fits = false; }
return fits;
}
void checkAxis(double axeOrigin, double axeDirection, double xMin, double xMax, double *axeMin, double *axeMax)
{
double tMinNumerator = (xMin - axeOrigin);
double tMaxNumerator = (xMax - axeOrigin);
if (fabs(axeDirection) >= getEpsilon())
{
*axeMin = tMinNumerator / axeDirection;
*axeMax = tMaxNumerator / axeDirection;
}
else
{
*axeMin = tMinNumerator * INFINITY;
*axeMax = tMaxNumerator * INFINITY;
}
if (*axeMin > *axeMax)
{
double swap = *axeMax;
*axeMax = *axeMin;
*axeMin = swap;
}
}
void reset()
{
this->isReset = true;
min.x = min.y = min.z = INFINITY;
max.x = max.y = max.z = -INFINITY;
}
bool isEmpty() { return this->isReset; };
bool intesectMe(Ray r) {
double xtMin, xtMax, ytMin, ytMax, ztMin, ztMax;
double tMin, tMax;
this->checkAxis(r.origin.x, r.direction.x, this->min.x, this->max.x, &xtMin, &xtMax);
this->checkAxis(r.origin.y, r.direction.y, this->min.y, this->max.y, &ytMin, &ytMax);
this->checkAxis(r.origin.z, r.direction.z, this->min.z, this->max.z, &ztMin, &ztMax);
tMin = max3(xtMin, ytMin, ztMin);
tMax = min3(xtMax, ytMax, ztMax);
if (tMin <= tMax)
{
return true;
}
return false;
}
};
#endif //DORAYME_BOUNDINGBOX_H

View File

@@ -28,6 +28,8 @@ public:
double maxCap;
Cone() : minCap(-INFINITY), maxCap(INFINITY), isClosed(false), Shape(SHAPE_CONE) {};
BoundingBox getBounds();
bool haveFiniteBounds() { return !(isinf(this->minCap) || isinf(this->maxCap)); };
};
#endif /* DORAYME_CONE_H */

View File

@@ -28,6 +28,9 @@ public:
double maxCap;
Cylinder() : minCap(-INFINITY), maxCap(INFINITY), isClosed(false), Shape(SHAPE_CYLINDER) {};
BoundingBox getBounds();
bool haveFiniteBounds() { return !(isinf(this->minCap) || isinf(this->maxCap)); };
};
#endif //DORAYME_CYLINDER_H

48
source/include/group.h Normal file
View File

@@ -0,0 +1,48 @@
/*
* DoRayMe - a quick and dirty Raytracer
* Group header
*
* Created by Manoël Trapier
* Copyright (c) 2020 986-Studio.
*
*/
#ifndef DORAYME_GROUP_H
#define DORAYME_GROUP_H
#include <shape.h>
/* TODO: Add a way to force(?) material from group to be applied on childs */
class Group : public Shape
{
private:
uint32_t allocatedObjectCount;
Shape* *objectList;
uint32_t objectCount;
uint32_t allocatedUnboxableObjectCount;
Shape* *unboxableObjectList;
uint32_t unboxableObjectCount;
protected:
Intersect localIntersect(Ray r);
Tuple localNormalAt(Tuple point);
BoundingBox bounds;
public:
bool isEmpty();
void addObject(Shape *s);
Shape *operator[](const int p) { return this->objectList[p]; }
Intersect intersect(Ray r);
BoundingBox getBounds();
void updateBoundingBox();
Group();
};
#endif /* DORAYME_GROUP_H */

View File

@@ -17,6 +17,8 @@ private:
public:
Plane() : Shape(SHAPE_PLANE) { };
BoundingBox getBounds();
bool haveFiniteBounds() { return false; };
};
#endif //DORAYME_PLANE_H

View File

@@ -16,6 +16,7 @@ class Shape;
#include <matrix.h>
#include <intersect.h>
#include <material.h>
#include <boundingbox.h>
enum ShapeType
{
@@ -25,6 +26,8 @@ enum ShapeType
SHAPE_CUBE,
SHAPE_CYLINDER,
SHAPE_CONE,
SHAPE_GROUP,
};
/* Base class for all object that can be presented in the world */
@@ -32,23 +35,36 @@ class Shape
{
private:
ShapeType type;
private:
Matrix localTransformMatrix;
protected:
virtual Intersect localIntersect(Ray r) = 0;
virtual Tuple localNormalAt(Tuple point) = 0;
public:
Matrix transformMatrix;
Matrix inverseTransform;
Matrix transposedInverseTransform;
Material material;
bool dropShadow;
Shape *parent;
public:
Shape(ShapeType = SHAPE_NONE);
Intersect intersect(Ray r);
virtual Intersect intersect(Ray r);
virtual Intersect intersectOOB(Ray r) { return this->intersect(r); };
Tuple normalAt(Tuple point);
/* Bounding box points are always world value */
virtual BoundingBox getBounds();
virtual bool haveFiniteBounds() { return true; };
void updateTransform();
Tuple worldToObject(Tuple point) { return this->inverseTransform * point; };
Tuple objectToWorld(Tuple point) { return this->transformMatrix * point; };
Tuple normalToWorld(Tuple normalVector);
void setTransform(Matrix transform);
void setMaterial(Material material) { this->material = material; };
Ray transform(Ray r) { return Ray(this->transformMatrix * r.origin, this->transformMatrix * r.direction); };

View File

@@ -15,7 +15,7 @@
class Sphere : public Shape
{
private:
protected:
Intersect localIntersect(Ray r);
Tuple localNormalAt(Tuple point);

View File

@@ -17,6 +17,7 @@ public:
double x, y, z, w;
public:
Tuple() : x(0), y(0), z(0), w(0.0) {};
Tuple(double x, double y, double z) : x(x), y(y), z(z), w(0.0) {};
Tuple(double x, double y, double z, double w) : x(x), y(y), z(z), w(w) {};
bool isPoint() { return (this->w == 1.0); };
@@ -39,6 +40,11 @@ public:
Tuple operator/(const double &b) const { return Tuple(this->x / b, this->y / b,
this->z / b, this->w / b); };
void fixPoint();
void fixVector();
bool isRepresentable();
void set(double nX, double nY, double nZ) { this->x = nX; this->y = nY; this->z = nZ; };
double magnitude();
Tuple normalise();
double dot(const Tuple &b);
@@ -49,12 +55,14 @@ public:
class Point: public Tuple
{
public:
Point() : Tuple(0, 0, 0, 1.0) {};
Point(double x, double y, double z) : Tuple(x, y, z, 1.0) {};
};
class Vector: public Tuple
{
public:
Vector() : Tuple(0, 0, 0, 0.0) {};
Vector(double x, double y, double z) : Tuple(x, y, z, 0.0) {};
};

View File

@@ -24,6 +24,7 @@ Intersect::Intersect()
Intersect::~Intersect()
{
/* Free stuff */
free(this->list);
}
void Intersect::add(Intersection i)

View File

@@ -25,6 +25,9 @@ double getEpsilon()
bool double_equal(double a, double b)
{
if (isinf(a) && isinf(b))
return true;
return fabs(a - b) < current_precision;
}

View File

@@ -18,7 +18,7 @@ Pattern::Pattern(Colour a, Colour b): a(a), b(b)
Colour Pattern::patternAtObject(Shape *object, Tuple worldPoint)
{
Tuple objectPoint = object->inverseTransform * worldPoint;
Tuple objectPoint = object->worldToObject(worldPoint);
Tuple patternPoint = this->inverseTransform * objectPoint;
return this->patternAt(patternPoint);

View File

@@ -122,3 +122,20 @@ Tuple Cone::localNormalAt(Tuple point)
}
return Vector(point.x, y, point.z);
}
BoundingBox Cone::getBounds()
{
BoundingBox ret;
double a = fabs(this->minCap);
double b = fabs(this->maxCap);
double limit = (a > b)?a:b;
ret.min = this->objectToWorld(Point(-limit, this->minCap, -limit));
ret.max = this->objectToWorld(Point(limit, this->maxCap, limit));
ret.min.fixPoint();
ret.max.fixPoint();
return ret;
}

View File

@@ -107,3 +107,16 @@ Tuple Cylinder::localNormalAt(Tuple point)
return Vector(point.x, 0, point.z);
}
BoundingBox Cylinder::getBounds()
{
BoundingBox ret;
ret.min = this->objectToWorld(Point(-1, this->minCap, -1));
ret.max = this->objectToWorld(Point(1, this->maxCap, 1));
ret.min.fixPoint();
ret.max.fixPoint();
return ret;
}

139
source/shapes/group.cpp Normal file
View File

@@ -0,0 +1,139 @@
/*
* DoRayMe - a quick and dirty Raytracer
* Group implementation
*
* Created by Manoël Trapier
* Copyright (c) 2020 986-Studio.
*
*/
#include <tuple.h>
#include <ray.h>
#include <group.h>
#include <cone.h>
#include <math_helper.h>
#define MIN_ALLOC (2)
Group::Group() : Shape(SHAPE_GROUP)
{
this->allocatedObjectCount = MIN_ALLOC;
this->objectList = (Shape **)calloc(sizeof(Shape *), MIN_ALLOC);
this->objectCount = 0;
this->allocatedUnboxableObjectCount = MIN_ALLOC;
this->unboxableObjectList = (Shape **)calloc(sizeof(Shape *), MIN_ALLOC);
this->unboxableObjectCount = 0;
}
Intersect Group::intersect(Ray r)
{
Intersect ret;
int i, j;
if (this->objectCount > 0)
{
if (this->bounds.intesectMe(r))
{
for (i = 0 ; i < this->objectCount ; i++)
{
Intersect xs = this->objectList[i]->intersect(r);
if (xs.count() > 0)
{
for (j = 0 ; j < xs.count() ; j++)
{
ret.add(xs[j]);
}
}
}
}
}
/* We are force to do them all the time */
if (this->unboxableObjectCount > 0)
{
for(i = 0; i < this->unboxableObjectCount; i++)
{
Intersect xs = this->unboxableObjectList[i]->intersect(r);
if (xs.count() > 0)
{
for(j = 0; j < xs.count(); j++)
{
ret.add(xs[j]);
}
}
}
}
return ret;
}
Intersect Group::localIntersect(Ray r)
{
return Intersect();
}
Tuple Group::localNormalAt(Tuple point)
{
return Vector(1, 0, 0);
}
/* ONLY INSERT SHAPES THAT ARE NOT GOING TO CHANGE ELSE..! */
void Group::addObject(Shape *s)
{
if (s->haveFiniteBounds())
{
if ((this->objectCount + 1) > this->allocatedObjectCount)
{
this->allocatedObjectCount *= 2;
this->objectList = (Shape **)realloc(this->objectList, sizeof(Shape **) * this->allocatedObjectCount);
}
s->parent = this;
s->updateTransform();
this->objectList[this->objectCount++] = s;
this->bounds | s->getBounds();
}
else
{
if ((this->unboxableObjectCount + 1) > this->allocatedUnboxableObjectCount)
{
this->allocatedUnboxableObjectCount *= 2;
this->unboxableObjectList = (Shape **)realloc(this->unboxableObjectList, sizeof(Shape **) * this->allocatedUnboxableObjectCount);
}
s->parent = this;
s->updateTransform();
this->unboxableObjectList[this->unboxableObjectCount++] = s;
}
}
bool Group::isEmpty()
{
return (this->objectCount == 0) && (this->unboxableObjectCount == 0);
}
BoundingBox Group::getBounds()
{
if (this->bounds.isEmpty()) { this->updateBoundingBox(); }
return this->bounds;
}
void Group::updateBoundingBox()
{
this->bounds.reset();
if (this->objectCount > 0)
{
int i;
for(i = 0; i < this->objectCount; i++)
{
if (!this->objectList[i]->haveFiniteBounds())
{
BoundingBox objB = this->objectList[i]->getBounds();
this->bounds | objB;
}
}
}
}

View File

@@ -33,4 +33,17 @@ Intersect Plane::localIntersect(Ray r)
Tuple Plane::localNormalAt(Tuple point)
{
return Vector(0, 1, 0);
}
BoundingBox Plane::getBounds()
{
BoundingBox ret;
ret.min = this->objectToWorld(Point(-INFINITY, 0-getEpsilon(), -INFINITY));
ret.max = this->objectToWorld(Point(INFINITY, 0+getEpsilon(), INFINITY));
ret.min.fixPoint();
ret.max.fixPoint();
return ret;
}

View File

@@ -15,10 +15,11 @@
Shape::Shape(ShapeType type)
{
this->parent = nullptr;
this->dropShadow = true;
this->type = type;
this->transformMatrix = Matrix4().identity();
this->inverseTransform = this->transformMatrix.inverse();
this->localTransformMatrix = Matrix4().identity();
this->updateTransform();
}
Intersect Shape::intersect(Ray r)
@@ -26,22 +27,50 @@ Intersect Shape::intersect(Ray r)
return this->localIntersect(this->invTransform(r));
};
Tuple Shape::normalAt(Tuple point)
Tuple Shape::normalToWorld(Tuple normalVector)
{
Tuple local_point = this->inverseTransform * point;
Tuple local_normal = this->localNormalAt(local_point);
Tuple world_normal = this->inverseTransform.transpose() * local_normal;
Tuple world_normal = this->transposedInverseTransform * normalVector;
/* W may get wrong, so hack it. This is perfectly normal as we are using a 4x4 matrix instead of a 3x3 */
world_normal.w = 0;
return world_normal.normalise();
};
Tuple Shape::normalAt(Tuple point)
{
Tuple local_point = this->worldToObject(point);
Tuple local_normal = this->localNormalAt(local_point);
Tuple world_normal = this->normalToWorld(local_normal);
return world_normal;
}
void Shape::updateTransform()
{
this->transformMatrix = this->localTransformMatrix;
if (this->parent != nullptr)
{
this->transformMatrix = this->parent->transformMatrix * this->transformMatrix;
}
this->inverseTransform = this->transformMatrix.inverse();
this->transposedInverseTransform = this->inverseTransform.transpose();
}
void Shape::setTransform(Matrix transform)
{
this->transformMatrix = transform;
this->inverseTransform = transform.inverse();
this->localTransformMatrix = transform;
this->updateTransform();
}
BoundingBox Shape::getBounds()
{
BoundingBox ret;
ret.min = this->objectToWorld(Point(-1, -1, -1));
ret.max = this->objectToWorld(Point(1, 1, 1));
return ret;
}

View File

@@ -43,4 +43,28 @@ Tuple Tuple::cross(const Tuple &b) const
Tuple Tuple::reflect(const Tuple &normal)
{
return *this - normal * 2 * this->dot(normal);
}
void Tuple::fixPoint()
{
if (isnan(this->x) || isnan(this->y) || isnan(this->z))
{
/* w is probably broken, so fix it */
this->w = 1;
}
}
void Tuple::fixVector()
{
if (isnan(this->x) || isnan(this->y) || isnan(this->z))
{
/* w is probably broken, so fix it */
this->w = 0;
}
}
bool Tuple::isRepresentable()
{
return !(isnan(this->x) || isnan(this->y) || isnan(this->z));
}

View File

@@ -5,7 +5,8 @@ find_package(Threads REQUIRED)
set(TESTS_SRC math_test.cpp tuple_test.cpp colour_test.cpp canvas_test.cpp matrix_test.cpp transformation_test.cpp
ray_test.cpp intersect_test.cpp sphere_test.cpp light_test.cpp material_test.cpp world_test.cpp camera_test.cpp
shape_test.cpp plane_test.cpp pattern_test.cpp cube_test.cpp cylinder_test.cpp cone_test.cpp)
shape_test.cpp plane_test.cpp pattern_test.cpp cube_test.cpp cylinder_test.cpp cone_test.cpp group_test.cpp
boundingbox_test.cpp)
add_executable(testMyRays)
target_include_directories(testMyRays PUBLIC ${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
@@ -24,6 +25,11 @@ target_include_directories(hw3render PUBLIC ../source/include)
target_sources(hw3render PRIVATE hw3render.cpp)
target_link_libraries(hw3render rayonnement)
add_executable(test_render)
target_include_directories(test_render PUBLIC ../source/include)
target_sources(test_render PRIVATE test_render.cpp)
target_link_libraries(test_render rayonnement)
add_executable(ch5_test)
target_include_directories(ch5_test PUBLIC ../source/include)
target_sources(ch5_test PRIVATE ch5_test.cpp)
@@ -90,4 +96,6 @@ add_test(NAME Chapter11_Test COMMAND $<TARGET_FILE:ch11_test>)
add_test(NAME Chapter12_Test COMMAND $<TARGET_FILE:ch12_test>)
add_test(NAME Chapter13_Test COMMAND $<TARGET_FILE:ch13_test>)
add_test(NAME Chapter13_ConeBonus COMMAND $<TARGET_FILE:ch13_cone>)
add_test(NAME Hw3Render COMMAND $<TARGET_FILE:hw3render> ${CMAKE_CURRENT_SOURCE_DIR}/test.hw3scene)
add_test(NAME Test_Rendering COMMAND $<TARGET_FILE:test_render>)
add_test(NAME Hw3Render COMMAND $<TARGET_FILE:hw3render> ${CMAKE_CURRENT_SOURCE_DIR}/test.hw3scene)
add_test(NAME Hw3RenderAllCmds COMMAND $<TARGET_FILE:hw3render> ${CMAKE_CURRENT_SOURCE_DIR}/test_keys.hw3scene)

View File

@@ -0,0 +1,81 @@
/*
* DoRayMe - a quick and dirty Raytracer
* Boundingbox unit tests
*
* Created by Manoël Trapier
* Copyright (c) 2020 986-Studio.
*
*/
/*
* DoRayMe - a quick and dirty Raytracer
* Camera unit tests
*
* Created by Manoël Trapier
* Copyright (c) 2020 986-Studio.
*
*/
#include <math.h>
#include <math_helper.h>
#include <ray.h>
#include <transformation.h>
#include <stdint.h>
#include <boundingbox.h>
#include <gtest/gtest.h>
TEST(BoundingBox, Default_boundingbox_is_not_set)
{
BoundingBox bb;
ASSERT_TRUE(bb.isEmpty());
ASSERT_EQ(bb.min, Point(INFINITY, INFINITY, INFINITY));
ASSERT_EQ(bb.max, Point(-INFINITY, -INFINITY, -INFINITY));
}
TEST(BoundingBox, Bounding_box_can_be_created_with_values)
{
BoundingBox bb = BoundingBox(Point(-1, -1, -1), Point(1, 1, 1));
ASSERT_FALSE(bb.isEmpty());
ASSERT_EQ(bb.min, Point(-1, -1, -1));
ASSERT_EQ(bb.max, Point(1, 1, 1));
}
TEST(BoundingBox, Cating_a_bb_to_an_empty_bb_reset_the_original_one)
{
BoundingBox bb;
bb | BoundingBox(Point(-1, -1, -1), Point(1, 1, 1));
ASSERT_FALSE(bb.isEmpty());
ASSERT_EQ(bb.min, Point(-1, -1, -1));
ASSERT_EQ(bb.max, Point(1, 1, 1));
}
TEST(BoundingBox, Cating_a_bb_to_another_bb_expand_the_original_one_if_needed)
{
BoundingBox bb(Point(-1, -1, -1), Point(1, 1, 1));
bb | BoundingBox(Point(-2, 0, -5), Point(4, 5, 0.5));
ASSERT_FALSE(bb.isEmpty());
ASSERT_EQ(bb.min, Point(-2, -1, -5));
ASSERT_EQ(bb.max, Point(4, 5, 1));
}
TEST(BoundingBox, A_smaller_bb_should_fit_in_a_bigger)
{
BoundingBox bigBb = BoundingBox(Point(-10, -10, -10), Point(10, 10, 10));
BoundingBox smallBb = BoundingBox(Point(-2, -2, -2), Point(2, 2, 2));
ASSERT_TRUE(bigBb.fitsIn(smallBb));
}
TEST(BoundingBox, A_big_bb_should_not_fit_in_a_smaller)
{
BoundingBox bigBb = BoundingBox(Point(-10, -10, -10), Point(10, 10, 10));
BoundingBox smallBb = BoundingBox(Point(-2, -2, -2), Point(2, 2, 2));
ASSERT_FALSE(smallBb.fitsIn(bigBb));
}

View File

@@ -130,3 +130,41 @@ TEST(ConeTest, Computing_the_normal_vector_on_a_cone)
ASSERT_EQ(cone.doLocalNormalAt(HitPointss[i]), Normals[i]);
}
}
TEST(ConeTest, The_bounding_box_of_a_cut_cone)
{
Cone t = Cone();
BoundingBox b = BoundingBox(Point(-8, -5, -8), Point(8, 8, 8));
t.minCap = -5;
t.maxCap = 8;
BoundingBox res = t.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}
TEST(ConeTest, The_bounding_box_of_a_uncut_cone)
{
/* This one is tricky. Infinite size don't cope well with transformations */
Cone t = Cone();
BoundingBox res = t.getBounds();
ASSERT_FALSE(res.min.isRepresentable());
ASSERT_FALSE(res.max.isRepresentable());
}
TEST(ConeTest, An_uncut_cone_have_infinite_bounds)
{
Cone t = Cone();
ASSERT_FALSE(t.haveFiniteBounds());
}
TEST(ConeTest, A_cut_cone_have_finite_bounds)
{
Cone t = Cone();
t.minCap = -1;
t.maxCap = 1;
ASSERT_TRUE(t.haveFiniteBounds());
}

View File

@@ -114,4 +114,22 @@ TEST(CubeTest, The_normal_on_the_surface_of_a_cube)
{
ASSERT_EQ(c.normalAt(HitPoints[i]), ExpectedNormals[i]);
}
}
TEST(CubeTest, The_bounding_box_of_a_cube)
{
Cube t = Cube();
BoundingBox b = BoundingBox(Point(-1, -1, -1), Point(1, 1, 1));
BoundingBox res = t.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}
TEST(CubeTest, A_cube_have_finite_bounds)
{
Cube t = Cube();
ASSERT_TRUE(t.haveFiniteBounds());
}

View File

@@ -220,4 +220,42 @@ TEST(CylinderTest, The_normal_on_a_cylinder_end_cap)
{
ASSERT_EQ(cyl.normalAt(HitPointss[idx]), Normals[idx]);
}
}
TEST(CylinderTest, The_bounding_box_of_a_cut_cylinder)
{
Cylinder t = Cylinder();
BoundingBox b = BoundingBox(Point(-1, -10000, -1), Point(1, 10000, 1));
t.minCap = -10000;
t.maxCap = 10000;
BoundingBox res = t.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}
TEST(CylinderTest, The_bounding_box_of_a_uncut_cylinder)
{
/* This one is tricky. Infinite size don't cope well with transformations */
Cylinder t = Cylinder();
BoundingBox res = t.getBounds();
ASSERT_FALSE(res.min.isRepresentable());
ASSERT_FALSE(res.max.isRepresentable());
}
TEST(CylinderTest, An_uncut_cylinder_have_infinite_bounds)
{
Cylinder t = Cylinder();
ASSERT_FALSE(t.haveFiniteBounds());
}
TEST(CylinderTest, A_cut_cylinder_have_finite_bounds)
{
Cylinder t = Cylinder();
t.minCap = -1;
t.maxCap = 1;
ASSERT_TRUE(t.haveFiniteBounds());
}

100
tests/group_test.cpp Normal file
View File

@@ -0,0 +1,100 @@
/*
* DoRayMe - a quick and dirty Raytracer
* Group unit tests
*
* Created by Manoël Trapier
* Copyright (c) 2020 986-Studio.
*
*/
#include <intersect.h>
#include <intersection.h>
#include <group.h>
#include <testshape.h>
#include <sphere.h>
#include <transformation.h>
#include <gtest/gtest.h>
TEST(GroupTest, Creating_a_new_group)
{
Group g = Group();
ASSERT_EQ(g.transformMatrix, Matrix4().identity());
ASSERT_TRUE(g.isEmpty());
}
TEST(GroupTest, Adding_a_child_to_a_group)
{
Group g = Group();
TestShape s = TestShape();
g.addObject(&s);
ASSERT_FALSE(g.isEmpty());
ASSERT_EQ(s.parent, &g);
ASSERT_EQ(g[0], &s);
}
TEST(GroupTest, Intersecting_a_ray_with_an_empty_group)
{
Group g = Group();
Ray r = Ray(Point(0, 0, 0), Vector(0, 0, 1));
Intersect xs = g.intersect(r);
ASSERT_EQ(xs.count(), 0);
}
TEST(GroupTest, Intersecting_a_ray_with_an_nonempty_group)
{
Group g = Group();
Sphere s1 = Sphere();
Sphere s2 = Sphere();
Sphere s3 = Sphere();
s2.setTransform(translation(0, 0, -3));
s3.setTransform(translation(5, 0, 0));
g.addObject(&s1);
g.addObject(&s2);
g.addObject(&s3);
Ray r = Ray(Point(0, 0, -5), Vector(0, 0, 1));
Intersect xs = g.intersect(r);
ASSERT_EQ(xs.count(), 4);
EXPECT_EQ(xs[0].object, &s2);
EXPECT_EQ(xs[1].object, &s2);
EXPECT_EQ(xs[2].object, &s1);
EXPECT_EQ(xs[3].object, &s1);
}
TEST(GroupTest, Intersecting_a_transformed_group)
{
Group g = Group();
Sphere s = Sphere();
g.setTransform(scaling(2, 2, 2));
s.setTransform(translation(5, 0, 0));
g.addObject(&s);
Ray r = Ray(Point(10, 0, -50), Vector(0, 0, 1));
Intersect xs = g.intersect(r);
ASSERT_EQ(xs.count(), 2);
}
TEST(GroupTest, Group_bounding_box)
{
Group g = Group();
Sphere s = Sphere();
g.setTransform(scaling(2, 2, 2));
s.setTransform(translation(5, 0, 0));
g.addObject(&s);
BoundingBox b = BoundingBox(Point(8, -2, -2), Point(12, 2, 2));
BoundingBox res = g.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}

View File

@@ -68,4 +68,21 @@ TEST(PlaneTest, A_ray_intersecting_a_plane_from_below)
ASSERT_EQ(xs.count(), 1);
ASSERT_EQ(xs[0].t, 1);
ASSERT_EQ(xs[0].object, &p);
}
TEST(PlaneTest, The_bounding_box_of_a_plane)
{
Plane t = Plane();
BoundingBox b = BoundingBox(Point(-8, -5, -8), Point(8, 8, 8));
BoundingBox res = t.getBounds();
ASSERT_FALSE(res.min.isRepresentable());
ASSERT_FALSE(res.max.isRepresentable());
}
TEST(PlaneTest, A_plane_have_infinite__bounds)
{
Plane t = Plane();
ASSERT_FALSE(t.haveFiniteBounds());
}

View File

@@ -9,6 +9,8 @@
#include <shape.h>
#include <testshape.h>
#include <matrix.h>
#include <group.h>
#include <sphere.h>
#include <transformation.h>
#include <gtest/gtest.h>
@@ -95,4 +97,107 @@ TEST(ShapeTest, Computing_the_normal_on_a_tranformed_shape)
ASSERT_EQ(n, Vector(0, 0.97014, -0.24254));
set_equal_precision(FLT_EPSILON);
}
TEST(ShapeTest, A_shape_has_a_parent_attribute)
{
TestShape s = TestShape();
ASSERT_EQ(s.parent, nullptr);
}
TEST(ShapeTest, Converting_a_point_from_world_to_object_space)
{
Group g1 = Group();
g1.setTransform(rotationY(M_PI / 2));
Group g2 = Group();
g2.setTransform(scaling(2, 2, 2));
g1.addObject(&g2);
Sphere s = Sphere();
s.setTransform(translation(5, 0, 0));
g2.addObject(&s);
Tuple p = s.worldToObject(Point(-2, 0, -10));
ASSERT_EQ(p, Point(0, 0, -1));
}
TEST(ShapeTest, Converting_a_normal_form_object_to_world_space)
{
Group g1 = Group();
g1.setTransform(rotationY(M_PI / 2));
Group g2 = Group();
g2.setTransform(scaling(1, 2, 3));
g1.addObject(&g2);
Sphere s = Sphere();
s.setTransform(translation(5, 0, 0));
g2.addObject(&s);
Tuple p = s.normalToWorld(Vector(sqrt(3)/3, sqrt(3)/3, sqrt(3)/3));
/* Temporary lower the precision */
set_equal_precision(0.0001);
ASSERT_EQ(p, Vector(0.2857, 0.4286, -0.8571));
set_equal_precision(FLT_EPSILON);
}
TEST(ShapeTest, Finding_the_normal_on_a_child_object)
{
Group g1 = Group();
g1.setTransform(rotationY(M_PI / 2));
Group g2 = Group();
g2.setTransform(scaling(1, 2, 3));
g1.addObject(&g2);
Sphere s = Sphere();
s.setTransform(translation(5, 0, 0));
g2.addObject(&s);
Tuple p = s.normalAt(Point(1.7321, 1.1547, -5.5774));
/* Temporary lower the precision */
set_equal_precision(0.0001);
ASSERT_EQ(p, Vector(0.2857, 0.4286, -0.8571));
set_equal_precision(FLT_EPSILON);
}
TEST(ShapeTest, Test_the_bouding_box_of_the_test_shape)
{
TestShape t = TestShape();
BoundingBox b = BoundingBox(Point(-1, -1, -1), Point(1, 1, 1));
BoundingBox res = t.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}
TEST(ShapeTest, Test_the_bouding_box_of_the_scaled_shape)
{
TestShape t = TestShape();
t.setTransform(scaling(3, 3, 3));
BoundingBox b = BoundingBox(Point(-3, -3, -3), Point(3, 3, 3));
BoundingBox res = t.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}
TEST(ShapeTest, Test_the_bouding_box_of_the_translated_shape)
{
TestShape t = TestShape();
t.setTransform(translation(10, 0, 0));
BoundingBox b = BoundingBox(Point(9, -1, -1), Point(11, 1, 1));
BoundingBox res = t.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}

View File

@@ -207,4 +207,22 @@ TEST(SphereTest, A_helper_for_producing_a_sphere_with_a_glassy_material)
ASSERT_EQ(s.transformMatrix, Matrix4().identity());
ASSERT_EQ(s.material.transparency, 1.0);
ASSERT_EQ(s.material.refractiveIndex, 1.5);
}
TEST(SphereTest, The_bounding_box_of_a_sphere)
{
Sphere t = Sphere();
BoundingBox b = BoundingBox(Point(-1, -1, -1), Point(1, 1, 1));
BoundingBox res = t.getBounds();
ASSERT_EQ(res.min, b.min);
ASSERT_EQ(res.max, b.max);
}
TEST(SphereTest, A_sphere_have_finite_bounds)
{
Sphere t = Sphere();
ASSERT_TRUE(t.haveFiniteBounds());
}

26
tests/test_keys.hw3scene Normal file
View File

@@ -0,0 +1,26 @@
# line with a comment
camera 0 0 -1 0 0 0 0 1 0 50
point 0 4 0 .7 .2 .2
point 4 -4 0 .2 .7 .2
point -4 -4 0 .2 .2 .7
color 1 1 1
ambient 1 1 1
sphere 0 0 0 .3
color 1 0 0
diffuse 0.4 0.4 0.4
specular 0.4 0.4 0.4
shininess 1
emission 0 0 0
pushTransform
scale 0.1 0.1 0.1
rotate 0 1 0 45
popTransform
pushTransform
translate .4 0 0
sphere 0 0 0 .1
popTransform

133
tests/test_render.cpp Normal file
View File

@@ -0,0 +1,133 @@
/*
* DoRayMe - a quick and dirty Raytracer
* Render test for reflection in chapter 13.
*
* Created by Manoël Trapier
* Copyright (c) 2020 986-Studio.
*
*/
#include <world.h>
#include <light.h>
#include <sphere.h>
#include <plane.h>
#include <cube.h>
#include <cylinder.h>
#include <material.h>
#include <colour.h>
#include <canvas.h>
#include <camera.h>
#include <pattern.h>
#include <strippattern.h>
#include <gradientpattern.h>
#include <checkerspattern.h>
#include <ringpattern.h>
#include <transformation.h>
int main()
{
World w = World();
/* Add light */
Light light = Light(POINT_LIGHT, Point(-5, 5, -5), Colour(1, 1, 1));
w.addLight(&light);
/* ----------------------------- */
/* Environment */
Plane p1 = Plane();
p1.setTransform(translation(0, 0, 5) * rotationX(-M_PI/2));
p1.material.pattern = new CheckersPattern(Colour(1, 1, 1), Colour(0.1, 0.1, 0.1));
w.addObject(&p1);
Plane p2 = Plane();
p2.setTransform( translation(0, 0, -6) * rotationX(-M_PI/2) );
p2.material.pattern = new CheckersPattern(Colour(1, 1, 1), Colour(0.1, 0.1, 0.1));
w.addObject(&p2);
Plane p3 = Plane();
p3.setTransform(translation(-6, 0, 0) * rotationZ(M_PI/2) );
p3.material.pattern = new CheckersPattern(Colour(1, 1, 1), Colour(0.1, 0.1, 0.1));
w.addObject(&p3);
Plane p4 = Plane();
p3.setTransform(translation(6, 0, 0) * rotationZ(M_PI/2) );
p3.material.pattern = new CheckersPattern(Colour(1, 1, 1), Colour(0.1, 0.1, 0.1));
w.addObject(&p3);
Plane p5 = Plane();
p5.material.pattern = new CheckersPattern(Colour(1, 1, 1), Colour(0.1, 0.1, 0.1));
w.addObject(&p5);
/* ----------------------------- */
/* Big Sphere */
Sphere bigS = Sphere();
bigS.setTransform(translation(-0.5, 1, 2) * scaling(2, 2, 2));
bigS.material.colour = Colour(0, 0.4, 0.4);
bigS.material.shininess = 50;
bigS.material.specular = 1;
bigS.material.reflective = 0.8;
w.addObject(&bigS);
/* Small Sphere */
Sphere smaS = Sphere();
smaS.setTransform(translation(2.2, 1, -1));
smaS.material.colour = Colour(0.4, 0, 0.4);
smaS.material.shininess = 50;
smaS.material.specular = 1;
smaS.material.reflective = 0.2;
w.addObject(&smaS);
/* Cylinder */
Cylinder cyl = Cylinder();
cyl.setTransform(translation(-3.5, 0, 1));
cyl.isClosed = true;
cyl.minCap = -1;
cyl.maxCap = 3;
cyl.material.colour = Colour(0.4, 1, 0.4);
cyl.material.shininess = 20;
cyl.material.specular = 1;
cyl.material.reflective = 0.01;
cyl.material.pattern = new GradientPattern(Colour(0.4, 1, 0.4), Colour(1.0, 0.2, 1.0));
cyl.material.pattern->setTransform(scaling(1.1, 1.1, 1.1) * rotationY(M_PI / 2));
w.addObject(&cyl);
/* Cube */
Cube cub = Cube();
cub.setTransform(translation(0.6, 0.4, -1) * scaling(0.4, 0.4, 0.4) * rotationY(deg_to_rad(60)));
cub.material.colour = Colour(1, 0, 0.4);
cub.material.shininess = 50;
cub.material.specular = 1;
cub.material.reflective = 0.3;
w.addObject(&cub);
/* Glass cube */
Cube gcub = Cube();
gcub.setTransform(translation(-0.5, 0.6, -0.8) * scaling(0.6, 0.6, 0.6) * rotationY(deg_to_rad(40)));
gcub.dropShadow = false;
gcub.material.colour = Colour(0, 0.4, 0.1);
gcub.material.specular = 1;
gcub.material.shininess = 400;
gcub.material.ambient = 0.1;
gcub.material.diffuse = 0.3;
gcub.material.reflective = 1;
gcub.material.transparency = 1;
gcub.material.refractiveIndex = 0.3;
w.addObject(&gcub);
/* ----------------------------- */
/* Set the camera */
Camera camera = Camera(1280, 900, deg_to_rad(90));
camera.setTransform(viewTransform(Point(-2, 2.5, -3.5),
Point(2, 0, 3),
Vector(0, 1, 0)));
/* Now render it */
Canvas image = camera.render(w, 20);
image.SaveAsPNG("test_renter.png");
return 0;
}