From 8c3cbfaeec0f3055aba9bfe4b2bdfab7ff93bf1b Mon Sep 17 00:00:00 2001 From: Keuin Date: Tue, 12 Apr 2022 23:04:25 +0800 Subject: Refactor: move hitlist, object, sphere, viewport into single files. Add bias_ctx for setting sub-pixel sampling bias. --- CMakeLists.txt | 6 +- hitlist.h | 73 ++++++++++++++++++++ main_simple_scanner.cpp | 173 ++---------------------------------------------- object.h | 41 ++++++++++++ sphere.h | 81 +++++++++++++++++++++++ viewport.h | 87 ++++++++++++++++++++++++ 6 files changed, 291 insertions(+), 170 deletions(-) create mode 100644 hitlist.h create mode 100644 object.h create mode 100644 sphere.h create mode 100644 viewport.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bd2edf..78eeed9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,9 +12,9 @@ set(CMAKE_VERBOSE_MAKEFILE on) # main executable -add_executable(rt main.cpp vec.h bitmap.h ray.h bitfont.h) -add_executable(image_output main_image_output.cpp vec.h bitmap.h bitfont.h) -add_executable(simple_scanner main_simple_scanner.cpp vec.h bitmap.h ray.h timer.h bitfont.h) +add_executable(rt main.cpp vec.h bitmap.h ray.h bitfont.h hitlist.h object.h sphere.h viewport.h) +add_executable(image_output main_image_output.cpp vec.h bitmap.h bitfont.h hitlist.h object.h sphere.h viewport.h) +add_executable(simple_scanner main_simple_scanner.cpp vec.h bitmap.h ray.h timer.h bitfont.h hitlist.h object.h sphere.h viewport.h) # googletest diff --git a/hitlist.h b/hitlist.h new file mode 100644 index 0000000..18a8226 --- /dev/null +++ b/hitlist.h @@ -0,0 +1,73 @@ +// +// Created by Keuin on 2022/4/12. +// + +#ifndef RT_HITLIST_H +#define RT_HITLIST_H + +#include "viewport.h" +#include "timer.h" +#include "bitmap.h" +#include "ray.h" +#include "vec.h" +#include "object.h" +#include +#include +#include +#include +#include +#include + + +// A world +class hitlist { + std::vector> objects; + +public: + hitlist() = default; + + hitlist(hitlist &other) = delete; // do not copy the world + + // Add an object to the world. + void add_object(std::shared_ptr &&obj) { + objects.push_back(std::move(obj)); + } + + // Given a ray, compute the color. + pixel8b color(const ray3d &r) const { + // Detect hits + bool hit = false; + double hit_t = std::numeric_limits::infinity(); + std::shared_ptr hit_obj; + // Check the nearest object we hit + for (const auto &obj: objects) { + double t_; + if (obj->hit(r, t_, 0.0) && t_ < hit_t) { + hit = true; + hit_t = t_; + hit_obj = obj; + } + } + if (hit) { + // normal vector on hit point + const auto nv = hit_obj->normal_vector(r.at(hit_t)); +// return obj->color(); + // visualize normal vector at hit point + return pixel8b::from_normalized(nv); + } + + + // Does not hit anything. Get background color (infinity) + const auto u = (r.direction().y + 1.0) * 0.5; + return mix( + pixel8b::from_normalized(1.0, 1.0, 1.0), + pixel8b::from_normalized(0.5, 0.7, 1.0), + 1.0 - u, + u + ); + } + + +}; + +#endif //RT_HITLIST_H diff --git a/main_simple_scanner.cpp b/main_simple_scanner.cpp index 7769c0f..6dadf30 100644 --- a/main_simple_scanner.cpp +++ b/main_simple_scanner.cpp @@ -5,192 +5,31 @@ #include #include #include -#include #include #include #include "vec.h" -#include "ray.h" -#include "bitmap.h" #include "timer.h" +#include "viewport.h" +#include "hitlist.h" +#include "sphere.h" #define DEMO_BALL -class object { -public: - // Will the given ray hit. Returns time t if hits in range [t1, t2]. - virtual bool hit(const ray3d &r, double &t, double t1, double t2) const = 0; - - // With t2 = infinity - inline bool hit(const ray3d &r, double &t, double t1) const { - return hit(r, t, t1, std::numeric_limits::infinity()); - } - - // Given a point on the surface, returns the normalized outer normal vector on that point. - virtual vec3d normal_vector(const vec3d &where) const = 0; - - // object color, currently not parameterized - virtual pixel8b color() const = 0; - - // subclasses must have virtual destructors - virtual ~object() = default; -}; - -class sphere : public object { - vec3d center; - double radius; - -public: - sphere() = delete; - - sphere(const vec3d ¢er, double radius) : center(center), radius(radius) {} - - ~sphere() override = default; - - vec3d normal_vector(const vec3d &where) const override { - // We assume the point is on surface, speeding up the normalization - return (where - center) / radius; - } - - bool hit(const ray3d &r, double &t, double t1, double t2) const override { - // Ray: {Source, Direction, time} - // Sphere: {Center, radius} - // sphere hit formula: |Source + Direction * time - Center| = radius - // |(Sx + Dx * t - Cx, Sy + Dy * t - Cy, Sz + Dz * t - Cz)| = radius - - const auto c2s = r.source() - center; // center to source - // A = D dot D - const double a = r.direction().mod2(); - // H = (S - C) dot D - const auto h = dot(c2s, r.direction()); - // B = 2H ( not used in our optimized routine ) - // C = (S - C) dot (S - C) - radius^2 - const double c = c2s.mod2() - radius * radius; - // 4delta = H^2 - AC - // delta_q = H^2 - AC (quarter delta) - const double delta_q = h * h - a * c; - - bool hit = false; - if (delta_q >= 0) { - // return the root in range [t1, t2] - // t = ( -H +- sqrt{ delta_q } ) / A - double root; - root = (-h - sqrt(delta_q)) / a; - if (root >= t1 && root <= t2) { - hit = true; - t = root; - } else { - root = (-h + sqrt(delta_q)) / a; - if (root >= t1 && root <= t2) { - hit = true; - t = root; - } - } - } - return hit; - } - - pixel8b color() const override { - return pixel8b::from_normalized(1.0, 0.0, 0.0); - } -}; - -// A world -class hitlist { - std::vector> objects; - -public: - hitlist() = default; - - hitlist(hitlist &other) = delete; // do not copy the world - - // Add an object to the world. - void add_object(std::shared_ptr &&obj) { - objects.push_back(std::move(obj)); - } - - // Given a ray, compute the color. - pixel8b color(const ray3d &r) const { - // Detect hits - bool hit = false; - double hit_t = std::numeric_limits::infinity(); - std::shared_ptr hit_obj; - // Check the nearest object we hit - for (const auto &obj: objects) { - double t_; - if (obj->hit(r, t_, 0.0) && t_ < hit_t) { - hit = true; - hit_t = t_; - hit_obj = obj; - } - } - if (hit) { - // normal vector on hit point - const auto nv = hit_obj->normal_vector(r.at(hit_t)); -// return obj->color(); - // visualize normal vector at hit point - return pixel8b::from_normalized(nv); - } - - - // Does not hit anything. Get background color (infinity) - const auto u = (r.direction().y + 1.0) * 0.5; - return mix( - pixel8b::from_normalized(1.0, 1.0, 1.0), - pixel8b::from_normalized(0.5, 0.7, 1.0), - 1.0 - u, - u - ); - } - - -}; - -class viewport { - double half_width, half_height; // viewport size - vec3d center; // coordinate of the viewport center point - -public: - viewport() = delete; - - viewport(double width, double height, vec3d viewport_center) : - half_width(width / 2.0), half_height(height / 2.0), center(viewport_center) {} - - // Generate the image seen on given viewpoint. - bitmap8b render(const hitlist &world, vec3d viewpoint, uint16_t image_width, uint16_t image_height) const { - bitmap8b image{image_width, image_height}; - const auto r = center - viewpoint; - const int img_hw = image_width / 2, img_hh = image_height / 2; - // iterate over every pixel on the image - for (int j = -img_hh + 1; j <= img_hh; ++j) { // axis y, transformation is needed - for (int i = -img_hw; i < img_hw; ++i) { // axis x - const vec3d off{ - .x=1.0 * i / img_hw * half_width, - .y=1.0 * j / img_hh * half_height, - .z=0.0 - }; // offset on screen plane - const auto dir = r + off; // direction vector from camera to current pixel on screen - const ray3d ray{viewpoint, dir}; // from camera to pixel (on the viewport) - const auto pixel = world.color(ray); - image.set(i + img_hw, -j + img_hh, pixel); - } - } - return image; - } -}; - void generate_image(uint16_t image_width, uint16_t image_height, double viewport_width, double focal_length, double sphere_z, double sphere_r, const std::string &caption = "", unsigned caption_scale = 1) { double r = 1.0 * image_width / image_height; viewport vp{viewport_width, viewport_width / r, vec3d{0, 0, -focal_length}}; hitlist world; + bias_ctx bias{false, 0}; world.add_object(std::make_shared( vec3d{0, -100.5, -1}, 100)); // the earth world.add_object(std::make_shared(vec3d{0, 0, sphere_z}, sphere_r)); timer tm; tm.start_measure(); - auto image = vp.render(world, vec3d::zero(), image_width, image_height); // camera position as the coordinate origin + auto image = vp.render(world, vec3d::zero(), + image_width, image_height, bias); // camera position as the coordinate origin tm.stop_measure(); if (!caption.empty()) { image.print(caption, diff --git a/object.h b/object.h new file mode 100644 index 0000000..b6e2bbd --- /dev/null +++ b/object.h @@ -0,0 +1,41 @@ +// +// Created by Keuin on 2022/4/12. +// + +#ifndef RT_OBJECT_H +#define RT_OBJECT_H + +#include "hitlist.h" +#include "viewport.h" +#include "timer.h" +#include "bitmap.h" +#include "ray.h" +#include "vec.h" +#include +#include +#include +#include +#include +#include + +class object { +public: + // Will the given ray hit. Returns time t if hits in range [t1, t2]. + virtual bool hit(const ray3d &r, double &t, double t1, double t2) const = 0; + + // With t2 = infinity + inline bool hit(const ray3d &r, double &t, double t1) const { + return hit(r, t, t1, std::numeric_limits::infinity()); + } + + // Given a point on the surface, returns the normalized outer normal vector on that point. + virtual vec3d normal_vector(const vec3d &where) const = 0; + + // object color, currently not parameterized + virtual pixel8b color() const = 0; + + // subclasses must have virtual destructors + virtual ~object() = default; +}; + +#endif //RT_OBJECT_H diff --git a/sphere.h b/sphere.h new file mode 100644 index 0000000..f604b74 --- /dev/null +++ b/sphere.h @@ -0,0 +1,81 @@ +// +// Created by Keuin on 2022/4/12. +// + +#ifndef RT_SPHERE_H +#define RT_SPHERE_H + +#include "object.h" +#include "hitlist.h" +#include "viewport.h" +#include "timer.h" +#include "bitmap.h" +#include "ray.h" +#include "vec.h" +#include +#include +#include +#include +#include +#include + +class sphere : public object { + vec3d center; + double radius; + +public: + sphere() = delete; + + sphere(const vec3d ¢er, double radius) : center(center), radius(radius) {} + + ~sphere() override = default; + + vec3d normal_vector(const vec3d &where) const override { + // We assume the point is on surface, speeding up the normalization + return (where - center) / radius; + } + + bool hit(const ray3d &r, double &t, double t1, double t2) const override { + // Ray: {Source, Direction, time} + // Sphere: {Center, radius} + // sphere hit formula: |Source + Direction * time - Center| = radius + // |(Sx + Dx * t - Cx, Sy + Dy * t - Cy, Sz + Dz * t - Cz)| = radius + + const auto c2s = r.source() - center; // center to source + // A = D dot D + const double a = r.direction().mod2(); + // H = (S - C) dot D + const auto h = dot(c2s, r.direction()); + // B = 2H ( not used in our optimized routine ) + // C = (S - C) dot (S - C) - radius^2 + const double c = c2s.mod2() - radius * radius; + // 4delta = H^2 - AC + // delta_q = H^2 - AC (quarter delta) + const double delta_q = h * h - a * c; + + bool hit = false; + if (delta_q >= 0) { + // return the root in range [t1, t2] + // t = ( -H +- sqrt{ delta_q } ) / A + double root; + root = (-h - sqrt(delta_q)) / a; + if (root >= t1 && root <= t2) { + hit = true; + t = root; + } else { + root = (-h + sqrt(delta_q)) / a; + if (root >= t1 && root <= t2) { + hit = true; + t = root; + } + } + } + return hit; + } + + pixel8b color() const override { + return pixel8b::from_normalized(1.0, 0.0, 0.0); + } +}; + +#endif //RT_SPHERE_H diff --git a/viewport.h b/viewport.h new file mode 100644 index 0000000..41c86a2 --- /dev/null +++ b/viewport.h @@ -0,0 +1,87 @@ +// +// Created by Keuin on 2022/4/12. +// + +#ifndef RT_VIEWPORT_H +#define RT_VIEWPORT_H + +#include "timer.h" +#include "bitmap.h" +#include "ray.h" +#include "vec.h" +#include "hitlist.h" +#include +#include +#include +#include +#include +#include +#include + +// bias context, used for placing sub-pixels +class bias_ctx { + bool enabled; // put all together, eliminating a virtual function call + std::mt19937_64 mt; + std::uniform_real_distribution uni{0.0, 1.0}; + +public: + bias_ctx(bool enabled, uint64_t seed = 0UL) : enabled(enabled), mt(std::mt19937_64{seed}) {} + + void operator()(double &bx, double &by) { + if (enabled) { + bx = uni(mt); + by = uni(mt); + } else { + bx = 0.0; + by = 0.0; + } + } +}; + +class viewport { + const double half_width, half_height; // viewport size + const vec3d center; // coordinate of the viewport center point + +public: + viewport() = delete; + + viewport(double width, double height, vec3d viewport_center) : + half_width(width / 2.0), half_height(height / 2.0), center(viewport_center) {} + + /** + * Generate the image seen on given viewpoint. + * @param bx bias on x axis (0.0 <= bx < 1.0) + * @param by bias on y axis (0.0 <= by < 1.0) + * @return + */ + bitmap8b render(const hitlist &world, vec3d viewpoint, + uint16_t image_width, uint16_t image_height, + bias_ctx &bias) const { + bitmap8b image{image_width, image_height}; + double bx, by; + const auto r = center - viewpoint; + const int img_hw = image_width / 2, img_hh = image_height / 2; + // iterate over every pixel on the image + for (int j = -img_hh + 1; j <= img_hh; ++j) { // axis y, transformation is needed + for (int i = -img_hw; i < img_hw; ++i) { // axis x + bias(bx, by); // get a random bias (bx, by) for sub-pixel sampling + assert(0 <= bx); + assert(0 <= by); + assert(bx < 1.0); + assert(by < 1.0); + const vec3d off{ + .x=1.0 * i / img_hw * half_width + bx, + .y=1.0 * j / img_hh * half_height + by, + .z=0.0 + }; // offset on screen plane + const auto dir = r + off; // direction vector from camera to current pixel on screen + const ray3d ray{viewpoint, dir}; // from camera to pixel (on the viewport) + const auto pixel = world.color(ray); + image.set(i + img_hw, -j + img_hh, pixel); + } + } + return image; + } +}; + +#endif //RT_VIEWPORT_H -- cgit v1.2.3