diff --git a/apps/raydiance.cpp b/apps/raydiance.cpp index 232aa3e..0f5ae46 100644 --- a/apps/raydiance.cpp +++ b/apps/raydiance.cpp @@ -18,12 +18,13 @@ int main() { auto matGround = std::make_shared(colour(0.8, 0.8, 0.0)); auto matCentre = std::make_shared(colour(0.7, 0.3, 0.3)); - auto matLeft = std::make_shared(colour(0.8, 0.8, 0.8), 0.3); - auto matRight = std::make_shared(colour(0.8, 0.6, 0.2), 1.0); + auto matLeft = std::make_shared(1.5); + auto matRight = std::make_shared(colour(0.8, 0.6, 0.2), 0.4); world.add(std::make_shared(point3(0.0, -100.5, -1.0), 100.0, matGround)); world.add(std::make_shared(point3(0.0, 0.0, -1.0), 0.5, matCentre)); world.add(std::make_shared(point3(-1.0, 0.0, -1.0), 0.5, matLeft)); + world.add(std::make_shared(point3(-1.0, 0.0, -1.0), -0.4, matLeft));// Negative radius trick for a hollow sphere world.add(std::make_shared(point3(1.0, 0.0, -1.0), 0.5, matRight)); camera cam; diff --git a/include/raydiance/material.h b/include/raydiance/material.h index e1f057b..dc5fd31 100644 --- a/include/raydiance/material.h +++ b/include/raydiance/material.h @@ -13,11 +13,11 @@ class material { virtual bool scatter(const ray &rIn, const intersection &i, colour &attenuation, ray &scattered) const = 0; }; +// Aka matte class lambertian : public material { public: explicit lambertian(const colour &a) : albedo(a) {} - // Always scatters bool scatter(const ray &rIn, const intersection &i, colour &attenuation, ray &scattered) const override; private: @@ -26,11 +26,24 @@ class lambertian : public material { class metal : public material { public: - metal(const colour &a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {} + metal(const colour &a, double f) : albedo(a), fuzz(f < 1.0 ? f : 1.0) {} bool scatter(const ray &rIn, const intersection &i, colour &attenuation, ray &scattered) const override; private: colour albedo; - double fuzz; + double fuzz;// 0.0 = no fuzz, 1.0 = maximum fuzz +}; + +// E.g. glass, diamond, water +class dielectric : public material { +public: + explicit dielectric(double ri) : refractionIndex(ri) {} + + bool scatter(const ray &rIn, const intersection &i, colour &attenuation, ray &scattered) const override; + +private: + double refractionIndex; + + static double reflectance(double cosine, double refractionIndex); }; \ No newline at end of file diff --git a/include/raydiance/scene.h b/include/raydiance/scene.h index 4e5a79c..663771a 100644 --- a/include/raydiance/scene.h +++ b/include/raydiance/scene.h @@ -14,6 +14,5 @@ class scene : public object { void clear() { objects.clear(); } void add(const std::shared_ptr &object) { objects.emplace_back(object); } - [[nodiscard]] bool isHit(const ray &r, interval tRange, intersection &i) const override; }; \ No newline at end of file diff --git a/include/raydiance/vec3.h b/include/raydiance/vec3.h index 7ba9186..a042410 100644 --- a/include/raydiance/vec3.h +++ b/include/raydiance/vec3.h @@ -116,15 +116,15 @@ inline vec3 randomUnitVector() { return unitVector(vec3::randomInUnitSphere()); } -inline vec3 randomVecOnHemisphere(const vec3 &normal) { - vec3 onUnitSphere = randomUnitVector(); - if (dot(onUnitSphere, normal) > 0.0) {// On the same hemisphere as the normal - return onUnitSphere; - } else { - return -onUnitSphere; - } -} - inline vec3 reflect(const vec3 &v, const vec3 &n) { return v - 2 * dot(v, n) * n; +} + +inline vec3 refract(const vec3 &v, const vec3 &n, double indexRatio) { + auto cosTheta = std::fmin(dot(-v, n), 1.0); + + vec3 rayOutPerp = indexRatio * (v + cosTheta * n); + vec3 rayOutParallel = -std::sqrt(std::fabs(1.0 - rayOutPerp.lengthSquared())) * n; + + return rayOutPerp + rayOutParallel; } \ No newline at end of file diff --git a/src/material.cpp b/src/material.cpp index afdc33d..31c5d10 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1,6 +1,7 @@ #include "raydiance/material.h" bool lambertian::scatter(const ray &rIn, const intersection &i, colour &attenuation, ray &scattered) const { + // Scatter in a random direction vec3 scatterDirection{i.normal + randomUnitVector()}; // Catch degenerate scatter direction @@ -10,12 +11,59 @@ bool lambertian::scatter(const ray &rIn, const intersection &i, colour &attenuat scattered = ray{i.p, scatterDirection}; attenuation = albedo; + + // Always scatters return true; } + bool metal::scatter(const ray &rIn, const intersection &i, colour &attenuation, ray &scattered) const { + // Calculate reflected ray vec3 reflected{reflect(unitVector(rIn.direction()), i.normal)}; + + // Add random fuzz to the reflected ray scattered = ray{i.p, reflected + fuzz * randomUnitVector()}; + attenuation = albedo; + // Only scatters if the reflected ray is in the same hemisphere as the normal return dot(scattered.direction(), i.normal) > 0; } + +bool dielectric::scatter(const ray &rIn, const intersection &i, colour &attenuation, ray &scattered) const { + // All colours are equally attenuated + attenuation = colour{1.0, 1.0, 1.0}; + + double refractionRatio{i.frontFace ? (1.0 / refractionIndex) : refractionIndex}; + + vec3 unitDirection{unitVector(rIn.direction())}; + + // Get cosine of the angle between the incident ray and the normal + double cosTheta{fmin(dot(-unitDirection, i.normal), 1.0)}; + + // Get sine of the same angle using trigonometric identity + double sinTheta{std::sqrt(1.0 - cosTheta * cosTheta)}; + + // If total internal reflection occurs, refraction isn't possible + bool cannotRefract{refractionRatio * sinTheta > 1.0}; + + vec3 direction; + + if (cannotRefract || reflectance(cosTheta, refractionRatio) > randomDouble()) { + direction = reflect(unitDirection, i.normal); + } else { + direction = refract(unitDirection, i.normal, refractionRatio); + } + + scattered = ray{i.p, direction}; + + // Always scatters + return true; +} + +double dielectric::reflectance(double cosine, double refractionIndex) { + // Use Schlick's approximation for reflectance + auto r0{(1 - refractionIndex) / (1 + refractionIndex)}; + r0 *= r0; + + return r0 + (1 - r0) * std::pow((1 - cosine), 5); +} diff --git a/tests/object_test.cpp b/tests/object_test.cpp index e85acd2..c4eab8a 100644 --- a/tests/object_test.cpp +++ b/tests/object_test.cpp @@ -6,7 +6,6 @@ TEST(IntersectionTest, SetFaceNormal) { vec3 on1{0, 0, -1}; intersection i1; i1.setFaceNormal(r1, on1); - EXPECT_TRUE(i1.frontFace); EXPECT_DOUBLE_EQ(i1.normal.x(), 0); EXPECT_DOUBLE_EQ(i1.normal.y(), 0); @@ -16,7 +15,6 @@ TEST(IntersectionTest, SetFaceNormal) { vec3 on2{0, 0, -1}; intersection i2; i2.setFaceNormal(r2, on2); - EXPECT_FALSE(i2.frontFace); EXPECT_DOUBLE_EQ(i2.normal.x(), 0); EXPECT_DOUBLE_EQ(i2.normal.y(), 0); diff --git a/tests/scene_test.cpp b/tests/scene_test.cpp index dfb2716..3b6bf0a 100644 --- a/tests/scene_test.cpp +++ b/tests/scene_test.cpp @@ -36,14 +36,13 @@ TEST(SceneTest, IsHit) { EXPECT_FALSE(s.isHit(r, tRange, i)); auto m = std::make_shared(colour{0.0, 0.0, 0.0}); - s.add(std::make_shared(point3{0, 0, 1}, 0.5, m)); + s.add(std::make_shared(point3{0, 0, 1}, 0.5, m)); EXPECT_FALSE(s.isHit(r, tRange, i)); s.add(std::make_shared(point3{0, 0, -1}, 0.5, m)); s.add(std::make_shared(point3{0, 0, -0.5}, 100, m)); s.add(std::make_shared(point3{0, 0, -3}, 1, m)); - EXPECT_TRUE(s.isHit(r, tRange, i)); EXPECT_DOUBLE_EQ(i.t, 0.5); EXPECT_DOUBLE_EQ(i.p.x(), 0); diff --git a/tests/vec3_test.cpp b/tests/vec3_test.cpp index e5ed260..acd2f8e 100644 --- a/tests/vec3_test.cpp +++ b/tests/vec3_test.cpp @@ -201,4 +201,22 @@ TEST(Vec3HelperTest, Reflect) { EXPECT_DOUBLE_EQ(r2.x(), 0.5); EXPECT_DOUBLE_EQ(r2.y(), 0.6); EXPECT_DOUBLE_EQ(r2.z(), 0.7); +} + +TEST(Vec3HelperTest, Refract) { + vec3 v1(0.5, 0.6, 0.7); + vec3 n1(0.1, -0.2, 0.5); + double ir1 = 1.5; + vec3 r1 = refract(v1, n1, ir1); + EXPECT_DOUBLE_EQ(r1.x(), 0.59959704801067448); + EXPECT_DOUBLE_EQ(r1.y(), 1.2008059039786507); + EXPECT_DOUBLE_EQ(r1.z(), 0.29798524005337268); + + vec3 v2(0.5, 0.6, 0.7); + vec3 n2(0.8, 0.9, -1.4); + double ir2 = 0.9; + vec3 r2 = refract(v2, n2, ir2); + EXPECT_DOUBLE_EQ(r2.x(), 0.21690213899307964); + EXPECT_DOUBLE_EQ(r2.y(), 0.27776490636721457); + EXPECT_DOUBLE_EQ(r2.z(), 1.0379212567621106); } \ No newline at end of file