From 7c794f049629b8264151ea4933ffc5cb48147ffe Mon Sep 17 00:00:00 2001 From: Godzil Date: Mon, 24 Feb 2020 09:25:52 +0000 Subject: [PATCH] Working on groups --- source/include/group.h | 38 +++++++++++++++++++ source/include/shape.h | 15 +++++++- source/include/sphere.h | 2 +- source/shapes/group.cpp | 73 ++++++++++++++++++++++++++++++++++++ source/shapes/shape.cpp | 23 +++++++++--- tests/CMakeLists.txt | 2 +- tests/group_test.cpp | 82 +++++++++++++++++++++++++++++++++++++++++ tests/shape_test.cpp | 46 +++++++++++++++++++++++ 8 files changed, 272 insertions(+), 9 deletions(-) create mode 100644 source/include/group.h create mode 100644 source/shapes/group.cpp create mode 100644 tests/group_test.cpp diff --git a/source/include/group.h b/source/include/group.h new file mode 100644 index 0000000..30bbeb7 --- /dev/null +++ b/source/include/group.h @@ -0,0 +1,38 @@ +/* + * 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 + +class Group : public Shape +{ +private: + uint32_t allocatedObjectCount; + Shape* *objectList; + uint32_t objectCount; + +protected: + Intersect localIntersect(Ray r); + Tuple localNormalAt(Tuple point); + + + +public: + bool isEmpty(); + + void addObject(Shape *s); + Shape *operator[](const int p) { return this->objectList[p]; } + + Intersect intersect(Ray r); + + Group(); +}; + +#endif /* DORAYME_GROUP_H */ diff --git a/source/include/shape.h b/source/include/shape.h index e0010aa..3c19999 100644 --- a/source/include/shape.h +++ b/source/include/shape.h @@ -25,6 +25,8 @@ enum ShapeType SHAPE_CUBE, SHAPE_CYLINDER, SHAPE_CONE, + SHAPE_GROUP, + }; /* Base class for all object that can be presented in the world */ @@ -32,16 +34,19 @@ 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); @@ -49,6 +54,12 @@ public: Intersect intersect(Ray r); Tuple normalAt(Tuple point); + //virtual Bounds getBounds(); + + void updateTransform(); + Tuple worldToObject(Tuple point) { return this->inverseTransform * point; }; + Tuple normalToWorld(Tuple normalVector) { return (this->transposedInverseTransform * normalVector).normalise(); }; + 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); }; diff --git a/source/include/sphere.h b/source/include/sphere.h index 3d0d5eb..3bc3f62 100644 --- a/source/include/sphere.h +++ b/source/include/sphere.h @@ -15,7 +15,7 @@ class Sphere : public Shape { -private: +protected: Intersect localIntersect(Ray r); Tuple localNormalAt(Tuple point); diff --git a/source/shapes/group.cpp b/source/shapes/group.cpp new file mode 100644 index 0000000..7aeff43 --- /dev/null +++ b/source/shapes/group.cpp @@ -0,0 +1,73 @@ +/* + * DoRayMe - a quick and dirty Raytracer + * Group implementation + * + * Created by Manoël Trapier + * Copyright (c) 2020 986-Studio. + * + */ +#include +#include +#include +#include +#include + +#define MIN_ALLOC (2) + +Group::Group() : Shape(SHAPE_GROUP) +{ + this->allocatedObjectCount = MIN_ALLOC; + this->objectList = (Shape **)calloc(sizeof(Shape *), MIN_ALLOC); + this->objectCount = 0; +} + +Intersect Group::intersect(Ray r) +{ + Intersect ret; + int i, j; + if (this->objectCount > 0) + { + 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]); + } + } + } + } + + return ret; +} + +Intersect Group::localIntersect(Ray r) +{ + return Intersect(); +} + +Tuple Group::localNormalAt(Tuple point) +{ + return Vector(1, 0, 0); +} + +void Group::addObject(Shape *s) +{ + 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; +} + +bool Group::isEmpty() +{ + return (this->objectCount == 0); +} \ No newline at end of file diff --git a/source/shapes/shape.cpp b/source/shapes/shape.cpp index 2744408..f12f326 100644 --- a/source/shapes/shape.cpp +++ b/source/shapes/shape.cpp @@ -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) @@ -32,7 +33,7 @@ Tuple Shape::normalAt(Tuple point) Tuple local_normal = this->localNormalAt(local_point); - Tuple world_normal = this->inverseTransform.transpose() * local_normal; + Tuple world_normal = this->transposedInverseTransform * local_normal; /* 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; @@ -40,8 +41,20 @@ Tuple Shape::normalAt(Tuple point) return world_normal.normalise(); } +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(); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1c9a53b..2586846 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,7 +5,7 @@ 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) add_executable(testMyRays) target_include_directories(testMyRays PUBLIC ${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) diff --git a/tests/group_test.cpp b/tests/group_test.cpp new file mode 100644 index 0000000..bf8fa3b --- /dev/null +++ b/tests/group_test.cpp @@ -0,0 +1,82 @@ +/* + * DoRayMe - a quick and dirty Raytracer + * Group unit tests + * + * Created by Manoël Trapier + * Copyright (c) 2020 986-Studio. + * + */ +#include +#include +#include +#include +#include + +#include +#include + +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_transformer_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); +} \ No newline at end of file diff --git a/tests/shape_test.cpp b/tests/shape_test.cpp index 7a516ad..1a2a7af 100644 --- a/tests/shape_test.cpp +++ b/tests/shape_test.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -94,5 +96,49 @@ 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(TestShape, 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(TestShape, 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(Point(sqrt(3)/3, sqrt(3)/3, sqrt(3)/3)); + + /* Temporary lower the precision */ + set_equal_precision(0.0001); + + ASSERT_EQ(p, Point(0.2857, 0.4286, -0.8571)); + set_equal_precision(FLT_EPSILON); } \ No newline at end of file