├── .gitignore ├── CMakeLists.txt ├── README.md ├── TODO.md ├── include ├── acceleration │ ├── aabb.h │ └── bvh.h ├── base │ ├── camera.h │ ├── hittable.h │ ├── material.h │ └── scene.h ├── math │ ├── interval.h │ ├── ray3d.h │ └── vec3d.h ├── shapes │ ├── box.h │ ├── parallelogram.h │ ├── shapes.h │ └── sphere.h └── util │ ├── image.h │ ├── progressbar.h │ ├── rand_util.h │ ├── rgb.h │ └── time_util.h ├── profiling_and_analysis ├── v1_profile.pdf ├── v1_rtow_final_image_runtime_analysis.png └── v1_with_old_bvh.pdf ├── rendered_images ├── christmas_tree_of_spheres.png ├── cornell_box_1.png ├── empty_cornell_box.png ├── millions_of_spheres.png ├── millions_of_spheres_with_lights.png ├── raining_on_the_dance_floor.png ├── raining_on_the_dance_floor_16_9_aspect_ratio.png ├── rtow_final_lights_with_tone_mapping.png ├── rtow_final_lights_without_tone_mapping.png └── rtweekend_final_image.png └── src └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | *.dbg 10 | *.rel 11 | 12 | # Precompiled Headers 13 | *.gch 14 | *.pch 15 | 16 | # Compiled Dynamic libraries 17 | *.so 18 | *.dylib 19 | *.dll 20 | 21 | # Fortran module files 22 | *.mod 23 | *.smod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | project(cpp_raytracer 3 | LANGUAGES CXX) 4 | 5 | # Notify user if their build generator is multi-configuration, because this prevents 6 | # us from setting a default `CMAKE_BUILD_TYPE` (as multi-config generators ignore 7 | # the `CMAKE_BUILD_TYPE` variable). 8 | if(CMAKE_CONFIGURATION_TYPES) 9 | message(STATUS "\ 10 | NOTE: You are on a multi-configuration generator (VSCode, XCode, etc). This means\n\ 11 | the build type (Debug, Release, etc) should be set from within the IDE itself, because\n\ 12 | multi-configuration generators ignore the CMAKE_BUILD_TYPE variable)." 13 | ) 14 | endif() 15 | 16 | # Set the default build type to Release if `CMAKE_BUILD_TYPE` has not been set previously, 17 | # and if the build generator is single-configuration (because if it is multi-config, then 18 | # it ignores `CMAKE_BUILD_TYPE`). From https://www.kitware.com/cmake-and-the-default-build-type/. 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | message(STATUS "NOTE: Setting build type to 'Release' as none was specified.") 21 | set(CMAKE_BUILD_TYPE "Release" CACHE 22 | STRING "Choose the type of build." FORCE) 23 | # Set the possible values of build type for cmakde-gui 24 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 25 | "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 26 | endif() 27 | 28 | set(CPP_RAYTRACER_SOURCES 29 | src/main.cpp) 30 | 31 | add_executable(cpp_raytracer ${CPP_RAYTRACER_SOURCES}) 32 | 33 | # Require C++20 for `cpp_raytracer` (and because I use PUBLIC, also for all targets that link to 34 | # `cpp_raytracer`), and also avoid extensions being added. 35 | target_compile_features(cpp_raytracer PUBLIC cxx_std_20) 36 | set_target_properties(cpp_raytracer PROPERTIES CXX_EXTENSIONS OFF) 37 | 38 | target_include_directories(cpp_raytracer PRIVATE ${CMAKE_SOURCE_DIR}/include) 39 | 40 | # The following is "the modern way to add OpenMP to a target" (cpp_raytracer is the target here). 41 | # (See https://cliutils.gitlab.io/modern-cmake/chapters/packages/OpenMP.html). 42 | find_package(OpenMP) 43 | if(OpenMP_CXX_FOUND) 44 | target_link_libraries(cpp_raytracer PUBLIC OpenMP::OpenMP_CXX) 45 | else() 46 | message(STATUS "Could not find OpenMP (libomp); compiling without it") 47 | endif() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpp_raytracer 2 | A simple raytracer (pathtracer), coded in modern C++, following various tutorials: 3 | - [_Introduction to Raytracing: A Simple Method for Creating 3D Images_](https://scratchapixel.com/lessons/3d-basic-rendering/introduction-to-ray-tracing/how-does-it-work.html) 4 | - The [_Ray Tracing in One Weekend_ series](https://raytracing.github.io/) 5 | - [_Physically Based Rendering: From Theory to Implementation_, 4th edition](https://pbr-book.org/4ed/contents) 6 | - [_Computer Graphics from Scratch_ by Gabriel Gambetta](https://gabrielgambetta.com/computer-graphics-from-scratch/) 7 | 8 | Some images rendered by this raytracer: 9 | ![image](rendered_images/rtweekend_final_image.png) 10 | ![image](rendered_images/rtow_final_lights_with_tone_mapping.png) 11 | ![image](rendered_images/millions_of_spheres_with_lights.png) 12 | 13 | 14 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ### Design 4 | - [ ] Consider major design overhaul of `Hittable` 5 | - Ideas: Let the user forgo using `std::make_shared<>` everywhere by making `Hittable` itself a tagged pointer (tagged to handle polymorphism); this is what is done in PBR. 6 | 7 | ### Features 8 | - [ ] Spatial and surface textures 9 | - [ ] Instancing (This enables easy object translation, rotation, and scaling) 10 | - [ ] Volumes (smoke, fog, etc) 11 | - Idea: Procedurally generate volumes, so that they have a more natural shape. Look into Perlin noise. 12 | 13 | ### Optimizations 14 | - [X] Optimize BVH by compressing the binary tree to an array. This improves cache locality. 15 | - Don't forget to build the BVH over a copy of the primitives, as we already do. This is what they do in PBR. 16 | - [X] Use C++20 concepts to guarantee that `Camera::render()` and other related functions use compile-time polymorphism rather than runtime polymorphism and dynamic dispatch to handle calls to `hit_by`, when possible. 17 | - [ ] Parallelize BVH build 18 | 19 | 20 | ### Documentation 21 | - [x] Add comments to the `aabb` fields of Sphere and Parallelogram. 22 | -------------------------------------------------------------------------------- /include/acceleration/aabb.h: -------------------------------------------------------------------------------- 1 | #ifndef AABB_H 2 | #define AABB_H 3 | 4 | #include "math/ray3d.h" 5 | #include "math/interval.h" 6 | 7 | class AABB { 8 | /* Observe that an n-dimensional axis-aligned bounding box is equivalent to the intersection of 9 | n axis-aligned intervals. Thus, a 3D axis-aligned bounding box is equivalent to the intersection 10 | of three intervals for the x/y/z-coordinates, which are stored in`x`, `y`, and `z` respectively. 11 | In raytracing, these axis-aligned intervals are called "slabs", and so this strategy of 12 | representing n-dimensional AABB's with n slabs is called the "slab method". */ 13 | Interval x, y, z; 14 | 15 | AABB(const Interval &x_, const Interval &y_, const Interval &z_) : x{x_}, y{y_}, z{z_} {} 16 | 17 | public: 18 | 19 | /* Returns the `Interval` corresponding to the axis specified by `axis`. Specifically, 20 | returns the x-, y-, and z- interval when `axis` is 0, 1, or 2, respectively. */ 21 | auto& operator[] (size_t axis) {return (axis == 0 ? x : (axis == 1 ? y : z));} 22 | /* Returns the `Interval` corresponding to the axis specified by `axis`. Specifically, 23 | returns the x-, y-, and z- interval when `axis` is 0, 1, or 2, respectively. */ 24 | const auto& operator[] (size_t axis) const {return (axis == 0 ? x : (axis == 1 ? y : z));} 25 | 26 | /* Returns the centroid of this `AABB`. */ 27 | auto centroid() const {return Point3D{x.midpoint(), y.midpoint(), z.midpoint()};} 28 | /* Returns the surface area of this `AABB`. */ 29 | auto surface_area() const { 30 | return 2 * (x.size() * x.size() + y.size() * y.size() + z.size() * z.size()); 31 | } 32 | /* Returns the volume of this `AABB`. */ 33 | auto volume() const { 34 | return x.size() * y.size() * z.size(); 35 | } 36 | 37 | /* Returns `true` if the ray `ray` intersects this `AABB` in the time range specified by 38 | `ray_times`. */ 39 | bool is_hit_by(const Ray3D &ray, Interval ray_times) const { 40 | /* Use the slab method to check ray-AABB intersections. Specifically, remember that a 41 | 3-dimensional AABB is equivalent to the intersection of an x-interval, a y-interval, and 42 | a z-interval. This means that a point (x0, y0, z0) is inside the AABB if and only if 43 | x0 is in (x.min, x.max), y0 is in (y.min, y.max), and z0 is in (z.min, z.max). Now, 44 | checking if a ray intersects with this AABB is equivalent to checking if there exists some 45 | `t` such that ray(t) = ray.origin + t * ray.dir has each coordinate in its corresponding 46 | interval. This, in turn, holds iff the intersection of the three intervals of times where 47 | the x-, y-, and z-coordinates of ray(t) are each in their corresponding intervals is 48 | non-empty. Note that we know that the set of times where each coordinate is in its 49 | corresponding interval forms a single interval, because a ray is defined by a linear 50 | equation in its coordinates (we will show how exactly the compute the time ranges for each 51 | coordinate below). Finally, it remains to check if the intersection of the three time 52 | intervals intersects with the interval `ray_times`; if it does, then the original ray `ray` 53 | does hit this `AABB` in the Interval `ray_times`, and otherwise, it does not. */ 54 | 55 | /* Note: We just copy-paste the code for each coordinate instead of using a loop because 56 | - I don't represent `Vec3D`s as arrays of `double`s. Instead, I represent them as three 57 | separate `double` variables. I'm afraid that writing a `double operator[]` would 58 | cause branching overhead, but I would need one to be able to simplify this code by using 59 | a `for`-loop. 60 | - This is a function that will take up a large portion of the runtime, because it will 61 | eventually be the main method used to determine ray-scene collisions. Thus, we'd want to 62 | have any loop we wrote inlined by the compiler (especially since it only has 3 63 | iterations). This approach pretty much inlines it automatically for us, so we know we 64 | don't have to worry about it. 65 | */ 66 | 67 | /* Compute the time range where the x-coordinate of `ray` is in the interval `x`. 68 | Because ray(t).x = ray.origin.x + t * ray.dir.x by the definition of a ray, we know that 69 | ray(t).x is inside the Interval `x` when t is between (x.min - ray.origin.x) / ray.dir.x 70 | and (x.max - ray.origin.x) / ray.dir.x. */ 71 | auto inverse_ray_dir = 1 / ray.dir.x; /* Only compute this once */ 72 | auto t0 = (x.min - ray.origin.x) * inverse_ray_dir; 73 | auto t1 = (x.max - ray.origin.x) * inverse_ray_dir; 74 | /* Make sure that `t0` < `t1`. This is necessary, because while we already know that 75 | `x.max` > `x.min`, whether or not `t0` is less than `t1` still depends on the sign of 76 | `inverse_ray_dir`. Specifically, when `inverse_ray_dir` is negative, then `t0` will 77 | actually be greater than `t1`, so we `std::swap` them then. */ 78 | if (inverse_ray_dir < 0) {std::swap(t0, t1);} 79 | /* Update `ray_times` to equal its intersection with (t0, t1). This is a concise way of 80 | finding the intersection of the time intervals we currently have calculated with the 81 | original desired time interval `ray_times`. Note that the reason we pass `ray_times` by 82 | copy is because we modify `ray_times` when finding its intersection with the time intervals 83 | we compute fo each coordinate. */ 84 | if (t0 > ray_times.min) {ray_times.min = t0;} 85 | if (t1 < ray_times.max) {ray_times.max = t1;} 86 | /* If `ray_times` is the empty interval, then there is no time t in `ray_times` where 87 | the x-coordinate is in the range specified by `x`. Thus, the ray `ray` does not intersect 88 | this `AABB` in the original time interval `ray_times`, and so we immediately return 89 | `false`. */ 90 | if (ray_times.max <= ray_times.min) {return false;} 91 | 92 | /* Compute time range where the y-coordinate of `ray` is in the interval `y`. Identical 93 | process as for the x-coordinate above. */ 94 | inverse_ray_dir = 1 / ray.dir.y; 95 | t0 = (y.min - ray.origin.y) * inverse_ray_dir; 96 | t1 = (y.max - ray.origin.y) * inverse_ray_dir; 97 | if (inverse_ray_dir < 0) {std::swap(t0, t1);} 98 | if (t0 > ray_times.min) {ray_times.min = t0;} 99 | if (t1 < ray_times.max) {ray_times.max = t1;} 100 | /* If `ray_times` is the empty interval, then there is no time t in `ray_times` where 101 | the x-coordinate is in the range specified by `x` and the y-coordinate is in the range 102 | specified by `y`. Thus, the ray `ray` does not intersect this `AABB` in the original time 103 | interval `ray_times`, and so we immediately return `false`. */ 104 | if (ray_times.max <= ray_times.min) {return false;} 105 | 106 | /* Compute time range where the z-coordinate of `ray` is in the interval `z`. Identical 107 | process as for the x- and y-coordinates above. */ 108 | inverse_ray_dir = 1 / ray.dir.z; 109 | t0 = (z.min - ray.origin.z) * inverse_ray_dir; 110 | t1 = (z.max - ray.origin.z) * inverse_ray_dir; 111 | if (inverse_ray_dir < 0) {std::swap(t0, t1);} 112 | if (t0 > ray_times.min) {ray_times.min = t0;} 113 | if (t1 < ray_times.max) {ray_times.max = t1;} 114 | /* If `ray_times` is the empty interval, then there is no time t in `ray_times` where 115 | the x-coordinate is in the range specified by `x`, the y-coordinate is in the range 116 | specified by `y`, and the z-coordinate is in the range specified by `z`. Thus, the ray 117 | `ray` does not intersect this `AABB` in the original time interval `ray_times`, and so 118 | we immediately return `false`. */ 119 | if (ray_times.max <= ray_times.min) {return false;} 120 | 121 | /* Otherwise, if `ray_times` is non-empty, that means there does exist some time `t` 122 | in the original time range `ray_times` such that `ray(t)` is inside this `AABB`. Thus, 123 | we return `true` here. */ 124 | return true; 125 | } 126 | 127 | /* Returns `true` if the ray `ray` intersects this `AABB` in the time range specified by 128 | `ray_times`. This function also takes the precomputed values `inverse_ray_direction` 129 | (the vector with components equal to the reciprocals of the given ray `ray`'s direction), 130 | and `direction_is_negative` (where `direction_is_negative[i]` = whether or not `ray.dir[i]` 131 | is negative). */ 132 | bool is_hit_by_optimized( 133 | const Ray3D &ray, const Interval &ray_times, 134 | const Vec3D &inverse_ray_direction, 135 | const std::array &direction_is_negative 136 | ) const { 137 | 138 | /* Precomputing `direction_is_negative` lets us directly compute the minimum and 139 | maximum time of intersection for each axis, rather than having to compute both 140 | times of intersection and then comparing them to see which one is the minimum. */ 141 | auto x_tmin = (x[ direction_is_negative[0]] - ray.origin.x) * inverse_ray_direction.x; 142 | auto x_tmax = (x[!direction_is_negative[0]] - ray.origin.x) * inverse_ray_direction.x; 143 | auto y_tmin = (y[ direction_is_negative[1]] - ray.origin.y) * inverse_ray_direction.y; 144 | auto y_tmax = (y[!direction_is_negative[1]] - ray.origin.y) * inverse_ray_direction.y; 145 | 146 | /* If the x- and y- time intervals are disjoint, then the ray does not intersect 147 | this AABB, and so we return false. */ 148 | if (x_tmin > y_tmax || y_tmin > x_tmax) { 149 | return false; 150 | } 151 | 152 | /* Otherwise, we merge the intervals [x_tmin, x_tmax] and [y_tmin, y_tmax] into 153 | `x_tmin` and `x_tmax`. */ 154 | if (y_tmin > x_tmin) {x_tmin = y_tmin;} 155 | if (y_tmax < x_tmax) {x_tmax = y_tmax;} 156 | 157 | /* Now, calculate the time interval for the ray's intersection along the z-axis */ 158 | auto z_tmin = (z[ direction_is_negative[2]] - ray.origin.z) * inverse_ray_direction.z; 159 | auto z_tmax = (z[!direction_is_negative[2]] - ray.origin.z) * inverse_ray_direction.z; 160 | if (x_tmin > z_tmax || z_tmin > x_tmax) { 161 | return false; 162 | } 163 | 164 | /* Now, merge the merged x- and y- time intervals with the z-axis time interval. 165 | After this, [x_tmin, x_tmax] gives the overall time interval for which the ray 166 | `ray` intersects with this `AABB`. */ 167 | if (z_tmin > x_tmin) {x_tmin = z_tmin; } 168 | if (z_tmax < x_tmax) {x_tmax = z_tmax;} 169 | 170 | /* If the overall time interval of the ray `ray`'s intersection with this AABB 171 | intersects with the given time interval `ray_times`, then there is a intersection. 172 | Otherwise, there isn't. */ 173 | return (x_tmin < ray_times.max) && (x_tmax > ray_times.min); 174 | } 175 | 176 | /* Updates (possibly expands) this `AABB` to also bound the `AABB` `other`. */ 177 | auto& merge_with(const AABB &other) { 178 | /* Just combine the x-, y-, and z- intervals with those from `other` */ 179 | x.merge_with(other.x); 180 | y.merge_with(other.y); 181 | z.merge_with(other.z); 182 | return *this; 183 | } 184 | 185 | /* Updates (possibly expands) this `AABB` to also bound the `Point3D` `p`. */ 186 | auto& merge_with(const Point3D &p) { 187 | x.merge_with(p.x); 188 | y.merge_with(p.y); 189 | z.merge_with(p.z); 190 | return *this; 191 | } 192 | 193 | /* Setters */ 194 | 195 | /* Pads all axes with length less than `min_axis_length` to have length exactly 196 | `min_axis_length` (well, "exactly" as far as floating-point arithmetic can give you). */ 197 | auto& ensure_min_axis_length(double min_axis_length) { 198 | if (x.size() < min_axis_length) {x.pad_with((min_axis_length - x.size()) / 2);} 199 | if (y.size() < min_axis_length) {y.pad_with((min_axis_length - y.size()) / 2);} 200 | if (z.size() < min_axis_length) {z.pad_with((min_axis_length - z.size()) / 2);} 201 | return *this; 202 | } 203 | 204 | /* Overload `operator<<` to allow printing `AABB`s to output streams */ 205 | friend std::ostream& operator<< (std::ostream &os, const AABB &aabb); 206 | 207 | /* --- CONSTRUCTORS --- */ 208 | 209 | /* The default constructor constructs an empty `AABB`; that is, the `AABB` where all intervals 210 | are the empty interval `Interval::empty()`. Note: Prefer using the named constructor 211 | `AABB::empty()` instead; its functionality is equivalent, and it is more readable. This 212 | default constructor exists merely to allow other classes which have an `AABB` as a member 213 | to themselves be default constructible without having to specify a default member initializer 214 | for fields of type `AABB`. */ 215 | AABB() : AABB(Interval::empty(), Interval::empty(), Interval::empty()) {} 216 | 217 | /* --- NAMED CONSTRUCTORS --- */ 218 | 219 | /* Returns an empty `AABB`; specifically, the `AABB` where all "slabs" are set to the empty 220 | interval `Interval::empty()`. This is equivalent to the default constructor of `AABB`, but it is 221 | recommended to use `AABB::empty()` over `AABB::AABB()` for improved readability. */ 222 | static AABB empty() { 223 | return AABB(); 224 | } 225 | 226 | /* Constructs an AABB (Axis-Aligned Bounding Box) consisting of all points with x-coordinate 227 | in the interval `x_`, y-coordinate in the interval `y_`, and z-coordinate in the range `z_`. 228 | In other words, this creates the AABB from the three "slabs" (axis intervals) `x_`, `y_`, and 229 | `z_`. */ 230 | static AABB from_axis_intervals(const Interval &x_, const Interval &y_, const Interval &z_) { 231 | return AABB(x_, y_, z_); 232 | } 233 | 234 | /* Constructs the minimum-volume AABB (Axis-Aligned Bounding Box) containing all the points 235 | specified in `points`. */ 236 | static AABB from_points(std::initializer_list points) { 237 | auto ret = AABB::empty(); 238 | for (const auto &p : points) { 239 | ret.merge_with(p); 240 | } 241 | return ret; 242 | } 243 | 244 | /* Returns the minimum-volume `AABB` that contains both of the `AABB`s `a` and `b`. 245 | That is, this returns the `AABB` that would result if `a` and `b` were combined into a single 246 | `AABB`. */ 247 | static AABB merge(const AABB &a, const AABB &b) { 248 | return AABB(Interval::merge(a.x, b.x), Interval::merge(a.y, b.y), 249 | Interval::merge(a.z, b.z)); 250 | } 251 | }; 252 | 253 | /* Overload `operator<<` to allow printing `AABB`s to output streams */ 254 | std::ostream& operator<< (std::ostream &os, const AABB &aabb) { 255 | os << "AABB {x: " << aabb.x << ", y: " << aabb.y << ", z: " << aabb.z << "} "; 256 | return os; 257 | } 258 | 259 | #endif -------------------------------------------------------------------------------- /include/base/camera.h: -------------------------------------------------------------------------------- 1 | #ifndef CAMERA_H 2 | #define CAMERA_H 3 | 4 | #include 5 | #include "util/image.h" 6 | #include "math/ray3d.h" 7 | #include "acceleration/bvh.h" 8 | 9 | /* The class `Camera` encapsulates the notion of a camera viewing a 3D scene from 10 | a designated camera/eye point, located a certain length (called the focal length) 11 | away from the "viewport" or "image plane": the virtual rectangle upon with the 12 | 3D scene is projected to form the final 2D image. */ 13 | class Camera { 14 | 15 | /* Width and height (in pixels) of the final rendered image. 1280 x 720 by default */ 16 | size_t image_w = 1280, image_h = 720; 17 | /* Width and height of the viewport (aka image plane). Note that these are real-valued. 18 | Both are determined by either `vertical_fov` or `horizontal_fov` (as well as the focal 19 | length) during `init()`. */ 20 | double viewport_w, viewport_h; 21 | /* The horizontal and vertical delta vectors from pixel to pixel in the viewport. */ 22 | Vec3D pixel_delta_x, pixel_delta_y; 23 | /* `camera` stores the camera ray; the coordinates of the camera/eye point, 24 | and the direction in which the camera looks. By default, the camera center is 25 | at the origin, and the camera looks toward the direction of negative z-axis 26 | (as is usual for right-handed coordinates, which we use). */ 27 | Ray3D camera{.origin = Point3D{0, 0, 0}, .dir = Vec3D{0, 0, -1}}; 28 | /* `camera_lookat`, if specified, is the point towards which the camera always looks, no matter 29 | where the camera center is. If not specified, then `camera.dir` will be used for the camera's 30 | direction. */ 31 | std::optional camera_lookat; 32 | /* `view_up_dir` allows the user to specify the "up" direction for the camera. 33 | Specifically, the "up" direction on the viewport is equal to the projection of 34 | `view_up_dir` onto the viewport. By default, the "up" direction is the same as the 35 | positive y-axis. */ 36 | Vec3D view_up_dir{0, 1, 0}; 37 | /* `cam_basis_x/y/z` form an orthonormal basis of the camera and its orientation. 38 | `cam_basis_x` is an unit vector pointing to the right, `cam_basis_y` is an unit vector 39 | pointing up (so `cam_basis_x` and `cam_basis_y` form an orthonormal basis for the viewport), 40 | and `cam_basis_z` is an unit vector pointing behind the camera, orthogonal to the viewport 41 | (it points behind the camera and not in front due to the use of right-handed coordinates). 42 | Calculated in `init()`, depends on `camera` and `view_up_dir` (and themselves; `cam_basis_z` 43 | is calculated by taking the cross product of `cam_basis_x` and `cam_basis_y`). */ 44 | Vec3D cam_basis_x, cam_basis_y, cam_basis_z; 45 | /* `focus_dist` is this Camera's focus distance; that is, the distance from the 46 | camera center to the plane of perfect focus. Definition-wise, it is different 47 | from the focal length, which is the distance from the camera center to the viewport. 48 | However, for our model, the focus distance will always equal the focal length; that is, 49 | we will always place our viewport on the plane of perfect focus. 50 | 51 | `focus_dist` may be explicitly set by the user through `set_focus_distance`. If not set, 52 | then the focus distance will default to the length of the camera's direction vector in 53 | `init()` (that is, `focus_dist` defaults to `camera.dir.mag()` if not explicitly set). 54 | The rationale behind this decision is explained in the comments for `init()`. */ 55 | std::optional focus_dist; 56 | /* `defocus_angle` is the angle of the cone with apex at the viewport's center and circular 57 | base equivalent to the defocus disk (which is centered at the camera center). A `defocus_angle` 58 | of 0 represents no blur, and that is the default. `defocus_angle` is stored in radians. May 59 | be explicitly set by the user through `set_defocus_angle`. */ 60 | double defocus_angle = 0; 61 | /* `defocus_disk_x/y` are the vectors representing the horizontal and vertical radii vectors 62 | of the defocus disk. Both are determined by `focal_length`, `defocus_angle`, and 63 | `cam_basis_x/y`. */ 64 | Vec3D defocus_disk_x, defocus_disk_y; 65 | /* Coordinates of the top-left image pixel (calculated in `init()`) */ 66 | Point3D pixel00_loc; 67 | /* Number of rays sampled per pixel, 1 by default */ 68 | size_t samples_per_pixel = 1; 69 | /* Maximum number of light ray bounces into the scene, 10 by default */ 70 | size_t max_depth = 10; 71 | /* Vertical and horizontal FOV (Field of View) of the camera, stored in radians. 72 | At any instant, only one of the vertical and horizontal FOV has a specified value 73 | (only one of `vertical_fov` and `horizontal_fov` will be a non-empty `std::optional`); 74 | the other will be inferred from the FOV that is specified, as well as the image's aspect ratio, 75 | in `init()`. By default, the vertical FOV is 90 degrees, and so the horizontal FOV will be 76 | determined from the vertical FOVof 90 degrees and the aspect ratio of the images in `init()`. 77 | */ 78 | std::optional vertical_fov{90}, horizontal_fov; 79 | /* `background` = The default background color of scenes rendered by this `Camera`. That is, 80 | whenever a ray hits no object in the scene, this color is returned as the color of that ray. 81 | `RGB::from_mag(0.5)` (gray; halfway between white and black) by default. */ 82 | RGB background{RGB::from_mag(0.5)}; 83 | 84 | /* Set the values of `viewport_w`, `viewport_h`, `pixel_delta_x`, `pixel_delta_y`, 85 | `upper_left_corner`, and `pixel00_loc` based on `image_w` and `image_h`. This function 86 | is called before every render. */ 87 | void init() { 88 | 89 | /* Calculate the true aspect ratio of the image. Note that this may be different 90 | from the aspect ratio passed in calls to `set_image_by_xxxxx_and_aspect_ratio`, 91 | because `image_w` and `image_h` both must be integers. */ 92 | auto aspect_ratio = static_cast(image_w) / static_cast(image_h); 93 | 94 | /* If the user has explicitly provided a lookat point for the camera, then update 95 | the camera's direction to be towards that lookat point from the current camera 96 | center. */ 97 | if (camera_lookat) { 98 | camera.dir = *camera_lookat - camera.origin; 99 | } 100 | 101 | /* If `focus_dist` is not explicitly provided by the user, then set it equal to 102 | the length of the camera's direction vector. This way, if the user only specifies 103 | a camera center and a camera lookat point, the focus distance will be so that the 104 | lookat point is in perfect focus. */ 105 | if (!focus_dist) { 106 | focus_dist = camera.dir.mag(); 107 | } 108 | 109 | /* The focal length (the distance from the camera center to the viewport) will always 110 | be set equal to the focus distance (the distance from the camera center to the plane of 111 | perfect focus). That is, we will always place our viewport on the plane of perfect 112 | focus. */ 113 | auto focal_length = *focus_dist; 114 | 115 | /* Set viewport_w and viewport_h based on `vertical_fov` or `horizontal_fov`, 116 | `focal_length`, and `aspect_ratio`. */ 117 | if (vertical_fov) { 118 | viewport_h = 2 * focal_length * std::tan(*vertical_fov / 2); 119 | viewport_w = viewport_h * aspect_ratio; 120 | } else { 121 | viewport_w = 2 * focal_length * std::tan(*horizontal_fov / 2); 122 | viewport_h = viewport_w / aspect_ratio; 123 | } 124 | 125 | /* Calculate an orthonormal basis for the camera and its orientation. */ 126 | cam_basis_z = -camera.dir.unit_vector(); 127 | cam_basis_x = cross(view_up_dir, cam_basis_z).unit_vector(); 128 | cam_basis_y = cross(cam_basis_z, cam_basis_x); /* Unit vector because cam_basis_x/z are */ 129 | 130 | /* `x_vec` and `y_vec` are the vectors right and down across the viewport (starting from the 131 | top-left corner), respectively. We use right-handed coordinates, which means the y-axis goes 132 | up, the x-axis goes right, and the NEGATIVE z-axis goes into the image. As a result, the 133 | vector going down across the viewport has a negative y-component. */ 134 | Vec3D x_vec = viewport_w * cam_basis_x, y_vec = -viewport_h * cam_basis_y; 135 | pixel_delta_x = x_vec / static_cast(image_w); /* Divide by pixels per row */ 136 | pixel_delta_y = y_vec / static_cast(image_h); /* Divide by pixels per column */ 137 | 138 | /* Find `upper_left_corner`, the coordinates of the upper left corner of the viewport. 139 | The upper left point of the viewport is found by starting at the camera, moving 140 | `focal_length` units towards the camera (so adding negative `focal_length` times 141 | `cam_basis_z` to `camera.origin`, due to the use of right-handed coordinates), then 142 | subtracting half of `x_vec` and `y_vec`. */ 143 | auto upper_left_corner = camera.origin - focal_length * cam_basis_z - x_vec / 2 - y_vec / 2; 144 | 145 | /* Pixels are inset from the edges of the camera by half the pixel-to-pixel distance. 146 | This ensures that the viewpoint area is evenly divided into `pixel_delta_x` by 147 | `pixel_delta_y`-sized regions. */ 148 | pixel00_loc = upper_left_corner + pixel_delta_x / 2 + pixel_delta_y / 2; 149 | 150 | /* Calculate the defocus disk radius vectors. To see how the calculation for 151 | `defocus_disk_radius` works, remember that the defocus disk is the base of the 152 | right cone with apex at the center of the viewport, apex angle equal to `defocus_angle`, 153 | and with its circular base centered at the camera center. */ 154 | auto defocus_disk_radius = focal_length * std::tan(defocus_angle / 2); 155 | defocus_disk_x = defocus_disk_radius * cam_basis_x; 156 | defocus_disk_y = defocus_disk_radius * cam_basis_y; 157 | } 158 | 159 | /* Returns a random point in the camera's defocus disk. */ 160 | auto random_point_in_defocus_disk() const { 161 | 162 | /* First, generate a random vector in the unit disk */ 163 | auto vec = Vec3D::random_vector_in_unit_disk(); 164 | 165 | /* Then, use the defocus disk basis vectors to turn `vec` into 166 | a random vector in the camera's defocus disk. */ 167 | return camera.origin + vec.x * defocus_disk_x + vec.y * defocus_disk_y; 168 | } 169 | 170 | /* 171 | Returns a ray originating from the defocus disk centered at `camera.origin`, and through 172 | a random point in the square region centered at the pixel in row `row` and column `col`. 173 | 174 | Note that the region is square because it is a rectangle with width |`pixel_delta_x`| and 175 | height |`pixel_delta_y`|, and we have `pixel_delta_x` = `x_vec` / `image_w` = 176 | `viewport_w` / `image_w`, and `pixel_delta_y` = `y_vec` / `image_h` = `viewport_h` / `image_h`. 177 | Then, `viewport_w` / `image_w` = `viewport_h` / `image_h` because `viewport_w` / `viewport_h` 178 | = `image_w` / `image_h` (as the aspect ratio of the viewport is equal to the aspect ratio of 179 | the image). 180 | 181 | Then why do we need both `pixel_delta_x` and `pixel_delta_y`? I think it's for clarity, or, 182 | perhaps, due to possible floating-point errors that cause slight differences in `pixel_delta_x` 183 | and `pixel_delta_y`. */ 184 | auto random_ray_through_pixel(size_t row, size_t col) const { 185 | 186 | /* The ray originates from a random point in the camera's defocus disk */ 187 | auto ray_origin = (defocus_angle <= 0 ? camera.origin : random_point_in_defocus_disk()); 188 | 189 | /* Find the center of the pixel */ 190 | auto pixel_center = pixel00_loc + static_cast(row) * pixel_delta_y 191 | + static_cast(col) * pixel_delta_x; 192 | 193 | /* Find a random point in the square region centered at `pixel_center`. The region 194 | has width `pixel_delta_x` and height `pixel_delta_y`, so a random point in this 195 | region is found by adding `pixel_delta_x` and `pixel_delta_y` each multiplied by 196 | a random real number in the range [-0.5, 0.5]. */ 197 | auto pixel_sample = pixel_center + rand_double(-0.5, 0.5) * pixel_delta_x 198 | + rand_double(-0.5, 0.5) * pixel_delta_y; 199 | return Ray3D(ray_origin, pixel_sample - ray_origin); 200 | } 201 | 202 | /* Computes and returns the color of the light ray `ray` shot into the `Hittable` 203 | specified by `world`. If `ray` has bounced more than `depth_left` times, returns 204 | `RGB::zero()`. */ 205 | template 206 | requires std::is_base_of_v 207 | auto ray_color(const Ray3D &ray, size_t depth_left, const T &world) { 208 | 209 | /* If the ray has bounced the maximum number of times, then no light is collected 210 | from it. Thus, we return the RGB color (r: 0, g: 0, b: 0). */ 211 | if (depth_left == 0) { 212 | return RGB::zero(); 213 | } 214 | 215 | /* Interval::with_min(0.00001) is the book's fix for shadow acne; ignore 216 | ray collisions that happen at very small times. */ 217 | if (auto info = world.hit_by(ray, Interval::with_min(0.00001)); info) { 218 | 219 | /* `emitted_color` = the color of light rays emitted from the current hit 220 | object's material. If the current object does not emit any light, then 221 | `info->material->emit()` and thus `emitted_color` will just equal `RGB::zero()`. */ 222 | auto emitted_color = info->material->emit(); 223 | 224 | /* If this ray hits an object in the scene, compute the scattered ray and the 225 | color attenuation. The resulting color of this ray is then equal to 226 | attenuation * ray_color(scattered ray). Finally, we add this to `emitted_color` 227 | (which, again, is the color contributed from the current hit material's light 228 | emission), and return their sum. */ 229 | if (auto scattered = info->material->scatter(ray, *info); scattered) { 230 | /* Return the sum of the color contributed from the current object's material's 231 | light emission, and the color contributed from the scattered ray's bounces off 232 | objects in the scene. */ 233 | return emitted_color 234 | + scattered->attenuation * ray_color(scattered->ray, depth_left - 1, world); 235 | } else { 236 | /* If the material intersected did not produce a new ray from light scattering 237 | (if it absorbed the ray, or if the ray was determined to have originated from that 238 | object (and so no further tracing of this ray's path back in time is needed)), then 239 | just return the color contribution from the object itself; that is, we just return 240 | the color of light rays emitted from the current object's material. */ 241 | return emitted_color; 242 | } 243 | } else { 244 | /* To show off the lights, we just make the background completely black (so 245 | we always return `RGB::zero()` when a ray flies into the background). As a result, 246 | all light in the resulting render comes from an actual light source, and not just 247 | from the background. */ 248 | return background; 249 | return RGB::zero(); 250 | 251 | /* If this ray doesn't intersect any object in the scene, then its color is determined 252 | by the background. Here, the background is a blue-to-white gradient depending on the 253 | ray's y-coordinate; bluer for lesser y-coordinates and whiter for larger y-coordinates 254 | (so bluer at the top and whiter at the bottom). */ 255 | return lerp(RGB::from_mag(1, 1, 1), RGB::from_mag(0.5, 0.7, 1), 256 | 0.5 * ray.dir.unit_vector().y + 0.5); 257 | } 258 | } 259 | 260 | public: 261 | 262 | /* Renders the `Hittable` specified by `world` to an `Image` and returns that image. 263 | Will render in parallel (using OpenMP for now) if available. */ 264 | template /* Guarantee static dispatch when possible using C++20 concepts */ 265 | requires std::is_base_of_v 266 | auto render(const T &world) { 267 | init(); 268 | 269 | /* Calculate and store the color of each pixel */ 270 | auto img = Image::with_dimensions(image_w, image_h); 271 | ProgressBar pb( 272 | image_h, 273 | "Rendering " + std::to_string(image_w) + " x " + std::to_string(image_h) + " image" 274 | ); 275 | 276 | /* Now use dynamic thread scheduling instead of static thread scheduling, with a block size 277 | of the maximum of `image_h` / 1024 and 1. */ 278 | const size_t thread_chunk_size = std::max(image_h >> 10, size_t{1}); 279 | #pragma omp parallel for schedule(dynamic, thread_chunk_size) 280 | for (size_t row = 0; row < image_h; ++row) { 281 | for (size_t col = 0; col < image_w; ++col) { 282 | 283 | /* Shoot `samples_per_pixel` random rays through the current pixel. 284 | The average of the resulting colors will be the color for this pixel. */ 285 | auto pixel_color = RGB::zero(); 286 | for (size_t sample = 0; sample < samples_per_pixel; ++sample) { 287 | auto ray = random_ray_through_pixel(row, col); 288 | pixel_color += ray_color(ray, max_depth, world); 289 | } 290 | pixel_color /= static_cast(samples_per_pixel); 291 | 292 | img[row][col] = pixel_color; 293 | } 294 | pb.complete_iteration(); 295 | } 296 | return img; 297 | } 298 | 299 | /* When rendering a `Scene`, `Camera::render()` will automatically build a `BVH` over 300 | the `Scene` and render using that `BVH` to improve performance. */ 301 | auto render(const Scene &world) { 302 | return render(BVH(world)); 303 | } 304 | 305 | /* Setters. Each returns a mutable reference to this object to create a functional interface */ 306 | 307 | /* Sets the camera center to the point `p`. This is where the camera is placed. */ 308 | auto& set_camera_center(const Point3D &p) {camera.origin = p; return *this;} 309 | /* Sets the camera direction to the vector `dir`. 310 | 311 | Note: If previously unset, then this Camera's focus distance will automatically be set to 312 | the length of the direction vector `dir`. This way, objects placed at the end of the 313 | camera's direction vector (or at other points that are the same distance away from the 314 | camera center) will appear in perfect focus, and will linearly appear blurrier the further 315 | away they are from that distance. */ 316 | auto& set_camera_direction(const Vec3D &dir) {camera.dir = dir; return *this;} 317 | /* Sets the direction of the camera to be the direction from the camera center 318 | to the point `p`. Note: this is NOT equivalent as `set_camera_lookat`; the latter 319 | instructs the camera to always face toward the provided lookat point, whereas this 320 | simply redirects the camera's current direction to face towards the provided point `p`. 321 | 322 | Note: If previously unset, then this Camera's focus distance will automatically be set to 323 | the distance from the camera center to the point `p`. This way, objects placed at the point 324 | `p` (and other points that are the same distance away from the camera center) will appear 325 | in perfect focus, and will linearly appear blurrier the further away they are from that 326 | distance. */ 327 | auto& set_camera_direction_towards(const Point3D &p) { 328 | camera.dir = p - camera.origin; 329 | camera_lookat.reset(); 330 | return *this; 331 | } 332 | /* Set the camera direction to always be towards the point `p`, no matter where the camera 333 | center is. */ 334 | auto& set_camera_lookat(const Point3D &p) {camera_lookat = p; return *this;} 335 | /* Set the focus distance (the distance from the camera center to the plane of perfect 336 | focus) to `focus_distance` units. Objects placed at that distance will appear in perfect 337 | focus, and will linearly appear blurrier the further away they are from that distance. This 338 | therefore allows for simulating defocus blur (depth-of-field) in the camera. */ 339 | auto& set_focus_distance(double focus_distance) {focus_dist = focus_distance; return *this;} 340 | /* Set the defocus angle of the camera (the angle of the cone with apex at the center of the 341 | viewpoint and with base as the defocus disk, which is centered at the camera center) to 342 | `defocus_angle_degrees` DEGREES, not radians. Smaller defocus angles result in less blur, 343 | and setting the defocus angle to 0 eliminates all blur, making everything render in perfect 344 | focus (which is equivalent to `Camera::turn_blur_off()`). */ 345 | auto& set_defocus_angle(double defocus_angle_degrees) { 346 | defocus_angle = defocus_angle_degrees * std::numbers::pi / 180; /* convert to radians */ 347 | return *this; 348 | } 349 | /* Causes this Camera to render the whole scene in perfect focus, with no defocus blur. */ 350 | auto& turn_blur_off() {defocus_angle = 0; return *this;} 351 | /* Sets the "camera up" direction to the vector `dir`. The true camera up direction will be 352 | determined by taking the projection of `dir` onto the viewport. */ 353 | auto& set_camera_up_direction(const Vec3D &dir) {view_up_dir = dir; return *this;} 354 | /* Sets the width of the final outputted image to `width`. */ 355 | auto& set_image_width(size_t width) {image_w = width; return *this;} 356 | /* Sets the height of the final outputted image to `height`. */ 357 | auto& set_image_height(size_t height) {image_h = height; return *this;} 358 | /* Sets the dimensions of the final outputted image to be `width` by `height`. */ 359 | auto& set_image_dimensions(size_t width, size_t height) { 360 | image_w = width; 361 | image_h = height; 362 | return *this; 363 | } 364 | /* Sets the image width to `width`, and infers the image height from `width` and 365 | `aspect_ratio`.*/ 366 | auto& set_image_by_width_and_aspect_ratio(size_t width, double aspect_ratio) { 367 | auto height = static_cast(std::round(static_cast(width) / aspect_ratio)); 368 | /* Make sure height is at least 1 */ 369 | return set_image_dimensions(width, std::max(size_t{1}, height)); 370 | } 371 | /* Sets the image height to `height`, and infers the image width from `height` and 372 | `aspect_ratio`. */ 373 | auto& set_image_by_height_and_aspect_ratio(size_t height, double aspect_ratio) { 374 | auto width = static_cast(std::round(static_cast(height) * aspect_ratio)); 375 | /* Make sure width is at least 1 */ 376 | return set_image_dimensions(std::max(size_t{1}, width), height); 377 | } 378 | 379 | /* Sets the number of rays sampled for each pixel to `samples`. */ 380 | auto& set_samples_per_pixel(size_t samples) {samples_per_pixel = samples; return *this;} 381 | /* Sets the maximum recursive depth for the camera (the maximum number of bounces for 382 | a given light ray) to `max_depth_`. */ 383 | auto& set_max_depth(size_t max_depth_) {max_depth = max_depth_; return *this;} 384 | /* Sets the vertical FOV (field of view) of the camera to `vertical_fov_degrees` degrees. 385 | The horizontal FOV will then be determined by the `vertical_fov` and the image's aspect 386 | ratio later in `init()`. */ 387 | auto& set_vertical_fov(double vertical_fov_degrees) { 388 | vertical_fov = vertical_fov_degrees * std::numbers::pi / 180; /* convert to radians */ 389 | horizontal_fov.reset(); 390 | return *this; 391 | } 392 | /* Sets the horizontal FOV (field of view) of the camera to `horizontal_fov_degrees` degrees. 393 | The vertical FOV will then be determined by the `horizontal_fov` and the image's 394 | `aspect_ratio` later in `init()`. */ 395 | auto& set_horizontal_fov(double horizontal_fov_degrees) { 396 | horizontal_fov = horizontal_fov_degrees * std::numbers::pi / 180; /* convert to radians */ 397 | vertical_fov.reset(); 398 | return *this; 399 | } 400 | /* Sets the default background color of the camera to `background_color`. This means that any 401 | ray which hits no object in a scene being rendered by this `Camera` will have color equal to 402 | `background_color`. */ 403 | auto& set_background(const RGB &background_color) { 404 | background = background_color; 405 | return *this; 406 | } 407 | 408 | /* Prints this `Camera` to the `std::ostream` specified by `os`. */ 409 | void print_to(std::ostream &os) { 410 | init(); /* Calculate all members before printing them */ 411 | os << "Camera {\n" 412 | << "\tImage dimensions: " << image_w << " x " << image_h << '\n' 413 | << "\tViewport dimensions: " << viewport_w << " x " << viewport_h << '\n' 414 | << "\tpixel_delta_x: " << pixel_delta_x << '\n' 415 | << "\tpixel_delta_y: " << pixel_delta_y << '\n' 416 | << "\tCamera center: " << camera.origin << '\n' 417 | << "\tCamera direction: " << camera.dir << '\n' 418 | << "\tUp direction: " << view_up_dir << '\n' 419 | << "\tCamera orientation x-, y-, and z- orthonormal basis vectors {" 420 | << "\n\t\tx: " << cam_basis_x 421 | << "\n\t\ty: " << cam_basis_y 422 | << "\n\t\tz: " << cam_basis_z << "\n\t}\n" 423 | << "\tFocus distance: " << *focus_dist << '\n' 424 | << "\tDefocus angle: " << defocus_angle << " rad, " 425 | << defocus_angle * 180 / std::numbers::pi << " degrees\n" 426 | << "\tDefocus dist x-, y- orthonormal basis vectors {" 427 | << "\n\t\tx: " << defocus_disk_x 428 | << "\n\t\ty: "<< defocus_disk_y << "\n\t}\n" 429 | << "\tTop-left pixel's center on viewport: " << pixel00_loc << '\n' 430 | << "\tSamples per pixel: " << samples_per_pixel << '\n' 431 | << "\tMaximum bounces per ray: " << max_depth << '\n' 432 | << "\tVertical FOV (-1 means not given): " << vertical_fov.value_or(-1) << " rad, " 433 | << (vertical_fov ? *vertical_fov * 180 / std::numbers::pi : -1) << " degrees\n" 434 | << "\tHorizontal FOV (-1 means not given): " << horizontal_fov.value_or(-1) << " rad, " 435 | << (horizontal_fov ? *horizontal_fov * 180 / std::numbers::pi : -1) << " degrees\n}"; 436 | } 437 | }; 438 | 439 | /* Overload `operator<<` to allow printing `Camera`s to output streams */ 440 | std::ostream& operator<< (std::ostream &os, Camera cam) { 441 | cam.print_to(os); 442 | return os; 443 | } 444 | 445 | #endif -------------------------------------------------------------------------------- /include/base/hittable.h: -------------------------------------------------------------------------------- 1 | #ifndef HITTABLE_AND_HIT_INFO_H 2 | #define HITTABLE_AND_HIT_INFO_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "math/ray3d.h" 10 | #include "math/interval.h" 11 | #include "acceleration/aabb.h" 12 | 13 | /* Forward-declare the class `Material` to avoid circular dependencies of 14 | "base/material.h" and "base/hittable.h" on each other */ 15 | class Material; 16 | 17 | /* `hit_info` stores information about a given ray-object intersection, including 18 | its hit time, hit point, unit surface normal, front vs back face detection, as 19 | well as the material of the object hit. */ 20 | struct hit_info { 21 | 22 | /* `hit_time` = The time where the ray intersects an object */ 23 | double hit_time; 24 | /* `hit_point` = The point at which the ray intersects the object. If `ray` 25 | is the ray, then `hit_point` is equivalent to `ray(hit_time)`. */ 26 | Point3D hit_point; 27 | /* `unit_surface_normal` = The unit vector normal to the surface at the point of intersection. 28 | `unit_surface_normal` points outward if the ray hit the outside of the surface, and it points 29 | inward if the ray hit the inside of the surface. Thus, `unit_surface_normal` represents the 30 | true unit surface normal at the hit point, taking into account whether the front or back face 31 | of the surface was hit. */ 32 | Vec3D unit_surface_normal; 33 | /* `hit_from_outside` = Whether or not the ray hit the outside of the surface. */ 34 | bool hit_from_outside = false; /* Named `front_face` in the tutorial */ 35 | /* `material` points to the `Material` of the object which the ray intersected. */ 36 | const Material* material; 37 | 38 | /* --- CONSTRUCTORS ---*/ 39 | 40 | /* Constructs a `hit_info` given `hit_time_` (the hit time), `hit_point_` (the point at 41 | which the ray intersects the surface), `outward_unit_surface_normal` (an UNIT VECTOR equalling 42 | the normal to the surface at the ray's hit point), the ray `ray` itself, and finally, 43 | `material_` (the material of the surface that `ray` hit). 44 | 45 | Again, `outward_unit_surface_normal` is assumed to be an unit vector. */ 46 | hit_info(double hit_time_, const Point3D &hit_point_, const Vec3D &outward_unit_surface_normal, 47 | const Ray3D &ray, const std::shared_ptr &material_) 48 | : hit_time{hit_time_}, hit_point{hit_point_}, material{material_.get()} 49 | { 50 | /* Determine, based on the directions of the ray and the outward surface normal at 51 | the ray's point of intersection, whether the ray was shot from inside the surface 52 | or from outside the surface. Set `unit_surface_normal` correspondingly: if the ray 53 | was shot from inside the surface, then the unit surface normal should point inward, 54 | and if the ray was shot from outside the surface, then the unit surface normal should 55 | point outward. */ 56 | if (dot(ray.dir, outward_unit_surface_normal) > 0) { 57 | /* Then the angle between the ray and the outward surface normal is in [0, 90) degrees, 58 | which means the ray originated INSIDE the object, and so the true surface normal will 59 | also point inward, and so we set `unit_surface_normal` equal to the negative of 60 | `outward_unit_surface_normal`. We also set `hit_from_outside` to false. */ 61 | unit_surface_normal = -outward_unit_surface_normal; /* Flip outward surface normal */ 62 | hit_from_outside = false; 63 | } else { 64 | /* Then the angle between the ray and the outward surface normal is in [90, 180) 65 | degrees, which means the ray originated OUTSIDE the object, and so the true surface 66 | normal will point outward, and so we just set `unit_surface_normal` to 67 | `outward_unit_surface_normal` itself. We also set `hit_from_outside` to true. */ 68 | unit_surface_normal = outward_unit_surface_normal; 69 | hit_from_outside = true; 70 | } 71 | } 72 | }; 73 | 74 | /* Overload `operator<<` for `hit_info` to allow printing it to output streams */ 75 | std::ostream& operator<< (std::ostream &os, const hit_info &info) { 76 | os << "hit_info {\n\thit_time: " << info.hit_time << "\n\thit_point: " << info.hit_point 77 | << "\n\tsurface_normal: " << info.unit_surface_normal << "\n\thit_from_outside: " 78 | << info.hit_from_outside << "\n}\n"; 79 | return os; 80 | } 81 | 82 | struct Hittable { 83 | 84 | /* Returns a `hit_info` with information about the earliest intersection of the ray 85 | `ray` with this `Hittable` in the time range `ray_times`. If there is no such 86 | intersection, then an empty `std::optional` is returned. 87 | 88 | The = 0 causes `hit_by` to be a pure virtual function, and so `Hittable` is an abstract 89 | class, which means it itself cannot be instantiated (good, it's meant to only be an 90 | interface). */ 91 | virtual std::optional hit_by(const Ray3D &ray, const Interval &ray_times) const = 0; 92 | 93 | /* Returns the AABB (Axis-Aligned Bounding Box) for this `Hittable` object. 94 | 95 | Note that any AABB can be returned. Obviously, though, smaller AABB's are better; they reduce 96 | the chance that a ray will collide with them, which results in more intersection checks in 97 | the BVH. */ 98 | virtual AABB get_aabb() const = 0; 99 | 100 | /* When a BVH (Bounding Volume Hierarchy) is built over a list of `Hittable`s, each `Hittable` 101 | in the list will be treated as a single indivisible unit. That is, when the BVH reaches a 102 | `Hittable`, it will not try to split it any further, because it sees the `Hittable` as an 103 | indivisible object. However, in reality, `Hittable`s are allowed to contain other `Hittable`s; 104 | these are called compound `Hittable`s, which include `Scene`, `Box`, and so on. This leads to a 105 | problem: because BVH's are unable to split compound `Hittable`s into their constituent 106 | `Hittable` components, ray-scene intersection tests will not be fully accelerated by the `BVH`, 107 | resulting in higher runtimes. For instance, if I had a complex `Hittable` with millions of 108 | `Hittable` components, then a BVH over a `Scene` containing that `Hittable` would treat it as 109 | a single primitive, and so it would not try to further split the millions of `Hittable` 110 | components. This would result in the millions of `Hittable` components being checked by 111 | brute-force whenever a ray-intersection test reaches the complex `Hittable`, which is clearly 112 | undesirable. 113 | 114 | To fix this problem, we allow `Hittable`s to return a `std::vector>` 115 | representing its constituent `Hittable` components. Then, when building a BVH over a `Scene` 116 | containing a `Hittable`, the `BVH` will build over the primitive components of all objects 117 | in the `Scene`, rather than just being built over the objects themselves. NOTE THAT IF A 118 | `Hittable` TYPE IS ALREADY AN INDIVISIBLE PRIMITIVE (such as `Sphere`), THEN IT SHOULD 119 | RETURN THE EMPTY `std::vector`; this is the default, and it tells the `BVH` to treat the 120 | current `Hittable` as an indivisible unit when it is being built over a `Scene`. */ 121 | virtual std::vector> get_primitive_components() const { 122 | 123 | /* Again, objects that are already indivisible primitives (that have no primitive 124 | components) should return `{}` from this function, which is the default. */ 125 | return {}; 126 | } 127 | 128 | /* Prints this `Hittable` object to the `std::ostream` specified by `os`. */ 129 | virtual void print_to(std::ostream &os) const = 0; 130 | 131 | /* We need a virtual destructor for `Hittable`, because we will be "deleting [instances] 132 | of a derived class through a pointer to [the] base class". See https://tinyurl.com/hnutv8se. */ 133 | virtual ~Hittable() = default; 134 | }; 135 | 136 | /* Overload `operator<<` for `Hittable` to allow printing it to output streams */ 137 | std::ostream& operator<< (std::ostream &os, const Hittable &object) { 138 | object.print_to(os); /* Call the overriden `print_to` function for the type of `object` */ 139 | return os; 140 | } 141 | 142 | #endif -------------------------------------------------------------------------------- /include/base/material.h: -------------------------------------------------------------------------------- 1 | #ifndef MATERIAL_H 2 | #define MATERIAL_H 3 | 4 | #include 5 | #include "util/rgb.h" 6 | #include "math/ray3d.h" 7 | #include "util/rand_util.h" 8 | 9 | /* `scatter_info` stores information about scattered rays; specifically, it stores the 10 | origin and direction of the scattered ray, as well as the color attenuation resulting from 11 | the material that the ray previously hit. */ 12 | struct scatter_info { 13 | 14 | /* `ray` = the scattered ray */ 15 | Ray3D ray; 16 | /* `attenuation` = The color by which `ray_color(ray)` will be multiplied by (multiplying colors 17 | means element-wise multiplication; apparently this is how objects' intrinsic colors are 18 | accounted for). TODO: Elaborate on why this color is named "attenuation". */ 19 | RGB attenuation; 20 | 21 | scatter_info(const Ray3D &ray_, const RGB &attenuation_) 22 | : ray{ray_}, attenuation{attenuation_} {} 23 | }; 24 | 25 | struct Material { 26 | /* Calculate the ray resulting from the scattering of the incident ray `ray` when it hits 27 | this `Material` with hit information (hit point, hit time, etc) specified by `hit_info`. Note 28 | that if the ray is not scattered (when it is absorbed by the material), an empty `std::optional` 29 | is returned. */ 30 | virtual std::optional scatter(const Ray3D &ray, const hit_info &info) const = 0; 31 | 32 | /* For emitters, `emit()` returns the color of light rays emitted from that emitter. 33 | 34 | We choose to NOT make `emit` a pure virtual function, to prevent from having to implement `emit` 35 | in every single class inheriting from `Material`. Instead, we provide a default for non-emitting 36 | materials: by default, `emit()` will just return `RGB::zero()`, representing no light being 37 | emitted from the material. */ 38 | virtual RGB emit() const { 39 | return RGB::zero(); 40 | } 41 | 42 | /* Prints this `Material` object to the `std::ostream` specified by `os`. */ 43 | virtual void print_to(std::ostream &os) const = 0; 44 | 45 | virtual ~Material() = default; 46 | }; 47 | 48 | /* Overload `operator<<` to for `Material` to allow printing it to output streams */ 49 | std::ostream& operator<< (std::ostream &os, const Material &mat) { 50 | mat.print_to(os); /* Call the overriden `print_to` function for the type of `mat` */ 51 | return os; 52 | } 53 | 54 | /* The `Lambertian` type encapsulates the notion of Lambertian reflectors (also called 55 | diffuse reflectors or matte surfaces). The defining property of Lambertian reflectors 56 | are that they obey the Lambertian Cosine Law, and so have the same luminance when 57 | viewed from any angle. */ 58 | class Lambertian : public Material { 59 | /* `intrinsic_color` = The color intrinsic to this Lambertian reflector */ 60 | RGB intrinsic_color; 61 | 62 | public: 63 | 64 | std::optional scatter(const Ray3D &ray, const hit_info &info) const override { 65 | 66 | /* Lambertian reflectance states that an incident ray will be reflected (scattered) at an 67 | angle of phi off the surface normal with probability cos(phi). This is equivalent to saying 68 | that the endpoint of the scattered ray is an uniformly random point on the unit sphere 69 | centered at the endpoint of the unit surface normal at the original ray's intersection 70 | point with the surface. */ 71 | auto scattered_direction = info.unit_surface_normal + Vec3D::random_unit_vector(); 72 | 73 | /* If the random unit vector happens to equal `-info.surface_normal`, then 74 | `scattered_direction` will be the zero vector, which will lead to numerical errors. 75 | Thus, when `scattered_direction` is nearly a zero vector (here, we say that is the 76 | case if all its components have magnitude less than `1e-8`), we just set it to 77 | the direction of the surface normal at the intersection point. */ 78 | if (scattered_direction.near_zero()) { 79 | scattered_direction = info.unit_surface_normal; 80 | } 81 | 82 | /* The scattered ray goes from the original ray's hit point to the randomly-chosen point 83 | on the unit sphere centered at the unit surface normal's endpoint, and the attenuation 84 | is the same as the intrinsic color. */ 85 | return scatter_info(Ray3D(info.hit_point, scattered_direction), intrinsic_color); 86 | } 87 | 88 | /* Prints this `Lambertian` material to the `std::ostream` specified by `os`. */ 89 | void print_to(std::ostream &os) const override { 90 | os << "Lambertian {color: " << intrinsic_color.as_string(", ", "()") << "} " << std::flush; 91 | } 92 | 93 | /* Constructs a Lambertian (diffuse) reflector with intrinsic color `intrinsic_color_`. */ 94 | Lambertian(const RGB &intrinsic_color_) : intrinsic_color{intrinsic_color_} {} 95 | }; 96 | 97 | /* The `Metal` type encapsulates the notion of a metallic material; a material that displays 98 | generally-specular reflection as opposed to Lambertian reflection due to its physical and 99 | electronic properties. 100 | 101 | The reason why metals tend to reflect light rather than absorbing or transmitting it is because 102 | metals tend to contain high numbers of free electrons. This means that when a photon hits the 103 | surface of a metal, it is relatively more likely that a free electron will be there to absorb 104 | and re-emit (reflect) the photon. As a result, metals tend to be light reflectors. */ 105 | class Metal : public Material { 106 | /* `intrinsic_color` = The color intrinsic to this metal */ 107 | RGB intrinsic_color; 108 | /* `fuzz_factor` represents how much "fuzz" there is in this metal's reflection; 109 | a fuzz factor of 0 represents perfect specular reflection, and a fuzz factor of 1 110 | represents Lambertian reflection (and those are the lowest and highest allowed 111 | fuzz factors, respectively). */ 112 | double fuzz_factor; 113 | 114 | public: 115 | 116 | std::optional scatter(const Ray3D &ray, const hit_info &info) const override { 117 | 118 | /* Unlike Lambertian reflectors, metals display specular reflection; the incident 119 | light ray is reflected about the surface normal. */ 120 | auto reflected_unit_dir = reflected(ray.dir.unit_vector(), info.unit_surface_normal); 121 | /* To simulate fuzzy reflection off metal surfaces, the end point is chosen randomly 122 | off the sphere with radius `fuzz_factor` centered at the endpoint of `reflected_unit_dir`. 123 | Thus, `fuzz_factor` = 0 results in perfect specular (perfectly mirror-like) reflection. 124 | Note that this is why `reflected_unit_dir` is made an unit vector (and so it is why 125 | `ray.dir` is normalized before being passed to `reflected`; it's to ensure every direction 126 | of reflection has the same probability, just like in Lambertian reflectors). */ 127 | auto scattered_dir = reflected_unit_dir + fuzz_factor * Vec3D::random_unit_vector(); 128 | 129 | /* If the scattered direction points into the surface, just have the surface absorb 130 | the light ray entirely (so return an empty std::optional) */ 131 | if (dot(info.unit_surface_normal, scattered_dir) < 0) { 132 | return {}; 133 | } 134 | 135 | /* The scattered ray goes from the original ray's hit point to the randomly-chosen point 136 | on the unit sphere centered at the unit surface normal's endpoint, and the attenuation 137 | is the same as the intrinsic color. */ 138 | return scatter_info(Ray3D(info.hit_point, scattered_dir), intrinsic_color); 139 | } 140 | 141 | /* Prints this `Metal` material to the `std::ostream` specified by `os`. */ 142 | void print_to(std::ostream &os) const override { 143 | os << "Metal {color: " << intrinsic_color.as_string(", ", "()") << ", fuzz factor: " 144 | << fuzz_factor << "} " << std::flush; 145 | } 146 | 147 | /* Constructs a metal (specular) reflector with intrinsic color `intrinsic_color_` 148 | and "fuzz factor" `fuzz`. A fuzz factor of 0 indicates perfect specular reflection 149 | (like a mirror), and the maximum fuzz factor is 1 (indicating diffuse reflection). */ 150 | Metal(const RGB &intrinsic_color_, double fuzz = 0) : intrinsic_color{intrinsic_color_}, 151 | fuzz_factor{std::fmin(fuzz, 1.)} {} 152 | }; 153 | 154 | /* The `Dielectric` type encapsulates the notion of dielectric (nonconducting) materials, 155 | such as glass. Unlike metallic materials, dielectric materials tend to absorb or transmit 156 | incoming photons instead of reflecting them. 157 | 158 | The reason why dielectric materials tend to absorb or transmit photons rather than 159 | reflecting photons is because dielectric materials, by definition, have NO FREE ELECTRONS. 160 | As a result, when a photon hits the surface of a dielectric material, there will be no 161 | free electrons to absorb and re-emit (reflect) the photon, resulting in reflection 162 | happening only in a few specific circumstances (as described by Snell's Law and the 163 | Fresnel Equations; we use Snell's Law in our implementation of dielectrics). */ 164 | class Dielectric : public Material { 165 | 166 | /* `refr_index` = The refractive index of this dielectric surface */ 167 | double refr_index; 168 | 169 | /* Calculate the reflectance (the specular reflection coefficient) of this 170 | dielectric material using Schlick's approximation. 171 | `cos_theta` = cos(theta), where theta is the angle between the incident light 172 | ray and the surface normal on the side of the initial medium. 173 | `refractive_index_ratio` = the ratio of the refractive index of the initial medium 174 | to the refractive index of the final medium. */ 175 | static auto reflectance(double cos_theta, double refractive_index_ratio) { 176 | /* Use Schlick's approximation to calculate the reflectance. 177 | See https://en.wikipedia.org/wiki/Schlick%27s_approximation. */ 178 | auto r0 = (1 - refractive_index_ratio) / (1 + refractive_index_ratio); 179 | r0 *= r0; 180 | return r0 + (1 - r0) * std::pow(1 - cos_theta, 5); 181 | } 182 | 183 | public: 184 | 185 | std::optional scatter(const Ray3D &ray, const hit_info &info) const override { 186 | 187 | /* If the ray hits this dielectric from the outside, then it is transitioning from 188 | air (refractive index assumed to be 1) to the current object, so the ratio is 1 / ri. 189 | Otherwise, if the ray hits this dielectric from the inside, then it is transitioning from 190 | the current object to air, so the ratio is the reciprocal ri / 1. */ 191 | auto refractive_index_ratio = (info.hit_from_outside ? 1. / refr_index : refr_index / 1.); 192 | auto unit_dir = ray.dir.unit_vector(); 193 | 194 | /* Calculate direction of resulting ray. Try refraction, then if no refraction is 195 | possible under Snell's Law the ray must be reflected. Also, even if refraction 196 | is possible, the material will have a reflectance depending on the current angle; 197 | we still reflect the light ray with probability equal to the reflectance. */ 198 | auto dir = refracted(unit_dir, info.unit_surface_normal, refractive_index_ratio); 199 | if (!dir) { 200 | /* If this dielectric material cannot refract the light ray, then it must reflect it. 201 | This is the phenomenon of "Total Internal Reflection". */ 202 | dir = reflected(unit_dir, info.unit_surface_normal); 203 | } else { 204 | /* Even if this dielectric material can refract the light ray, it has a reflectance; 205 | we reflect the light ray with probability equal to the reflectance. */ 206 | auto cos_theta = std::fmin(dot(-unit_dir, info.unit_surface_normal), 1.); 207 | if (rand_double() < reflectance(cos_theta, refractive_index_ratio)) { 208 | dir = reflected(unit_dir, info.unit_surface_normal); 209 | } 210 | } 211 | 212 | /* Attenuance is just (1, 1, 1); color is not lost when moving through (or reflecting 213 | off of) a Dielectric material apparently. The tutorial says "Attenuation is always 1" 214 | because "glass surface[s] absorb nothing". Makes sense, but does this hold for ALL 215 | dielectric surfaces? Maybe for tinted glass it doesn't, but our current implementation 216 | of Dielectric doesn't have a field for intrinsic color or tint. */ 217 | return scatter_info(Ray3D(info.hit_point, *dir), RGB::from_mag(1, 1, 1)); 218 | } 219 | 220 | /* Prints this `Dielectric` material to the `std::ostream` specified by `os`. */ 221 | void print_to(std::ostream &os) const override { 222 | os << "Dielectric {refractive index: " << refr_index << "} " << std::flush; 223 | } 224 | 225 | /* Constructs a dielectric reflector with refractive index `refractive_index`. */ 226 | Dielectric(double refractive_index) : refr_index{refractive_index} {} 227 | }; 228 | 229 | /* The `DiffuseLight` type encapsulates the notion of a diffuse (uniform) light: a light that 230 | emits photons uniformly in all directions. */ 231 | class DiffuseLight : public Material { 232 | 233 | /* `intrinsic_color` = The color of the photons emitted by this `Light`. In the eye 234 | tracing model, this (well, technically it is multiplied by `intensity` first) is the 235 | color taken on by rays when their paths, traced backward in time, lead to this 236 | `DiffuseLight`. */ 237 | RGB intrinsic_color; 238 | /* `intensity` = The relative linear intensity of the light source. In real life, this 239 | represents the number of photons emitted from the light sources per unit time. In this 240 | raytracer, it represents the "strength" of the light; rays that originate from the light 241 | source have color set to `intensity * intrinsic_color`. Thus, higher values for `intensity` 242 | result in this `DiffuseLight` having a larger effect on the objects surrounding it in the 243 | scene. */ 244 | double intensity; 245 | 246 | public: 247 | 248 | std::optional scatter(const Ray3D &ray, const hit_info &info) const override { 249 | /* `DiffuseLights` never scatter light rays; that is, if the ray `ray` is found to have 250 | previously intersected with a diffuse light, then we will assume that it was in fact 251 | *emitted* by that diffuse light (we assume that it originated from that diffuse light). 252 | As a result, when a ray hits a diffuse light, we have finished tracing its path back in 253 | time, because if a ray hits a diffuse light, we assume that that's where its path begun. 254 | Thus, `DiffuseLight::scatter` always returns an empty `std::optional`. */ 255 | return {}; 256 | } 257 | 258 | /* Return the color of light rays emitted from this `DiffuseLight`; as explained above, this 259 | is always `intensity * intrinsic_color` (because a diffuse light, by definition, emits light 260 | uniformly in all directions). */ 261 | RGB emit() const override { 262 | return intensity * intrinsic_color; 263 | } 264 | 265 | /* Prints this `DiffuseLight` material to the `std::ostream` specified by `os`. */ 266 | void print_to(std::ostream &os) const override { 267 | os << "DiffuseLight {color: " << intrinsic_color.as_string(", ", "()") << ", intensity: " 268 | << intensity << "} " << std::flush; 269 | } 270 | 271 | /* Constructs a diffuse light with intrinsic color `intrinsic_color_` and relative intensity 272 | `intensity_`. */ 273 | DiffuseLight(const RGB &intrinsic_color_, double intensity_) 274 | : intrinsic_color{intrinsic_color_}, intensity{intensity_} {} 275 | }; 276 | 277 | #endif -------------------------------------------------------------------------------- /include/base/scene.h: -------------------------------------------------------------------------------- 1 | #ifndef SCENE_H 2 | #define SCENE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "base/hittable.h" 9 | 10 | /* `Scene` is an abstraction over a list of `Hittable` objects in 3D space. 11 | As its name suggests, it is designed to be used to represent `Scene`s of objects. */ 12 | class Scene : public Hittable { 13 | std::vector> objects; 14 | AABB aabb; 15 | 16 | public: 17 | 18 | /* Conversion operators to `std::vector>`. */ 19 | operator auto&() {return objects;} 20 | operator const auto&() const {return objects;} 21 | 22 | /* Implement convenience functions so that `Scene` can be used like a plain `std::vector`: 23 | `size()`, `clear`, `operator[]`, etc. */ 24 | auto size() const {return objects.size();} 25 | void clear() {objects.clear();} 26 | auto& operator[] (size_t index) {return objects[index];} 27 | const auto& operator[] (size_t index) const {return objects[index];} 28 | /* To allow for range-`for` loops */ 29 | auto begin() {return objects.begin();} 30 | auto begin() const {return objects.cbegin();} 31 | auto end() {return objects.end();} 32 | auto end() const {return objects.cend();} 33 | 34 | /* Add a object, stored within a `std::shared_ptr`, to this `Scene` */ 35 | void add(std::shared_ptr object) { 36 | /* Pass `shared_ptr` by copy, so this `Scene` will keep the object alive 37 | as long as it itself has not been destroyed */ 38 | 39 | /* Update `aabb` with the new object `object` */ 40 | aabb.merge_with(object->get_aabb()); /* This must happen BEFORE `object` is moved! */ 41 | /* Use `std::move` when inserting the `std::shared_ptr` into the `std::vector` 42 | of `Hittable`s. Passing the `std::shared_ptr` by copy and then 43 | moving it follows R34 of the C++ Core Guidelines (see https://tinyurl.com/bdfjfrub). */ 44 | objects.push_back(std::move(object)); 45 | } 46 | 47 | /* Adds all objects in the Scene `scene` to this `Scene`. */ 48 | void add(const Scene &scene) { 49 | /* The reason we don't just add a single `std::shared_ptr` to `Scene` to 50 | `objects` is because (a) `scene` isn't a `std::shared_ptr` already, and 51 | (b) it makes the debugging output more confusing to have `Scene`s being 52 | printed as part of other `Scene`s' output. */ 53 | for (const auto &object : scene) { 54 | add(object); 55 | } 56 | } 57 | 58 | /* Return the `hit_info`, if any, from the earliest object hit by the 3D ray `ray`. */ 59 | std::optional hit_by(const Ray3D &ray, const Interval &ray_times) const override { 60 | 61 | std::optional result; 62 | auto min_hit_time = ray_times.max; 63 | 64 | for (const auto &object : objects) { 65 | 66 | /* Update `result` and `min_hit_time` if the `ray` hits the current object 67 | in the time range before `min_hit_time` (the range (`t_min`, `min_hit_time`))*/ 68 | if (auto curr = object->hit_by(ray, Interval(ray_times.min, min_hit_time)); curr) { 69 | result = curr; 70 | min_hit_time = curr->hit_time; 71 | } 72 | } 73 | 74 | return result; 75 | } 76 | 77 | /* Returns the AABB (Axis-Aligned Bounding Box) for this `Scene`. */ 78 | AABB get_aabb() const override { 79 | return aabb; 80 | } 81 | 82 | /* Returns the list of primitive components of all `Hittable` objects in this `Scene`, 83 | which contributes to more efficient and complete `BVH`s. See the comments for this 84 | function in `Hittable` for a more detailed explanation. */ 85 | std::vector> get_primitive_components() const override { 86 | 87 | /* Simply collect all the primitive components for each object in turn into `ret`, 88 | using the `get_primitive_components()` function that all `Hittable`s must have. */ 89 | std::vector> ret; 90 | for (const auto &obj : objects) { 91 | if (auto obj_components = obj->get_primitive_components(); !obj_components.empty()) { 92 | /* If `get_primitive_components()` returns a list of `Hittable` components for 93 | `obj`, then add those components into `ret`. */ 94 | ret.insert(ret.end(), std::make_move_iterator(obj_components.begin()), 95 | std::make_move_iterator(obj_components.end())); 96 | } else { 97 | /* If `get_primitive_components()` returns an empty list, that means the current 98 | object `obj` is already an indivisible unit; it itself is a primitive, so we will 99 | just add itself to `ret`. */ 100 | ret.push_back(obj); 101 | } 102 | } 103 | 104 | return ret; 105 | } 106 | 107 | /* Prints every object in this `Scene` to the `std::ostream` specified by `os`. */ 108 | void print_to(std::ostream &os) const override { 109 | os << "Scene with " << size() << " objects:\n"; 110 | for (const auto &object : objects) { 111 | object->print_to(os); 112 | os << '\n'; 113 | } 114 | os << std::flush; 115 | } 116 | 117 | Scene() = default; 118 | 119 | /* Constructs a `Scene` with objects given in `objs` */ 120 | Scene(std::span> objects_) { 121 | for (const auto &i : objects_) { 122 | add(i); 123 | } 124 | } 125 | }; 126 | 127 | #endif -------------------------------------------------------------------------------- /include/math/interval.h: -------------------------------------------------------------------------------- 1 | #ifndef INTERVAL_H 2 | #define INTERVAL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static_assert(std::numeric_limits::has_infinity, "`double` needs to have " 10 | "positive infinity"); 11 | static_assert(std::numeric_limits::is_iec559, "`double` needs to be IEEE754 " 12 | "in order for -std::numeric_limits::infinity() to equal negative infinity"); 13 | 14 | /* The `Interval` class represents an interval from `min` to `max` (both of type 15 | `double`), and provides helper functions both open and closed intervals. */ 16 | struct Interval { 17 | constexpr static double DOUBLE_INF = std::numeric_limits::infinity(); 18 | 19 | /* `min`/`max` = Minimum/maximum values in the interval, respectively */ 20 | double min, max; 21 | 22 | /* Returns the minimum bound of this `Interval` if `index` is 0, and otherwise returns the 23 | maximum bound of this `Interval`. */ 24 | const auto& operator[] (size_t index) const {return (index ? max : min);} 25 | 26 | /* Returns the midpoint of this `Interval`; that is, `(min + max) / 2`. */ 27 | auto midpoint() const {return std::midpoint(min, max);} 28 | /* Returns the size of this `Interval`; that is, `max - min`. */ 29 | auto size() const {return max - min;} 30 | /* Returns `true` if this `Interval`, viewed as an INCLUSIVE range, is empty 31 | (that is, if `max - min < 0`). */ 32 | bool is_empty_inclusive() const {return size() < 0;} 33 | /* Returns `true` if this `Interval`, viewed as an EXCLUSIVE range, is empty 34 | (that is, if `max - min <= 0`). */ 35 | bool is_empty_exclusive() const {return size() <= 0;} 36 | 37 | /* Returns `true` if `d` is in the INCLUSIVE range [min, max]. */ 38 | bool contains_inclusive(double d) const {return min <= d && d <= max;} 39 | /* Returns `true` if `d` is in the EXCLUSIVE range (min, max). */ 40 | bool contains_exclusive(double d) const {return min < d && d < max;} 41 | /* Returns the value of `d` when it is clamped to the range [min, max]. */ 42 | auto clamp(double d) const {return (d <= min ? min : (d >= max ? max : d));} 43 | 44 | /* Updates (possibly expands) this `Interval` to also contain the `Interval` `other`. */ 45 | void merge_with(const Interval &other) { 46 | min = std::fmin(min, other.min); 47 | max = std::fmax(max, other.max); 48 | } 49 | 50 | /* Updates (possibly expands) this `Interval` to contain `d`. */ 51 | void merge_with(double d) { 52 | min = std::fmin(min, d); 53 | max = std::fmax(max, d); 54 | } 55 | 56 | /* Setters */ 57 | 58 | /* Pads this interval with `padding`; that is, expands this `Interval` by `padding` on both 59 | ends. */ 60 | auto& pad_with(double padding) { 61 | min -= padding; 62 | max += padding; 63 | return *this; 64 | } 65 | 66 | /* --- CONSTRUCTORS --- */ 67 | 68 | /* Constructs an `Interval` from `min_` to `max_`. */ 69 | Interval(double min_, double max_) : min{min_}, max{max_} {} 70 | 71 | /* --- NAMED CONSTRUCTORS --- */ 72 | 73 | /* Returns an empty interval; specifically, the interval `(DOUBLE_INF, -DOUBLE_INF)`. 74 | The rationale behind using `(DOUBLE_INF, -DOUBLE_INF)` is that it allows for easier 75 | computation of intersections of intervals, which is needed in `AABB::is_hit_by()`.*/ 76 | static auto empty() {return Interval(DOUBLE_INF, -DOUBLE_INF);} 77 | 78 | /* Returns the interval of all non-negative integers: `[0, DOUBLE_INF)`. */ 79 | static auto nonnegative() {return Interval(0, DOUBLE_INF);} 80 | 81 | /* Returns the interval with minimum `min_` and maximum `DOUBLE_INF`. */ 82 | static auto with_min(double min_) {return Interval(min_, DOUBLE_INF);} 83 | 84 | /* Returns the interval with maximum `max_` and minimum `-DOUBLE_INF`. */ 85 | static auto with_max(double max_) {return Interval(-DOUBLE_INF, max_);} 86 | 87 | /* Returns the interval of all real numbers */ 88 | static auto universe() {return Interval(-DOUBLE_INF, DOUBLE_INF);} 89 | 90 | /* Returns the minimum-size interval that fully contains both of the intervals `a` and `b`; 91 | that is, returns the interval that would result if `a` and `b` were to be merged into a single 92 | interval. Thus, this returns the interval from `min(a.min, b.min)` to `max(a.max, b.max)`. */ 93 | static auto merge(const Interval &a, const Interval &b) { 94 | return Interval(std::fmin(a.min, b.min), std::fmax(a.max, b.max)); 95 | } 96 | }; 97 | 98 | /* Overload `operator<<` to allow printing `Interval`s to output streams */ 99 | std::ostream& operator<< (std::ostream &os, const Interval &i) { 100 | os << "Interval {min: " << i.min << ", max: " << i.max << "} "; 101 | return os; 102 | } 103 | 104 | #endif -------------------------------------------------------------------------------- /include/math/ray3d.h: -------------------------------------------------------------------------------- 1 | #ifndef RAY3D_H 2 | #define RAY3D_H 3 | 4 | #include 5 | #include "math/vec3d.h" 6 | 7 | /* `Ray3D` represents a ray in 3D space; that is, an origin (a 3D point) and 8 | a direction (a 3D vector). */ 9 | struct Ray3D { 10 | /* `origin` = a 3D point representing the origin of the ray. (0, 0, 0) by default. */ 11 | Point3D origin{0, 0, 0}; 12 | /* `dir` = a 3D vector representing the direction of the ray. (0, 0, 0) by default. */ 13 | Vec3D dir{0, 0, 0}; 14 | 15 | /* Returns the point located at time `t` on this ray */ 16 | auto operator() (double t) const {return origin + t * dir;} 17 | }; 18 | 19 | /* Overload `operator<<` to allow printing `Ray3D`s to output streams */ 20 | std::ostream& operator<< (std::ostream &os, const Ray3D &ray) { 21 | os << "Ray3D {origin: " << ray.origin << ", dir: " << ray.dir << "}"; 22 | return os; 23 | } 24 | 25 | #endif -------------------------------------------------------------------------------- /include/math/vec3d.h: -------------------------------------------------------------------------------- 1 | #ifndef VEC3D_H 2 | #define VEC3D_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "util/rand_util.h" 8 | 9 | /* Vec3D represents a 3-dimensional vector, or equivalently, a point in 3D space. */ 10 | struct Vec3D { 11 | /* Three `double` components, all 0 by default. 12 | 13 | Note: Later, we may no longer set the components to 0 by default with a member default 14 | value, so that `Vec3D` can be trivially constructible. */ 15 | double x = 0, y = 0, z = 0; 16 | 17 | /* Returns the coordinate of this `Vec3D` on the axis specified by `axis`. Specifically, 18 | returns the `x`-, `y`-, and `z`- coordinate if `axis` is `0`, `1`, or `2`, respectively. */ 19 | const auto& operator[] (size_t axis) const {return (axis == 0 ? x : (axis == 1 ? y : z));} 20 | auto& operator[] (size_t axis) {return (axis == 0 ? x : (axis == 1 ? y : z));} 21 | 22 | /* Mathematical negation, +=, -=, *=, /= operators */ 23 | 24 | /* Element-wise negation for `Vec3D`s */ 25 | auto operator-() const {return Vec3D{-x, -y, -z};} 26 | 27 | /* Element-wise addition assignment operator for `Vec3D`s */ 28 | auto& operator+= (const Vec3D &rhs) {x += rhs.x; y += rhs.y; z += rhs.z; return *this;} 29 | /* Element-wise subtraction assignment operator for `Vec3D`s */ 30 | auto& operator-= (const Vec3D &rhs) {x -= rhs.x; y -= rhs.y; z -= rhs.z; return *this;} 31 | /* Element-wise multiplication assignment operator for `Vec3D`s */ 32 | auto& operator*= (double d) {x *= d; y *= d; z *= d; return *this;} 33 | /* Element-wise division assignment operator for `Vec3D`s */ 34 | auto& operator/= (double d) {return *this *= (1 / d);} /* Multiply by 1/d for less divisions */ 35 | 36 | /* Compute magnitude (length) of this vector */ 37 | auto mag() const {return std::sqrt(x * x + y * y + z * z);} 38 | /* Compute squared magnitude (squared length) of this vector */ 39 | auto mag_squared() const {return x * x + y * y + z * z;} 40 | 41 | /* Compute unit vector (forward declared) since it requires operator/, which has 42 | not yet been defined */ 43 | 44 | Vec3D unit_vector() const; 45 | 46 | /* Returns `true` if all three components have magnitude strictly less than `epsilon`. */ 47 | auto near_zero(double epsilon = 1e-8) { 48 | return (std::fabs(x) < epsilon) && (std::fabs(y) < epsilon) && (std::fabs(z) < epsilon); 49 | } 50 | 51 | /* --- NAMED CONSTRUCTORS --- */ 52 | 53 | /* Returns a vector with all components set to 0; the zero vector. */ 54 | static auto zero() { 55 | return Vec3D{0, 0, 0}; 56 | } 57 | 58 | /* Generate random vector with real components in the interval [min, max] ([0, 1] by default) */ 59 | static auto random(double min = 0, double max = 1) { 60 | return Vec3D{rand_double(min, max), rand_double(min, max), rand_double(min, max)}; 61 | } 62 | 63 | /* Generates an uniformly random unit vector */ 64 | static auto random_unit_vector() { 65 | /* Generate a random vector in the unit sphere, then normalize it (turn it into 66 | an unit vector). This ensures that each unit vector has a theoretically equal 67 | probability of being generated, unlike simply returning Vec3D::random(-1, 1). */ 68 | Vec3D result; 69 | do { 70 | result = Vec3D::random(-1, 1); 71 | } while (!(result.mag_squared() < 1)); 72 | 73 | /* Return the unit vector of the random vector in the unit sphere */ 74 | return result.unit_vector(); 75 | } 76 | 77 | /* Generates an uniformly random vector in the unit disk; that is, generates a 78 | vector (a, b, 0) where a^2 + b^2 = 1. */ 79 | static auto random_vector_in_unit_disk() { 80 | Vec3D result; 81 | do { 82 | result = Vec3D{rand_double(-1, 1), rand_double(-1, 1), 0}; 83 | } while (!(result.mag_squared() < 1)); 84 | return result; 85 | } 86 | 87 | /* Generate a random unit vector that is in the same hemisphere as `surface_normal`, 88 | which is an OUTWARD surface normal at the same point on some surface as the random unit 89 | vector to be generated. Thus, this function returns an unit vector pointing out of 90 | a surface, from the same point as the given outward surface normal `surface_normal`. 91 | as not been defined yet. */ 92 | static auto random_unit_vector_on_hemisphere(const Vec3D &surface_normal); 93 | }; 94 | 95 | /* Math utility functions; vector addition/subtraction, multiplication/division by a scalar */ 96 | 97 | /* Performs element-wise addition on two `Vec3D`s */ 98 | auto operator+ (const Vec3D &a, const Vec3D &b) {auto ret = a; ret += b; return ret;} 99 | /* Performs element-wise subtraction on two `Vec3D`s */ 100 | auto operator- (const Vec3D &a, const Vec3D &b) {auto ret = a; ret -= b; return ret;} 101 | /* Performs element-wise multiplication by `d` on a `Vec3D` */ 102 | auto operator* (const Vec3D &a, double d) {auto ret = a; ret *= d; return ret;} 103 | /* Performs element-wise multiplication by `d` on a `Vec3D` */ 104 | auto operator* (double d, const Vec3D &a) {return a * d;} 105 | /* Performs element-wise division by `d` on a `Vec3D` */ 106 | auto operator/ (const Vec3D &a, double d) {auto ret = a; ret /= d; return ret;} 107 | 108 | /* Dot and cross product of two vectors. Note that these are not static because 109 | in OOP, static functions ought to not depend on the values of the member variables, 110 | or on the existence of actual instances of the class which they are a static member of. 111 | See https://softwareengineering.stackexchange.com/a/113034/426687. */ 112 | 113 | /* Computes the dot product of `a` and `b` */ 114 | auto dot(const Vec3D &a, const Vec3D &b) {return a.x * b.x + a.y * b.y + a.z * b.z;} 115 | /* Computes the cross product of `a` and `b` */ 116 | auto cross(const Vec3D &a, const Vec3D &b) { 117 | return Vec3D{a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; 118 | } 119 | 120 | /* Overload operator<< to allow printing `Vec3D`s to output streams */ 121 | std::ostream& operator<< (std::ostream& os, const Vec3D &v) { 122 | os << "(" << v.x << ", " << v.y << ", " << v.z << ")"; 123 | return os; 124 | } 125 | 126 | /* Returns the unit vector of this `Vec3D`. */ 127 | Vec3D Vec3D::unit_vector() const { 128 | /* Unit vector is found by dividing the vector by its length/magnitude */ 129 | return *this / this->mag(); 130 | } 131 | 132 | /* Now implement `random_unit_vector_on_hemisphere`, after `dot` has been defined. */ 133 | 134 | auto Vec3D::random_unit_vector_on_hemisphere(const Vec3D &surface_normal) { 135 | auto result = Vec3D::random_unit_vector(); 136 | /* If the angle between `result` and the surface normal is less than 90 degrees, 137 | then `result` points in the correct hemisphere; that is, out of the surface. */ 138 | return (dot(surface_normal, result) > 0 ? result : -result); 139 | } 140 | 141 | /* Returns the resulting direction when the direction vector `dir` is reflected across the unit 142 | normal vector `unit_normal`, where the endpoint of `dir` is assumed to coincide with the origin 143 | of `unit_normal`. The returned direction vector will thus have the same magnitude as `dir`. */ 144 | auto reflected(const Vec3D &dir, const Vec3D &unit_normal) { 145 | /* Observe that the reflected direction is equivalent to `dir + 2*b`, where `b` is the 146 | vector parallel to `unit_normal` with magnitude `|dir|cos(theta)` (where `theta` is 147 | the angle made between the incoming vector and `unit_normal`). Now, observe that 148 | `|dir|cos(theta) = |dir||unit_normal|cos(theta)` since `|unit_normal| = 1`. Finally, 149 | observe that `theta` is the supplementary angle of the actual angle `x` between 150 | `dir` and `unit_normal`, and so `cos(theta) = -cos(x)`. Thus, `b` is the vector 151 | parallel to `unit_normal` with magnitude `|dir||unit_normal|cos(theta) 152 | = -|dir||unit_normal|cos(x) = -dot(dir, unit_normal)`, and so the reflected direction 153 | (which is `dir - 2 * b`) is calculated as follows: */ 154 | return dir - 2 * dot(dir, unit_normal) * unit_normal; 155 | } 156 | 157 | /* 158 | @brief Returns the direction of the resulting ray when an incident ray with direction `unit_vec` 159 | is refracted at the interface (boundary) between two isotropic (uniform) media with a given 160 | refractive index ratio. If the ray cannot be refracted (under Snell's Law), then an empty 161 | `std::optional` object is returned. 162 | @param `unit_dir`: The unit direction of the incoming ray. Assumed to be an unit vector. 163 | @param `unit_normal`: An unit normal to the interface, pointing on the side of `unit_dir`. 164 | @param `refractive_index_ratio`: The ratio of the refractive index of the medium the ray is 165 | initially passing through, to the refractive index of the medium the ray is passing into. 166 | For instance, if going from a medium with a refractive index of 1.5 to a medium with a refractive 167 | index of 2, `refractive_index_ratio` should be set to 1.5 / 2 = 0.75. */ 168 | std::optional refracted(const Vec3D &unit_dir, const Vec3D &unit_normal, 169 | double refractive_index_ratio) 170 | { 171 | /* Use Snell's Law to compute the direction of the unit vector `v` after transitioning 172 | from a medium with refractive index x to a medium with refractive index y, where 173 | `refractive_index_ratio` = x / y. */ 174 | 175 | /* Use `std::fmin` to bound `cos_theta` from above by 1., just in case a floating-point 176 | inaccuracy occurs which makes it a little greater than 1.. This prevents the computation 177 | of `sin_theta` from taking the square root of a negative number. */ 178 | auto cos_theta = std::fmin(dot(-unit_dir, unit_normal), 1.); 179 | auto sin_theta = std::sqrt(1 - cos_theta * cos_theta); 180 | 181 | /* By Snell's law, n1sin(theta_1) = n2sin(theta_2), where n1 and n2 are the refractive 182 | indices of the initial and final mediums, theta_1 is the angle between the incident ray 183 | and the surface normal on the side of the initial medium, and theta_2 is the angle between 184 | the resulting ray and the surface normal on the side of the final medium. Clearly, a 185 | solution to theta_2 exists if and only if (n1/n2) * sin(theta_1) <= 1 (we already know this 186 | is >= 0 because n1/n2 >= 0 and 0 <= theta_1 <= 90 degrees). Thus, if 187 | `refractive_index_ratio * sin_theta` > 1, there is no solution and thus no refracted ray. */ 188 | if (refractive_index_ratio * sin_theta > 1) { 189 | /* This ray cannot be refracted under Snell's Law as-is. We report this by returning an 190 | empty `std::optional`, and allow the caller to decide what should be done instead. */ 191 | return {}; 192 | } 193 | 194 | /* Individually compute the components of the resulting vector that are perpendicular 195 | and parallel to the surface normal on the side of the final medium, and then sum them 196 | to get the resulting vector. */ 197 | auto v_out_perp = refractive_index_ratio * (unit_dir + cos_theta * unit_normal); 198 | auto v_out_para = -std::sqrt(std::fabs(1 - v_out_perp.mag_squared())) * unit_normal; 199 | return v_out_perp + v_out_para; 200 | } 201 | 202 | /* `Point3D` is a type alias for `Vec3D`, declared to improve clarity in the code */ 203 | using Point3D = Vec3D; 204 | 205 | #endif -------------------------------------------------------------------------------- /include/shapes/box.h: -------------------------------------------------------------------------------- 1 | #ifndef BOX_SURFACE_H 2 | #define BOX_SURFACE_H 3 | 4 | #include 5 | #include "base/hittable.h" 6 | #include "shapes/parallelogram.h" 7 | #include "base/scene.h" 8 | 9 | /* `Box` is an abstraction over a 3D box - a rectangular prism - in 3D space. */ 10 | class Box : public Hittable { 11 | /* `faces` holds the six faces of this `Box`. */ 12 | Scene faces; 13 | /* `material`: The material for the surface of this `Box`. */ 14 | std::shared_ptr material; 15 | 16 | public: 17 | 18 | /* A `Box` is practically identical to a `Scene`, so the abstract functions inherited from 19 | `Hittable` can just be implemented in terms of the same functions for `Scene`. */ 20 | 21 | /* Returns a `hit_info` representing the earliest (minimum hit-time) intersection of the ray 22 | `ray` with this `Box` in the time itnerval `ray_times`. If no such intersection exists, then 23 | an empty `std::optional` is returned. */ 24 | std::optional hit_by(const Ray3D &ray, const Interval &ray_times) const override { 25 | /* Simply just delegate this to the `Scene`; this returns the earliest intersection of 26 | `ray` in the time interval `ray_times` with the six faces of the box, as desired. */ 27 | return faces.hit_by(ray, ray_times); 28 | } 29 | 30 | /* Returns the primitive components of this `Box`; namely, its six `Parallelogram` 31 | faces, which contributes to the construction of more efficient and complete 32 | `BVH`s. See the comments in `Hittable::get_primitive_components()` for a more 33 | detailed explanation. */ 34 | std::vector> get_primitive_components() const override { 35 | return faces.get_primitive_components(); 36 | } 37 | 38 | /* Prints this `Box` to the `std::ostream` specified by `os`. */ 39 | void print_to(std::ostream &os) const override { 40 | os << "Box {faces: " << faces << "} " << std::flush; 41 | } 42 | 43 | /* Returns the `AABB` for this `Box`. */ 44 | AABB get_aabb() const override { 45 | /* Once more, we delegate this to the `Scene` field. */ 46 | return faces.get_aabb(); 47 | } 48 | 49 | /* --- CONSTRUCTORS --- */ 50 | 51 | /* Constructs a `Box` with opposite vertices `vertex` and `opposite_vertex`, as well as 52 | material specified by `material_`. */ 53 | Box(const Point3D &vertex, const Point3D &opposite_vertex, 54 | std::shared_ptr material_) 55 | : material{std::move(material_)} 56 | { 57 | 58 | /* Compute `min_coordinates` and `max_coordinates`, which respectively are the points with 59 | all minimal and all maximal x-, y-, and z- coordinates from the given points `vertex` and 60 | `opposite_vertex`. Observe that `min_coordinates` and `max_coordinates` are both vertices 61 | of the box; in fact, they are opposite vertices of the box. */ 62 | Point3D min_coordinates, max_coordinates; 63 | for (int i = 0; i < 3; ++i) { 64 | min_coordinates[i] = std::fmin(vertex[i], opposite_vertex[i]); 65 | max_coordinates[i] = std::fmax(vertex[i], opposite_vertex[i]); 66 | } 67 | 68 | /* Precompute the edge vectors of the box; namely, the x-, y-, and z- displacement vectors 69 | from `min_coordinates` to `max_coordinates`. */ 70 | auto side_x = Vec3D{max_coordinates.x - min_coordinates.x, 0, 0}; 71 | auto side_y = Vec3D{0, max_coordinates.y - min_coordinates.y, 0}; 72 | auto side_z = Vec3D{0, 0, max_coordinates.z - min_coordinates.z}; 73 | 74 | /* A `Box` is represented by 6 parallelogram (specifcially, rectangular) faces. 75 | `min_coordinates` and `max_coordinates` eachlie on three of those faces. We add 76 | the 6 faces to `faces`. */ 77 | faces.add(std::make_shared(min_coordinates, side_x, side_y, material)); 78 | faces.add(std::make_shared(min_coordinates, side_x, side_z, material)); 79 | faces.add(std::make_shared(min_coordinates, side_y, side_z, material)); 80 | 81 | faces.add(std::make_shared(max_coordinates, -side_x, -side_y, material)); 82 | faces.add(std::make_shared(max_coordinates, -side_x, -side_z, material)); 83 | faces.add(std::make_shared(max_coordinates, -side_y, -side_z, material)); 84 | } 85 | }; 86 | 87 | #endif -------------------------------------------------------------------------------- /include/shapes/parallelogram.h: -------------------------------------------------------------------------------- 1 | #ifndef PARALLELOGRAM_H 2 | #define PARALLELOGRAM_H 3 | 4 | #include "math/vec3d.h" 5 | #include "base/hittable.h" 6 | #include "base/material.h" 7 | 8 | /* `Parallelogram` is an abstraction over a 2D parallelogram in 3D space. */ 9 | class Parallelogram : public Hittable { 10 | /* A 2D parallelogram in 3D space is represented by a given vertex, and two vectors 11 | corresponding to the two sides of the parallelogram. */ 12 | 13 | /* `vertex` = A given vertex of the parallelogram. */ 14 | Point3D vertex; 15 | /* `side1` = A vector representing the first side of the parallelogram, starting at the given 16 | vertex `vertex`. As a result, `vertex + side1` yields the vertex adjacent to `vertex` 17 | along side 1. 18 | 19 | `side2` = A vector representing the second side of the parallelogram, starting at the given 20 | vertex `vertex`. As a result, `vertex + side2` yields the vertex adjacent to `vertex` 21 | along side 2. 22 | 23 | And by the Parallelogram Vector Addition Rule, the vertex opposite to `vertex` is equivalent 24 | to `vertex + side1 + side2`. */ 25 | Vec3D side1, side2; 26 | /* `material` = The material of this `Parallelogram` object. */ 27 | std::shared_ptr material; 28 | 29 | /* `unit_plane_normal` is a unit vector normal to the plane containing this `Parallelogram`. 30 | Specifically, `unit_plane_normal` is the unit vector of `cross(side1, side2)`, 31 | which we know is a normal vector to the plane of this `Parallelogram`. We precompute this 32 | quantity because (a) we will use it in some of our computations in `Parallelogram::hit_by()`, 33 | and (b) it will be the `outward_unit_surface_normal` field of every `hit_info` returned 34 | from `Parallelogram::hit_by()`, so it is beneficial to compute it exactly once. 35 | 36 | About the "outward" in `outward_unit_surface_normal`, note that there is no singular definition 37 | of "outside" and "inside" for a flat object such as a parallelogram. Here, we declare that the 38 | direction of the normal `cross(side1, side2)` is outward-facing. You could say instead that the 39 | direction of `cross(side1, side2)` is inward-facing; the only difference would be that the 40 | `hit_from_outside` field of all `hit_info`s returned from `Parallelogram::hit_by()` will be 41 | flipped. */ 42 | Vec3D unit_plane_normal; 43 | /* `scaled_plane_normal` = n / dot(n, n), where `n = cross(side1, side2)`, a normal vector 44 | to the plane containing this `Parallelogram`. We precompute this quantity to be used in 45 | `Parallelogram::hit_by()`. */ 46 | Vec3D scaled_plane_normal; 47 | /* `aabb` = The AABB (Axis-Aligned Bounding Box) for this `Parallelogram`. */ 48 | AABB aabb; 49 | 50 | public: 51 | 52 | /* There are three steps to perform a ray-parallelogram intersection check: 53 | 1. Find the unique plane containing the parallelogram. 54 | 2. Find the intersection point of the ray with that parallelogram-containing plane. 55 | 3. Determine if the hit point of the ray on the plane lies within the parallelogram itself. 56 | 57 | STEP 1: Find the plane that contains the parallelogram. 58 | Remember that a plane is fully specified by two things: a normal vector to the plane and a 59 | point on the plane. Now, we want to determine the plane containing the parallelogram. Firstly, 60 | observe that a vector is normal to the parallelogram-containing plane iff it is normal to 61 | the parallelogram itself. And so, it suffices to find a vector normal to the parallelogram; 62 | this can be done by taking the cross product of the two side vectors: `cross(side1, side2)`. 63 | Now, we need an arbitrary point on the parallelogram-containing plane; for this, we can just 64 | take the given vertex of the parallelogram `vertex`, which we know to be on the parallelogram 65 | (and so we know it to be on the parallelogram-containing plane as well). 66 | 67 | We have found a normal vector to the plane containing this parallelogram: `cross(side1, side2)`. 68 | We also have a point on the plane: `vertex`. Now, given a normal vector to a plane `n` and a 69 | point `p` on that plane, the plane consists of exactly the points `P` that satisfy the equation 70 | `dot(n, P - p) = 0` (because the equation is equivalent to the statement that a point `P` is 71 | on a plane iff the vector from some point on the plane to it is normal to the plane's normal 72 | vector, which is clearly true). An equivalent and more useful formulation of this equation 73 | that we will use is `dot(n, P) = dot(n, p)`. Finally, because all nonzero scalar multiples 74 | of a normal vector to a plane are also normal to a plane (as multiplying a vector by a nonzero 75 | scalar preserves its direction), and because we know that `cross(side1, side2)` is normal to 76 | the parallelogram-containing plane, we have that any `k * cross(side1, side2)` for a nonzero 77 | real number `k` is normal to the parallelogram-containing plane. So, if we let `n` equal 78 | `cross(side1, side2)`, then our parallelogram-containing plane consists of exactly the points 79 | `P` where `dot(kn, P) = dot(kn, vertex)` for any fixed nonzero real number `k`. This completes 80 | the first step. 81 | 82 | STEP 2: Find the intersection point of the ray with the parallelogram-containing plane. 83 | Let the ray be defined by R(t) = O + tD. Now, we first solve for the hit time of the ray with 84 | the plane. Using the equation we derived in Step 1, solving for the hit time is equivalent to 85 | solving `dot(kn, R(t)) = dot(kn, vertex)` for `t`, where `k` is any nonzero real number. 86 | Then, substituting O + tD for R(t), this equation becomes `dot(kn, O + tD) = dot(kn, vertex)`, 87 | and so `dot(kn, O) + dot(kn, tD) = dot(kn, vertex)`. Solving, we find that 88 | `t = dot(kn, vertex - O) / dot(kn, D)`. 89 | 90 | However, it is possible for a ray to not intersect the plane at all; this happens when the ray 91 | is parallel to the plane and the ray's origin is not on the plane. A ray is parallel to the 92 | plane iff it is perpendicular to the plane's normal vector, which holds iff `dot(kn, D)` 93 | equals 0 (where `D` is the direction vector of the ray, remember). Observe that this corresponds 94 | to the case where calculating `t` results in a division by 0 (because the denominator of the 95 | fraction that equals `t` is also `dot(kn, d)`); clearly, `t` does not exist in that case. 96 | In practice, we make two simplifications: firstly, we immediately reject all rays that are 97 | parallel to the plane, even if the ray's origin is on the plane (because while such a ray does 98 | indeed intersect the plane (at infinitely many points, in fact), it causes problems insofar that 99 | the direction of the `outward_unit_surface_normal` from the intersection point would be unclear, 100 | and that fundamentally, a plane is infinitely thin, and there's no analogy to real life for a 101 | ray (photon) colliding with an infinitely-thin edge, which is what would happen in this case). 102 | The second simplification we will make is that we will just reject all rays where `dot(kn, D)` 103 | is small (not necessarily exactly 0), to avoid numerical issues. In my implementation below, 104 | I just reject all rays where `|dot(kn, D)| < 1e-9`. 105 | Note: Because we are checking |dot(kn, d)| against a constant, the choice of `k` becomes 106 | important. In particular, if the components of `kn` are too small, then `dot(kn, D)` may 107 | have magnitude less than `1e-9` even when `kn` and `D` are not close to parallel, resulting 108 | in rays being incorrectly rejected. At the same time, though, if the components of `kn` are 109 | too big, then the calculation of `dot(kn, d)` will be more susceptible to floating-point 110 | inaccuracies. My solution is to always use the unit vector of `n` in the equation (so set 111 | k = 1 / |n|). This ensures some level of consistency across the magnitudes of the components 112 | of `kn`, and seems to work well from my experimentation. 113 | 114 | We now have shown that the hit time is `t = dot(kn, vertex - O) / dot(kn, D)` (assuming 115 | that `dot(kn, D)` is not too small; if it is, then again, we just assume the ray does 116 | not hit this parallelogram to avoid numerical issues). To find the hit point, just 117 | find `R(t) = O + tD` with that value of `t`. This completes the second step. 118 | 119 | STEP 3: Determine if the hit point of the ray with the parallelogram-containing plane also 120 | lies within the parallelogram itself. 121 | 122 | First, observe that {`side1`, `side2`} forms a basis for the parallelogram-containing plane, 123 | because `side1` and `side2` are linearly independent (since they are not parallel), and because 124 | they are two vectors in a space of dimension two (the plane). Now, we choose the "origin" of 125 | the parallelogram-containing plane to be `vertex`. Then, we know that 126 | (a) Because {`side1`, `side2`} is a basis of the plane, there exist unique scalars alpha/beta 127 | such that `hit_point = vertex + alpha * side1 + beta * side2`. 128 | (b) Because of the definition of a parallelogram, and because we chose the origin of the plane 129 | to be `vertex` itself, we know that the hit point is inside the parallelogram if and only if 130 | 0 <= alpha, beta <= 1, where `hit_point = vertex + alpha * side1 + beta * side2` as stated 131 | above. 132 | 133 | Thus, it remains to solve the equation `hit_point = vertex + alpha * side1 + beta * side2`. 134 | This is equivalent to solving `hit_point - vertex = alpha * side1 + beta * side2`. We can 135 | solve for `beta` by taking the cross product of both sides with `side1`: then we get 136 | `cross(side1, hit_point - vertex) = cross(side1, alpha * side1) + cross(side1, beta * side2)`. 137 | Then, `cross(side1, alpha * side1) = alpha * cross(side1, side1) = alpha * 0 = 0`, and so 138 | we have `cross(side1, hit_point - vertex) = cross(side1, beta * side2) 139 | = beta * cross(side1, side2)`. Now, because we cannot divide by vectors in vector math, we 140 | solve for `beta` by taking the dot product of both sides with `n` (wait until (*) to see why 141 | we specifically choose to take the dot product of both sides with `n`), the normal to the plane 142 | (which equals `cross(side1, side2)`). This yields `dot(n, cross(side1, hit_point - vertex)) 143 | = dot(n, beta * cross(side1, side2)) = beta * dot(n, cross(side1, side2))`. Now, because the 144 | dot product is a scalar, we are allowed to divide by both sides of the equation by 145 | `dot(n, cross(side1, side2))` (*). Finally, this yields 146 | `beta = dot(n, cross(side1, hit_point - vertex)) / dot(n, cross(side1, side2))`. Finally, 147 | because `n = cross(side1, side2)` by definition, we can simplify this to 148 | `beta = dot(n / dot(n, n), cross(side1, hit_point - vertex)). 149 | 150 | To find alpha` instead, go back to the original equation `hit_point - vertex = alpha * side1 151 | + beta * side2`, and take the cross product of both sides with `side2` instead of `side1`. 152 | This yields 153 | `alpha = dot(n / dot(n, n), cross(hit_point - vertex, side2))`. 154 | 155 | We will precompute `n / dot(n, n)` (we save this in `scaled_plane_normal`; it is equal to 156 | `n / |n|^2`, not sure if there's a better name for this) to increase performance. Finally, 157 | it suffices to check that `0 <= alpha, beta <= 1` to see if the hit point is inside the 158 | parallelogram itself. This completes the final step, and we are done. Now, check out the 159 | implementation below. 160 | 161 | (*) We would not be allowed to divide both sides of the equation by dot(n, cross(side1, side2)) 162 | if it was equal to 0. However, because `n` is the normal to the plane, it in fact is EQUAL to 163 | `cross(side1, side2)`, and so `dot(n, cross(side1, side2)) = dot(n, n) = |n|^2 > 0`, so we 164 | can always divide by `dot(n, cross(side1, side2))`. Note that this assumes that `n != 0`, but 165 | this is fine, because the only way that `n = 0` is if `side1` and `side2` are parallel, or when 166 | one of the sides has magnitude 0, both of which mean that the parallelogram itself is invalid. 167 | We assume that the parallelogram is valid before doing a ray-parallelogram intersection, so 168 | this will not affect our algorithm. 169 | */ 170 | 171 | /* `Parallelogram::hit_by(ray)` returns a `std::optional` object representing the 172 | minimum time of intersection (well, for parallelograms there's at most one time of intersection 173 | except when the ray is coplanar; we actually consider that case as a non-intersection to avoid 174 | numerical issues) in the time range specified by `ray_times`, of `ray` with this Parallelogram. 175 | If `ray` does not hit this Parallelogram in the interval `ray_times`, an empty `std::optional` 176 | object is returned. */ 177 | std::optional hit_by(const Ray3D &ray, const Interval &ray_times) const override { 178 | 179 | /* If the ray `ray` hits the plane containing this `Parallelogram`, then the hit time is 180 | equal to dot(kn, vertex - ray.origin) / dot(kn, ray.dir), where `n = cross(side1, side2)` 181 | and `k` is any nonzero real number. 182 | 183 | However, as explained above, before computing the hit time of the ray, we will first check 184 | if the ray intersects with the plane at all. For our purposes, as explained above, 185 | this is equivalent to checking if `dot(kn, ray.dir)` (the denominator of the fraction 186 | that equals the hit time) is smaller than some constant (1e-9 here). Finally, the choice 187 | of `k` is important, as explained above; we will choose to use `k = 1 / |n|` (so 188 | kn = `unit_plane_normal` exactly), because this prevents the components of `kn` from 189 | becoming too small (which could cause `hit_time_denominator` to be less than 1e-9 more 190 | often than it should) or too large (which could lead to floating-point inaccuracies in 191 | the computation of `dot(kn, ray.dir)`). When I initially used `kn = scaled_plane_normal 192 | = n / |n|^2`, parallelograms with coordinates on the order of 1e6 started rejecting all 193 | rays, resulting in them not rendering at all. From my testing,`unit_plane_normal` does 194 | not have the same issue. 195 | 196 | In summary, if the ray is parallel or very close to parallel to the parallelogram-containing 197 | plane, we just immediately reject the ray to avoid numerical issues. To do this, we return 198 | an empty `std::optional` if `dot(unit_plane_normal, ray.dir)` is less than our 199 | chosen constant. */ 200 | auto hit_time_denominator = dot(unit_plane_normal, ray.dir); /* Use `unit_plane_normal` */ 201 | if (std::fabs(hit_time_denominator) < 1e-9) { 202 | return {}; 203 | } 204 | 205 | /* If the ray is not parallel/very close to parallel to the plane, compute the hit time. 206 | First, make sure that the hit time is in the desired time range `ray_times`. */ 207 | auto hit_time = dot(unit_plane_normal, vertex - ray.origin) / hit_time_denominator; 208 | /* ^^ We must use `unit_plane_normal` here because we used `unit_plane_normal` in computing 209 | the `hit_time_denominator`. The normal we use must be the same in the numerator and the 210 | denominator. */ 211 | if (!ray_times.contains_exclusive(hit_time)) { /* Remember that `ray_times` is exclusive */ 212 | return {}; 213 | } 214 | 215 | /* Compute the hit point by evaluating the ray at `hit_time`. */ 216 | auto hit_point = ray(hit_time); 217 | /* Store `hit_point - vertex` in a variable. Note that `hit_point - vertex` is a planar 218 | vector, because both `hit_point` and `vertex` are in the parallelogram-containing plane. 219 | Specifically, it is the vector from the plane's origin (which we set to `vertex`) to 220 | the hitpoint of this ray with the plane. Thus, we call it `planar_hitpoint_vector`. */ 221 | auto planar_hitpoint_vector = hit_point - vertex; 222 | 223 | /* Compute the basis coordinates (using the basis {`side1`, `side2`}) of `hit_point` 224 | in this plane, again, using `vertex` as the origin of this plane. */ 225 | auto alpha = dot(scaled_plane_normal, cross(planar_hitpoint_vector, side2)); 226 | auto beta = dot(scaled_plane_normal, cross(side1, planar_hitpoint_vector)); 227 | 228 | /* The ray's hitpoint on the parallelogram-containing plane is in the parallelogram itself, 229 | iff `0 <= alpha, beta <= 1`. */ 230 | if (auto i = Interval(0, 1); i.contains_inclusive(alpha) && i.contains_inclusive(beta)) { 231 | /* We just pass in `unit_plane_normal`, which we precomputed, as the 232 | `outward_unit_surface_normal`. See the comments above the definition of 233 | `unit_plane_normal` for more explanation on why we do this.*/ 234 | return hit_info(hit_time, hit_point, unit_plane_normal, ray, material); 235 | } 236 | 237 | /* The ray hit the parallelogram-containing plane, but it did not hit the parallelogram 238 | itself, so we will still return an empty `std::optional`.*/ 239 | return {}; 240 | } 241 | 242 | /* Returns the AABB (Axis-Aligned Bounding Box) for this `Parallelogram`. */ 243 | AABB get_aabb() const override { 244 | return aabb; 245 | } 246 | 247 | /* Prints this `Parallelogram` to the `std::ostream` specified by `os`. */ 248 | void print_to(std::ostream &os) const override { 249 | os << "Parallelogram {vertex: " << vertex << ", side 1 vector: " << side1 250 | << ", side 2 vector: " << side2 << " } " << std::flush; 251 | } 252 | 253 | /* --- CONSTRUCTORS --- */ 254 | 255 | /* @brief Returns the parallelogram specified by a vertex `vertex_` (a `Point3D`) and 256 | sides `side1_` and `side2_` (which are `Vec3D`s starting from the vertex `vertex_`), 257 | with material specified by `material_`. 258 | 259 | @param vertex_ Some vertex of the parallelogram. 260 | @param side1_ The first side of the parallelogram as a `Vec3D`. `vertex_ + side1_` thus yields 261 | the vertex adjacent to `vertex_` along the first side of the parallelogram. 262 | @param side2_ The second side of the parallelogram as a `Vec3D`. `vertex_ + side2_` thus yields 263 | the vertex adjacent to `vertex_` along the second side of the parallelogram. 264 | @param material_ The material of the resulting parallelogram. 265 | 266 | @note This used to be a named constructor called `with_vertex_and_sides`; however, a named 267 | constructor would not work here because `Parallelogram` objects need to be stored within 268 | `std::shared_ptr`s. */ 269 | Parallelogram(const Point3D &vertex_, const Vec3D &side1_, const Vec3D &side2_, 270 | std::shared_ptr material_) 271 | : vertex{vertex_}, side1{side1_}, side2{side2_}, material{std::move(material_)} 272 | { 273 | /* Precompute `unit_plane_normal` and `scaled_plane_normal`; see the comments above 274 | their respective definitions. */ 275 | auto plane_normal = cross(side1, side2); /* This is `n` in those comments */ 276 | unit_plane_normal = plane_normal.unit_vector(); 277 | /* Note that dot(n, n) = |n|^2, so `scaled_plane_normal` = n / dot(n, n) = n / |n|^2. */ 278 | scaled_plane_normal = plane_normal / plane_normal.mag_squared(); 279 | 280 | /* The `AABB` for a `Parallelogram` is simply the minimum-size `AABB` that contains all the 281 | vertices of the parallelogram; that is, the `AABB` containing `vertex`, `vertex + side1`, 282 | `vertex + side2`, and the opposite vertex (which, remember, is calculated by 283 | `vertex + side1 + side2` for parallelograms). This is because parallelograms are convex, 284 | so a bounding box that contains the vertices contains the whole shape. 285 | 286 | Then, because parallelograms are 2D, the resulting AABB may have zero thickness in one 287 | of its dimensions (when it is parallel to one of the xy-/xz-/yz-planes), which can result 288 | in numerical issues. To avoid this, we pad every axis interval of the AABB; specifically, 289 | we ensure that every axis interval has length at least some small constant (1e-4 here). */ 290 | aabb = AABB::from_points({ 291 | vertex, /* The given vertex of this parallelogram */ 292 | vertex + side1, /* The vertex opposite to `vertex` along the first side */ 293 | vertex + side2, /* The vertex opposite to `vertex` along the second side */ 294 | vertex + side1 + side2 /* The vertex opposite to `vertex` in this parallelogram */ 295 | }).ensure_min_axis_length(1e-4); /* Make sure each axis of the AABB has length >= 1e-4 */ 296 | } 297 | }; 298 | 299 | #endif -------------------------------------------------------------------------------- /include/shapes/shapes.h: -------------------------------------------------------------------------------- 1 | #ifndef SHAPES_H 2 | #define SHAPES_H 3 | 4 | #include "shapes/box.h" 5 | #include "shapes/parallelogram.h" 6 | #include "shapes/sphere.h" 7 | 8 | #endif -------------------------------------------------------------------------------- /include/shapes/sphere.h: -------------------------------------------------------------------------------- 1 | #ifndef SPHERE_H 2 | #define SPHERE_H 3 | 4 | #include 5 | #include 6 | #include "base/hittable.h" 7 | #include "math/vec3d.h" 8 | #include "math/ray3d.h" 9 | #include "base/material.h" 10 | 11 | /* `Sphere` is an abstraction over a sphere in 3D space. */ 12 | struct Sphere : public Hittable { 13 | /* A sphere in 3D space is represented by its `center` and its `radius`. */ 14 | 15 | /* `center`: The center of this `Sphere`. */ 16 | Point3D center; 17 | /* `radius`: The radius of this `Sphere`. */ 18 | double radius; 19 | /* `material`: The material of this `Sphere`. */ 20 | std::shared_ptr material; 21 | /* `aabb` = The AABB (Axis-Aligned Bounding Box) for this `Sphere`. */ 22 | AABB aabb; 23 | 24 | /* A ray hits a sphere iff it intersects its surface. Now, a sphere with radius R centered at 25 | C = (sx, sy, sz) can be expressed as the vector equation (P - C) dot (P - C) = R^2; any point 26 | P that satisfies this equation is on the sphere. So, the ray P(t) = A + tB hits the sphere if 27 | there exists some t for which (P(t) - C) dot (P(t) - C) = R^2; if we can find a solution to 28 | the equation ((A + tb) - C) dot ((A + tb) - C) = R^2. Grouping the t's, rearranging, and 29 | expanding, we find that t^2(b dot b) + 2tb dot (A - C) + (A - C) dot (A - C) - r^2 = 0. 30 | This is a quadratic in t; at^2 + bt + c = 0, where 31 | a = b dot b 32 | b = 2b dot (A - C) 33 | c = (A - C) dot (A - C) - r^2 34 | So, we use the Quadratic formula to find if any t exists which solves the equation. 35 | 36 | Note: In reality, we need to find a non-negative solution of t, because the ray heads in 37 | that direction. The current function would return true if the LINE intersects the sphere, 38 | not necessarily the ray (so if the sphere was located behind the camera, it could still be 39 | drawn). We will fix this in the future. */ 40 | 41 | /* `Sphere::hit_by(ray)` returns a `std::optional` object with information about 42 | the earliest intersection in the time range specified by `ray_times`, of `ray` with this Sphere. 43 | If `ray` does not hit this Sphere in the interval `ray_times`, an empty `std::optional` object 44 | is returned. */ 45 | std::optional hit_by(const Ray3D &ray, const Interval &ray_times) const override { 46 | 47 | /* Set up quadratic formula calculation */ 48 | auto center_to_origin = ray.origin - center; 49 | auto a = dot(ray.dir, ray.dir); 50 | auto b_half = dot(ray.dir, center_to_origin); 51 | auto c = dot(center_to_origin, center_to_origin) - radius * radius; 52 | auto discriminant_quarter = b_half * b_half - a * c; 53 | 54 | /* Quadratic has no solutions whenever the discriminant is negative */ 55 | if (discriminant_quarter < 0) {return {};} 56 | 57 | /* If the quadratic has solutions, find the smallest one in the range `ray_times` */ 58 | auto discriminant_quarter_sqrt = std::sqrt(discriminant_quarter); /* Evaluate this once */ 59 | /* Check smaller root (hit time) first, because we want to find the earliest intersection 60 | of the ray `ray` with this Sphere in the given time range `ray_times`. */ 61 | auto root = (-b_half - discriminant_quarter_sqrt) / a; 62 | 63 | if (!ray_times.contains_exclusive(root)) { 64 | /* Smaller root not in the range `ray_times`, try the other root */ 65 | root = (-b_half + discriminant_quarter_sqrt) / a; 66 | 67 | if (!ray_times.contains_exclusive(root)) { 68 | /* No root in the range `ray_times`, return {} */ 69 | return {}; 70 | } 71 | } 72 | 73 | /* Shadow acne occurs when `hit_time` is a little too large; that causes `hit_point` 74 | to be inside the Sphere, and so the next reflected ray will hit the inside of the sphere 75 | at a very small time and then continue to bounce off the inside of the sphere over and 76 | over. I fix this in a different method from the book: the book ignores small hit times, 77 | and I just decrease the hit time. Specifically, if `hit_time` > 1e-8, I simply subtract 78 | 1e-8 from it, and otherwise I halve it. This ensures that `hit_point` will not be inside 79 | this Sphere. 80 | 81 | Fix has been disabled for now in favor of the book's fix (which is to make `ray_times` 82 | the interval (some small constant, inf) instead of (0, inf)). My fix, which was to shift 83 | the hit point back along the original ray, results in rays never being able to go inside 84 | Spheres. This is bad because for rays that are refracted (for Dielectric spheres), they 85 | need to go inside. */ 86 | // root = (root > 1e-8 ? root - 1e-8 : root / 2); 87 | 88 | auto hit_point = ray(root); /* Evaluate this once */ 89 | /* Finding the outward unit surface normal at the `hit_point` is exceptionally simple and 90 | efficient for spheres: an outward surface normal to any point p on the sphere's surface 91 | is parallel to p - sphere_center. Furthermore, p - sphere_center has magnitude equal to 92 | the sphere's radius, so we can simply divide by `radius` to find the unit vector of the 93 | outward surface normal. */ 94 | auto outward_unit_normal = (hit_point - center) / radius; 95 | return hit_info(root, hit_point, outward_unit_normal, ray, material); 96 | } 97 | 98 | /* Returns the AABB (Axis-Aligned Bounding Box) for this `Sphere`. */ 99 | AABB get_aabb() const override { 100 | return aabb; 101 | } 102 | 103 | /* Prints this `Sphere` to the `std::ostream` specified by `os`. */ 104 | void print_to(std::ostream &os) const override { 105 | /* Desmos format is "sphere((x, y, z), radius)" */ 106 | os << "Sphere {center: " << center << ", radius: " << radius << ", material: " << *material 107 | << "} " << std::flush; 108 | } 109 | 110 | /* Constructs a Sphere with center `center_`, radius `radius_`, and material 111 | specified by `material_`. */ 112 | Sphere(const Point3D ¢er_, double radius_, std::shared_ptr material_) 113 | : center{center_}, radius{radius_}, material{std::move(material_)} 114 | { 115 | /* Compute the AABB for this `Sphere`. To do this, simply observe that the AABB's x-, y-, 116 | and z-intervals are [center.x/y/z - radius, center.x/y/z + radius]. In other words, the 117 | AABB of a sphere is just the axis-aligned cube with side length equal to 2 * radius 118 | centered at the sphere's center. Thus, the AABB is constructed from the two points 119 | (center.x +- radius, center.y +- radius, center.z +- radius). */ 120 | auto radius_vector = Vec3D{radius, radius, radius}; 121 | aabb = AABB::from_points({center - radius_vector, center + radius_vector}); 122 | } 123 | }; 124 | 125 | #endif -------------------------------------------------------------------------------- /include/util/image.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGE_H 2 | #define IMAGE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include /* For std::exit() */ 10 | #include "util/rgb.h" 11 | #include "util/progressbar.h" 12 | 13 | /* The `Image` type encapsulates a 2D image as a 2D array of `RGB` pixels. It is appropriate for 14 | images that need manipulations, because it stores and allows access to all the`RGB` pixels. If you 15 | only need an image to be sent as PPM to a file, use `ImagePPMStream`. */ 16 | class Image { 17 | /* Image width, image height, and the 2D array of `RGB` pixels with that width and height */ 18 | size_t w, h; 19 | std::vector> pixels; 20 | 21 | /* If given only a width and a height for the array of pixels, set all pixels to have 22 | color (0, 0, 0); that is, black. */ 23 | Image(size_t w_, size_t h_) : w{w_}, h{h_}, pixels(h, std::vector(w, RGB::zero())) {} 24 | 25 | /* Constructs an `Image` from the given rectangular pixel array `pixels_`. */ 26 | Image(const std::vector> &pixels_) : w{pixels_[0].size()}, h{pixels_.size()}, 27 | pixels{pixels_} {} 28 | 29 | public: 30 | auto width() const {return w;} 31 | auto height() const {return h;} 32 | auto& operator[] (size_t row) {return pixels[row];} 33 | const auto& operator[] (size_t row) const {return pixels[row];} 34 | 35 | auto aspect_ratio() const {return static_cast(w) / static_cast(h);} 36 | 37 | /* Prints this `Image` in PPM format to the file with name specified by `destination`. */ 38 | void send_as_ppm(const std::string &destination) const { 39 | if (std::ofstream fout(destination); !fout.is_open()) { 40 | std::cout << "Error: In Image::print_as_ppm(), could not open the file \"" 41 | << destination << "\"" << std::endl; 42 | std::exit(-1); 43 | } else { 44 | /* See https://en.wikipedia.org/wiki/Netpbm#PPM_example */ 45 | fout << "P3\n" << w << " " << h << "\n255\n"; 46 | ProgressBar pb(h, "Storing PPM image to " + destination); 47 | for (size_t row = 0; row < h; ++row) { 48 | for (size_t col = 0; col < w; ++col) { 49 | fout << pixels[row][col].as_string() << '\n'; 50 | } 51 | pb.complete_iteration(); 52 | } 53 | 54 | std::cout << "Image successfully saved to \"" << destination << "\"" << std::endl; 55 | } 56 | } 57 | 58 | auto& outline_border() { 59 | for (size_t row = 0; row < h; ++row) { 60 | pixels[row][0] = pixels[row][w - 1] = RGB::from_mag(1); 61 | } 62 | 63 | for (size_t col = 1; col < w - 1; ++col) { 64 | pixels[0][col] = pixels[h - 1][col] = RGB::from_mag(1); 65 | } 66 | 67 | return *this; 68 | } 69 | 70 | /* --- NAMED CONSTRUCTORS --- */ 71 | 72 | /* Creates an image with width `width` and height `height` */ 73 | static auto with_dimensions(size_t width, size_t height) { 74 | return Image(width, height); 75 | } 76 | 77 | /* Creates an image with width `w` and width-to-height ratio `aspect_ratio` */ 78 | static auto with_width_and_aspect_ratio(size_t width, double aspect_ratio) { 79 | auto height = static_cast(std::round(static_cast(width) / aspect_ratio)); 80 | 81 | /* Make sure height is at least 1 */ 82 | return with_dimensions(width, std::max(size_t{1}, height)); 83 | } 84 | 85 | /* Creates an image with height `h` and width-to-height ratio `aspect_ratio` */ 86 | static auto with_height_and_aspect_ratio(size_t height, double aspect_ratio) { 87 | auto width = static_cast(std::round(static_cast(height) * aspect_ratio)); 88 | 89 | /* Make sure width is at least 1 */ 90 | return with_dimensions(std::max(size_t{1}, width), height); 91 | } 92 | 93 | /* Creates an image from a two-dimensional array of `RGB` pixels. 94 | Requires `img` to be a rectangular array. */ 95 | static auto from_data(const std::vector> &img) { 96 | return Image(img); 97 | } 98 | 99 | /* Creates an image corresponding to the PPM file with name `file_name`. */ 100 | static auto from_ppm_file(const std::string &file_name) { 101 | 102 | /* Try to open the file `file_name` */ 103 | std::ifstream fin(file_name); 104 | if (!fin.is_open()) { 105 | std::cout << "Error: In Image::from_ppm_file(), could not find/open the file \"" 106 | << file_name << "\"" << std::endl; 107 | std::exit(-1); 108 | } 109 | 110 | /* Require that first line is "P3" */ 111 | std::string first_line; 112 | std::getline(fin, first_line); 113 | if (first_line != "P3") { 114 | std::cout << "Error: In Image::from_ppm_file(\"" << file_name << "\"), first line " 115 | "of file was not \"P3\", but instead was " << first_line << std::endl; 116 | std::exit(-1); 117 | } 118 | 119 | /* Input image width and image height */ 120 | size_t image_width, image_height; 121 | if (!(fin >> image_width >> image_height)) { 122 | std::cout << "Error: In Image::from_ppm_file(\"" << file_name << "\"), could not " 123 | "parse image width and height (two integers) on second line" << std::endl; 124 | std::exit(-1); 125 | } 126 | 127 | /* Input maximum RGB magnitude */ 128 | int max_magnitude; 129 | if (!(fin >> max_magnitude)) { 130 | std::cout << "Error: In Image::from_ppm_file(\"" << file_name << "\"), could not " 131 | "parse RGB max magnitude (one integer), which should occur on the third " 132 | "line, right after the image width and height on the second line" 133 | << std::endl; 134 | std::exit(-1); 135 | } 136 | 137 | /* Input RGB data for all pixels*/ 138 | std::vector ppm_data(image_height, std::vector(image_width, RGB::zero())); /* CTAD */ 139 | for (size_t row = 0; row < image_height; ++row) { 140 | for (size_t col = 0; col < image_width; ++col) { 141 | 142 | /* Read (r, g, b). If it fails, raise an error */ 143 | int r, g, b; 144 | if (!(fin >> r >> g >> b)) { 145 | std::cout << "Error: In Image::from_ppm_file(\"" << file_name << "\"), failed " 146 | "to parse color #" << (row * image_width) + col + 1 << 147 | " (three integers (r, g, b))" << std::endl; 148 | std::exit(-1); 149 | } 150 | 151 | /* Require that all of r, g, b are non-negative */ 152 | if (r < 0 || g < 0 || b < 0) { 153 | std::cout << "Error: In Image::from_ppm_file(\"" << file_name << "\"), found " 154 | "negative RGB channel value; color #" 155 | << (row * image_width) + col + 1 << " was (" << r << ", " << g 156 | << ", " << b << ")" << std::endl; 157 | std::exit(-1); 158 | } 159 | 160 | ppm_data[row][col] = RGB::from_rgb(r, g, b, max_magnitude); 161 | } 162 | } 163 | 164 | return from_data(ppm_data); 165 | } 166 | }; 167 | 168 | /* `ImagePPMStream` progressively takes in the `RGB` pixels of an image with a specified width and 169 | height, in the order of top to bottom then left to right, and prints those pixels to a specified 170 | file. Unlike `Image`, it does not allow access to the pixels of the image, because it does not store 171 | the 2D array of pixels representing the image, which saves storage. Thus, for images that need 172 | post-processing, the `Image` type is more appropriate. */ 173 | class ImagePPMStream { 174 | std::string file; 175 | std::ofstream fout; 176 | size_t w, h, curr_index; 177 | 178 | ImagePPMStream(const std::string &file_, size_t w_, size_t h_) 179 | : file{file_}, fout{file}, w{w_}, h{h_}, curr_index{0} 180 | { 181 | /* Print PPM header upon construction */ 182 | fout << "P3\n" << w << " " << h << "\n255\n"; 183 | } 184 | 185 | public: 186 | 187 | size_t width() const {return w;} 188 | size_t height() const {return h;} 189 | size_t size() const {return w * h;} 190 | auto aspect_ratio() const {return static_cast(w) / static_cast(h);} 191 | 192 | /* Redirect this `ImagePPMStream` to print to the file `file_name`. Gives an error 193 | if `file_name` cannot be opened, and gives a warning if the file switch takes 194 | place in the middle of image printing (meaning the first file will be left with 195 | an incomplete PPM image) */ 196 | void set_file(const std::string &file_name) { 197 | if (fout.open(file_name); !fout.is_open()) { /* Could not open `file_name` */ 198 | std::cout << "Error: In ImagePPMStream::set_file(), could not open the file \"" 199 | << file_name << "\"" << std::endl; 200 | std::exit(-1); 201 | } 202 | 203 | /* Warn user if they switch in the middle of printing an image */ 204 | if (curr_index > 0) { 205 | std::cout << "Warning: In ImagePPMStream::set_file(\"" << file_name << "\"), original" 206 | << " file \"" << file << "\" is left incomplete; " << curr_index 207 | << " out of " << w * h << " pixels printed" << std::endl; 208 | } 209 | 210 | file = file_name; 211 | curr_index = 0; /* Printing starts over for the new file */ 212 | } 213 | 214 | void add(const RGB &rgb) { 215 | if (curr_index == size()) { /* Error, already printed every pixel */ 216 | std::cout << "Error: Called ImagePPMStream::add() " << size() + 1 << " times for image" 217 | "of size " << size() << std::endl; 218 | std::exit(-1); 219 | } 220 | fout << rgb.as_string() << '\n'; 221 | ++curr_index; 222 | } 223 | 224 | /* --- NAMED CONSTRUCTORS --- */ 225 | 226 | /* Creates an ImagePPMStream with specified width and height, to be printed to the file 227 | `file_name` */ 228 | static auto with_dimensions(size_t width, size_t height, const std::string &file_name) { 229 | return ImagePPMStream(file_name, width, height); 230 | } 231 | 232 | /* Creates an ImagePPMStream with width `w` and width-to-height ratio `aspect_ratio`, to 233 | be printed to the file `file_name` */ 234 | static auto with_width_and_aspect_ratio(size_t width, double aspect_ratio, 235 | const std::string &file_name) { 236 | auto height = static_cast(std::round(static_cast(width) / aspect_ratio)); 237 | 238 | /* Make sure height is at least 1 */ 239 | return with_dimensions(width, std::max(size_t{1}, height), file_name); 240 | } 241 | 242 | /* Creates an ImagePPMStream with height `h` and width-to-height ratio `aspect_ratio`, 243 | to be printed to the file `file_name` */ 244 | static auto with_height_and_aspect_ratio(size_t height, double aspect_ratio, 245 | const std::string &file_name) { 246 | auto width = static_cast(std::round(static_cast(height) * aspect_ratio)); 247 | 248 | /* Make sure width is at least 1 */ 249 | return with_dimensions(std::max(size_t{1}, width), height, file_name); 250 | } 251 | 252 | ~ImagePPMStream() { 253 | if (curr_index == w * h) { /* All w * h pixels outputted, good */ 254 | std::cout << "Image successfully saved to \"" << file << "\"" << std::endl; 255 | } else { /* Warn user if some pixel(s) are missing (were not `add`ed) */ 256 | std::cout << "Warning: ImagePPMStream to \"" << file << "\" incomplete; " << curr_index 257 | << " out of " << "(" << w << " * " << h << ") = " << w * h 258 | << " RGB strings printed at time of destruction" << std::endl; 259 | } 260 | } 261 | }; 262 | 263 | #endif -------------------------------------------------------------------------------- /include/util/progressbar.h: -------------------------------------------------------------------------------- 1 | #ifndef PROGRESS_BAR_H 2 | #define PROGRESS_BAR_H 3 | 4 | #include 5 | #include "util/time_util.h" 6 | 7 | /* ProgressBar displays a live progress bar for loops where the total number of iterations is 8 | known beforehand. If DISABLE_PRINTING is true, then the progress bar will not print anything. 9 | ProgressBar is thread-safe. 10 | 11 | Usage: 12 | 13 | ```cpp 14 | auto progress_bar = ProgressBar("Rendering", 100); 15 | for (int i = 0; i < 100; ++i) { 16 | // Do stuff 17 | progress_bar.complete_iteration(); 18 | } 19 | ``` */ 20 | template 21 | class ProgressBar { 22 | using Clock = std::chrono::high_resolution_clock; 23 | using TimePoint = std::chrono::time_point; 24 | 25 | size_t total_iterations, iterations_done; 26 | unsigned percent_done; 27 | unsigned downscale_factor; /* The progress bar will have length (100 / `downscale_factor`) */ 28 | std::string description; /* Description of task */ 29 | TimePoint start_time; 30 | std::string progress_info; /* Printed after progress bar, contains time elapsed, % done, etc. */ 31 | std::mutex update_progress_bar_mtx; 32 | 33 | public: 34 | 35 | /* Increments the number of iterations done, and, if the next percent towards finishing has been 36 | achieved, also updates the progress bar and the estimated time left. Thread-safe. */ 37 | void complete_iteration() { 38 | if constexpr (DISABLE_PRINTING) { 39 | return; 40 | } 41 | 42 | /* Allow only one thread to execute `update()` at a time. */ 43 | std::lock_guard guard(update_progress_bar_mtx); 44 | 45 | ++iterations_done; 46 | 47 | /* Compute the current proportion and current percent of iterations completed */ 48 | auto curr_proportion_done = static_cast(iterations_done) 49 | / static_cast(total_iterations); 50 | auto curr_percent_done = static_cast(100 * curr_proportion_done); 51 | 52 | /* Check if the next percent towards finishing has been achieved */ 53 | if (curr_percent_done > percent_done) { 54 | 55 | /* Delete the progress information printed previously */ 56 | for (size_t i = 0; i < progress_info.size(); ++i) {std::cout << "\b \b";} 57 | 58 | /* Update the progress bar with the necessary number of `#`s. When we are x% done, we 59 | will have printed (x / downscale_factor) `#`s, so we will add (curr_percent_done / 60 | downscale_factor) - (percent_done / downscale_factor) `#`s. */ 61 | std::cout << std::string( 62 | curr_percent_done / downscale_factor - percent_done / downscale_factor, 63 | '#' 64 | ); 65 | 66 | /* Compute the estimated time left. To do this, we assume that the remaining iterations 67 | will be completed at the same average rate as the currently-finished iterations. Thus, 68 | the estimated time left is given by (time elapsed) * (1 - R) / R, where R is the current 69 | proportion of iterations completed. Note that we compute the seconds passed by dividing 70 | the milliseconds passed by 1000; this makes `seconds_left` more accurate. */ 71 | auto seconds_passed = static_cast(ms_diff(start_time, Clock::now())) / 1000; 72 | auto seconds_left = static_cast( 73 | seconds_passed * (1 - curr_proportion_done) / curr_proportion_done 74 | ); 75 | 76 | /* Update `progress_info` with new information. `progress_info` includes the percentage 77 | towards finishing, the current time elapsed, and the estimated time left. */ 78 | progress_info = " " + std::to_string(curr_percent_done) + "% done, " 79 | + seconds_to_dhms(static_cast(seconds_passed)) + " elapsed, " 80 | + seconds_to_dhms(seconds_left) + " left (est.)"; 81 | 82 | /* As long as we are not done, print `progress_info` after the progress bar. */ 83 | if (iterations_done != total_iterations) { 84 | std::cout << progress_info << std::flush; 85 | } 86 | 87 | /* Update `percent_done` */ 88 | percent_done = curr_percent_done; 89 | } 90 | 91 | /* If completed, print a completion message with the total time elapsed */ 92 | if (iterations_done == total_iterations) { 93 | std::cout << "\n" << description << ": Finished in " 94 | << seconds_to_dhms(seconds_diff(start_time, Clock::now())) 95 | << '\n' 96 | << std::endl; 97 | } 98 | } 99 | 100 | /* Constructs a `ProgressBar` for the task described by `task_description_`, which requires 101 | `total_iterations_` iterations in total. The progress bar will be (100 / `downscale_factor_`) 102 | characters long; `downscale_factor_` is 2 by default, resulting in a 50-character-long progress 103 | bar. */ 104 | ProgressBar(size_t total_iterations_, const std::string &task_description = "Progress", 105 | unsigned downscale_factor_ = 2 ) 106 | : total_iterations{total_iterations_}, 107 | iterations_done{0}, 108 | percent_done{0}, 109 | downscale_factor{downscale_factor_}, 110 | description{task_description}, 111 | start_time{Clock::now()} 112 | { 113 | if constexpr (!DISABLE_PRINTING) { 114 | std::cout << description << '\n'; 115 | std::cout << '|' << std::string(100 / downscale_factor, ' ') << "|\n"; 116 | std::cout << ' ' << std::flush; 117 | } 118 | } 119 | }; 120 | 121 | #endif -------------------------------------------------------------------------------- /include/util/rand_util.h: -------------------------------------------------------------------------------- 1 | #ifndef RAND_UTIL_H 2 | #define RAND_UTIL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | /* `SeedSeqGenerator` is a singleton class whose sole instance generates the sequence of random 10 | seeds which supplies random seeds to the `thread_local` RNGs used in the `rand_double` function. */ 11 | class SeedSeqGenerator { 12 | using seed_type = uint32_t; /* Determines the LCG's period (it equals 2^WIDTH); read below */ 13 | 14 | /* `custom_seed` = the unsigned integer seed for the sequence of seeds generated by this 15 | `SeedSeqGenerator`. If `custom_seed` is not explicitly set by the user, then a seed will 16 | later be automatically generated. */ 17 | std::optional custom_seed; 18 | /* `generate_next_seed_mtx` is used to ensure that only one thread at a time can advance 19 | the seed sequence generated by this `SeedSeqGenerator`. */ 20 | std::mutex generate_next_seed_mtx; 21 | 22 | /* Because `SeedSeqGenerator` is a singleton class, we make its default constructor private, 23 | but we do not delete it like we delete the copy constructor, copy assignment operator, move 24 | constructor, and move assignment operator. Making the default constructor private (and not 25 | declaring any other constructor) ensures this class may not be instantiated outside of this 26 | class, which is necessary for a singleton class. However, it is vital that we do not delete 27 | the default constructor, because this would mean that no instance of the class can be created 28 | at all, not even within the class! A singleton class needs one instance exactly, so it still 29 | needs to have SOME constructor defined for it. */ 30 | SeedSeqGenerator() = default; 31 | 32 | public: 33 | 34 | /* Returns the sole `SeedSeqGenerator` instance. */ 35 | static auto& get_instance() { 36 | /* I use the Meyer Singleton to implement this Singleton class. */ 37 | static SeedSeqGenerator seeds; 38 | return seeds; 39 | } 40 | 41 | /* Delete the copy constructor, copy assignment operator, move constructor, and 42 | move assignment operator. This ensures that no other instance of the `SeedSeqGenerator` 43 | class may be created (via copying the instance returned from `get_instance`), and also 44 | ensures that the instance returned by `get_instance()` will not be `std::move`d away. */ 45 | SeedSeqGenerator(const SeedSeqGenerator&) = delete; 46 | SeedSeqGenerator& operator= (const SeedSeqGenerator&) = delete; 47 | SeedSeqGenerator(SeedSeqGenerator&&) = delete; 48 | SeedSeqGenerator& operator= (SeedSeqGenerator&&) = delete; 49 | 50 | /* Generates and returns the next random seed. */ 51 | auto next_seed() { 52 | 53 | /* Ensure thread safety; allow only one thread to advance the state of this 54 | `SeedSeqGenerator` at a time. */ 55 | std::lock_guard guard(generate_next_seed_mtx); 56 | 57 | /* If no seed was provided for this `SeedSeqGenerator`, use `std::random_device` to generate 58 | one, and notify the user. */ 59 | if (!custom_seed) { 60 | custom_seed = std::random_device{}(); 61 | std::cout << "SeedSeqGenerator: No random seed provided, using " << *custom_seed 62 | << " (Use SeedSeqGenerator::get_instance().set_seed([custom seed]) " 63 | "to set a custom seed)" 64 | << std::endl; 65 | } 66 | 67 | /* The seed sequence is simply the output of a Linear Congruential Generator starting from 68 | `custom_seed`. See `rand_double` for a discussion about the importance of the specific 69 | constants chosen in LCGs. */ 70 | custom_seed = 2'483'477 * (*custom_seed) + 2'987'434'823; 71 | return *custom_seed; 72 | } 73 | 74 | /* Seeds the seed generator with `seed`. */ 75 | void set_seed(seed_type seed) { 76 | std::cout << "SeedSeqGenerator: Using user-provided random seed " << seed 77 | << '\n' << std::endl; 78 | custom_seed = seed; 79 | } 80 | }; 81 | 82 | /* Generates an uniformly-random `double` in the range [`min`, `max`] 83 | (by default [0, 1]). Now trades quality for speed; we no longer use `` 84 | in favor of a Linear Congruential Generator. */ 85 | auto rand_double(double min = 0, double max = 1) { 86 | /* I use a Linear Congruential Generator to generates random integers, which I will then 87 | use to generate the random `double`s in the range [min, max]. 88 | 89 | The LCG is defined by the recurrence relation X_{n + 1} = (A * X_n + C) % MOD, where X is the 90 | sequence of pseudorandom integers, X_0 is equal to the seed generated by the `SeedSeqGenerator` 91 | for this thread, A = 1664525, C = 1013904223, and MOD = 2^WIDTH, where the type `seed_type` 92 | of the generated seed is defined as `uintWIDTH_t` for some WIDTH. 93 | 94 | By the Hull-Dobell Theorem, this choice of A, C, and MOD guarantee that this LCG has period 95 | MOD (which is the maximum possible period length), no matter what X_0 we choose. Specifically, 96 | this is because the three conditions that (a) MOD and C are relatively prime, (b) A - 1 is 97 | divisible by all prime factors of M (which is just 2), and (c) 4 divides (A - 1) if 4 divides 98 | M, are all satisfied. See http://tinyurl.com/mwb8fwac. 99 | 100 | Note that the modulo 2^WIDTH operation is done implicitly, because the type of `seed` is 101 | `uintWIDTH_t` (so all arithmetic operations on `seed` are computed modulo 2^WIDTH; unsigned 102 | integers are awesome). This is a common trick, and is a big reason why many LCGs use a modulo 103 | which equals the word size (according to the Wikipedia page linked above). */ 104 | 105 | /* Generate a new seed for this thread's LCG */ 106 | thread_local auto seed = SeedSeqGenerator::get_instance().next_seed(); 107 | seed = 1'664'525 * seed + 1'013'904'223; /* The first random integer used is X_1, not X_0. */ 108 | /* The LCG generates uniformly random INTEGERS from 0 to (MOD - 1), inclusive, where 109 | MOD = (1 << 32) here. To generate uniformly random `double`s in the range [min, max], 110 | it suffices to normalize the generated integer to the range [0, 1] (by dividing it by 111 | (MOD - 1)), and then using that as the linear interpolation parameter between `min` 112 | and `max` (so we will be returning min + (max - min) * (seed / (MOD - 1)). */ 113 | constexpr auto SCALE = 1 / static_cast( 114 | std::numeric_limits::max() - 1 /* To handle different `seed_type`s */ 115 | ); 116 | return min + (max - min) * static_cast(seed) * SCALE; 117 | } 118 | 119 | /* Generates an uniformly-random `int` in the range [`min`, `max`] ([0, 1] by default). */ 120 | auto rand_int(int min = 0, int max = 1) { 121 | /* Is just use a `std::mt19937` for now. If performance becomes an issue I'll switch to 122 | a LCG like I did with `rand_double()`. */ 123 | thread_local std::mt19937 generator{SeedSeqGenerator::get_instance().next_seed()}; 124 | thread_local std::uniform_int_distribution<> dist; 125 | dist.param(std::uniform_int_distribution<>::param_type{min, max}); 126 | return dist(generator); 127 | } 128 | 129 | #endif -------------------------------------------------------------------------------- /include/util/rgb.h: -------------------------------------------------------------------------------- 1 | #ifndef RGB_H 2 | #define RGB_H 3 | 4 | #include 5 | #include 6 | #include "util/rand_util.h" 7 | #include "math/interval.h" 8 | 9 | /* Returns the gamma-encoded value of the magnitude `d`, under a gamma of `gamma` (2 by default). */ 10 | auto linear_to_gamma(double d, double gamma = 2) { 11 | /* See https://stackoverflow.com/a/16521337/12597781. */ 12 | return std::pow(d, 1 / gamma); 13 | } 14 | 15 | /* `RGB` encapsulates the notion of color as three real-valued numbers in the range [0, 1], 16 | representing the magnitudes of the red, green, and blue components, respectively. */ 17 | class RGB { 18 | RGB(double r_, double g_, double b_) : r{r_}, g{g_}, b{b_} {} 19 | 20 | public: 21 | 22 | /* Real-valued red, green, and blue components, each ranging from 0.0 to 1.0 23 | (if representing a valid color). */ 24 | double r, g, b; 25 | 26 | /* Returns the luminance of this `RGB` color. This is a very well-known formula in 27 | computer graphics (see https://tinyurl.com/3rku9fy4). */ 28 | auto luminance() const { 29 | return 0.2126 * r + 0.7152 * g + 0.0722 * b; 30 | } 31 | 32 | /* --- NAMED CONSTRUCTORS --- */ 33 | 34 | /* Creates a RGB color, given red, green, and blue components (each in the range 0.0 to 1.0) */ 35 | static RGB from_mag(double red, double green, double blue) { 36 | return RGB(red, green, blue); 37 | } 38 | 39 | /* Creates a RGB color with red, green, and blue components all set to `val` (where 40 | 0.0 <= `val` <= 1.0) */ 41 | static RGB from_mag(double val) { 42 | return from_mag(val, val, val); 43 | } 44 | 45 | /* Creates a RGB color with red, green, and blue components, where each ranges from 46 | `0` to `max_magnitude` */ 47 | static RGB from_rgb(double red, double green, double blue, double max_magnitude = 255) { 48 | return RGB(red / max_magnitude, green / max_magnitude, blue / max_magnitude); 49 | } 50 | 51 | /* Creates a RGB color where the red, green, and blue components range from `0` to 52 | `max_magnitude`, with all three components equal to `val` */ 53 | static RGB from_rgb(double val, double max_magnitude = 255) { 54 | return from_rgb(val, val, val, max_magnitude); 55 | } 56 | 57 | /* Creates a RGB color with red, green, and blue components set to 0 */ 58 | static RGB zero() { 59 | return from_mag(0); 60 | } 61 | 62 | /* Creates a RGB with random red, green, and blue components, each a real number 63 | in the range [`min`, `max`] (by default [0, 1]). */ 64 | static RGB random(double min = 0, double max = 1) { 65 | return from_mag(rand_double(min, max), rand_double(min, max), rand_double(min, max)); 66 | } 67 | 68 | /* Mathematical operators (since anti-aliasing requires finding the average of 69 | multiple colors, so we need += and /=) */ 70 | 71 | /* Element-wise addition assignment operator for `RGB`s */ 72 | auto& operator+= (const RGB &rhs) {r += rhs.r; g += rhs.g; b += rhs.b; return *this;} 73 | /* Element-wise multiplication assignment operator for `RGB`s */ 74 | auto& operator*= (double d) {r *= d; g *= d; b *= d; return *this;} 75 | /* Element-wise division assignment operator for `RGB`s */ 76 | auto& operator/= (double d) {return *this *= (1 / d);} /* Multiply by 1/d for less divisions */ 77 | 78 | /* 79 | @brief Returns this `RGB` object gamma-encoded, and as a string. 80 | @param `delimiter`: What is printed between the red, green, and blue components. A space by 81 | default. 82 | @param `surrounding`: What is printed at the beginning and the end; if empty, then nothing is 83 | printed. Otherwise, the first and second characters are printed directly before and after 84 | the numbers, respectively. 85 | @param `max_magnitude`: Represents the "full" magnitudes of red, green, and blue. 255 by 86 | default. 87 | @param `gamma`: The encoding gamma for gamma correction. 2 by default. If the raw values of 88 | the RGB intensities are desired, set `gamma` to 1. 89 | */ 90 | auto as_string(std::string delimiter = " ", std::string surrounding = "", 91 | double max_magnitude = 255, double gamma = 2, 92 | bool use_tone_mapping = true) const 93 | { 94 | auto r2 = r, g2 = g, b2 = b; 95 | 96 | /* Use the Reinhard operator to perform tone mapping on the RGB components of this 97 | color. The Reinhard operator works by taking the linear (not gamma-corrected) RGB 98 | components, and dividing each by (1 + L), where L is the luminance of this color. */ 99 | if (use_tone_mapping) { 100 | auto L = luminance(); 101 | r2 /= 1 + L; 102 | g2 /= 1 + L; 103 | b2 /= 1 + L; 104 | } 105 | 106 | /* Add 0.999999 to `max_magnitude` to allow truncating to `max_magnitude` itself */ 107 | auto scale = max_magnitude + 0.999999; 108 | return (surrounding.empty() ? "" : std::string{surrounding[0]}) 109 | + std::to_string(static_cast(scale * linear_to_gamma(r2, gamma))) + delimiter 110 | + std::to_string(static_cast(scale * linear_to_gamma(g2, gamma))) + delimiter 111 | + std::to_string(static_cast(scale * linear_to_gamma(b2, gamma))) 112 | + (surrounding.empty() ? "" : std::string{surrounding[1]}); 113 | } 114 | }; 115 | 116 | /* Mathematical utility functions */ 117 | 118 | /* Element-wise addition of two `RGB`s. */ 119 | auto operator+ (const RGB &a, const RGB &b) {return RGB::from_mag(a.r + b.r, a.g + b.g, a.b + b.b);} 120 | 121 | /* Element-wise multiplication by a double `d` */ 122 | auto operator* (const RGB &a, double d) {auto ret = a; ret *= d; return ret;} 123 | 124 | /* Element-wise multiplication by a double `d` */ 125 | auto operator* (double d, const RGB &a) {return a * d;} 126 | 127 | /* Element-wise multiplication of two `RGB` objects */ 128 | auto operator* (const RGB &a, const RGB &b) {return RGB::from_mag(a.r * b.r, a.g * b.g, a.b * b.b);} 129 | 130 | /* Returns a color linearly interpolated, with a proportion of `1 - d` of `a` and 131 | a proportion of `d` of `b`. `d` must be in the range [0, 1]; if not, then an error 132 | is raised. */ 133 | auto lerp(const RGB &a, const RGB &b, double d) { 134 | 135 | /* Raise an error if `d` is not in the range [0, 1]. */ 136 | if (!Interval(0, 1).contains_inclusive(d)) { 137 | std::cout << "Error: In `lerp(" << a.as_string(", ", "()", 255, 1) << ", " 138 | << b.as_string(", ", "()", 255, 1) << ", " << d << "), lerp proportion " 139 | << d << " is not in the range [0, 1]." << std::endl; 140 | std::exit(-1); 141 | } 142 | 143 | return RGB::from_mag( 144 | (1 - d) * a.r + d * b.r, 145 | (1 - d) * a.g + d * b.g, 146 | (1 - d) * a.b + d * b.b 147 | ); 148 | } 149 | 150 | #endif -------------------------------------------------------------------------------- /include/util/time_util.h: -------------------------------------------------------------------------------- 1 | #ifndef TIME_UTIL_H 2 | #define TIME_UTIL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | template 10 | auto seconds_diff(const std::chrono::time_point &start, const std::chrono::time_point &end) { 11 | return std::chrono::duration_cast(end - start).count(); 12 | } 13 | 14 | template 15 | auto ms_diff(const std::chrono::time_point &start, const std::chrono::time_point &end) { 16 | return std::chrono::duration_cast(end - start).count(); 17 | } 18 | 19 | /* Converts `seconds` seconds into DHMS (days, hours, minutes, seconds), in the readable 20 | format "_d _hr _min _s". If a quantity is 0, then its corresponding time unit is omitted 21 | (so 1 hour and 5 seconds would just be "1hr 5s", not "1hr 0min 5s"), for instance. */ 22 | auto seconds_to_dhms(unsigned long long seconds) { 23 | const static std::array, 4> conv{{ 24 | {86400, "d"}, {3600, "hr"}, {60, "min"}, {1, "s"} 25 | }}; 26 | 27 | std::string ret; 28 | for (const auto &[factor, time_unit] : conv) { 29 | if (seconds >= factor) { 30 | auto num = seconds / factor; 31 | ret += std::to_string(num) + time_unit + " "; 32 | seconds %= factor; 33 | } 34 | } 35 | 36 | /* If seconds is 0 then ret is blank. We make ret = "0s" in that case. */ 37 | if (ret.empty()) { 38 | ret = "0s"; 39 | } else if (ret.back() == ' ') { 40 | ret.pop_back(); 41 | } 42 | 43 | return ret; 44 | } 45 | 46 | #endif -------------------------------------------------------------------------------- /profiling_and_analysis/v1_profile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/profiling_and_analysis/v1_profile.pdf -------------------------------------------------------------------------------- /profiling_and_analysis/v1_rtow_final_image_runtime_analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/profiling_and_analysis/v1_rtow_final_image_runtime_analysis.png -------------------------------------------------------------------------------- /profiling_and_analysis/v1_with_old_bvh.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/profiling_and_analysis/v1_with_old_bvh.pdf -------------------------------------------------------------------------------- /rendered_images/christmas_tree_of_spheres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/christmas_tree_of_spheres.png -------------------------------------------------------------------------------- /rendered_images/cornell_box_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/cornell_box_1.png -------------------------------------------------------------------------------- /rendered_images/empty_cornell_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/empty_cornell_box.png -------------------------------------------------------------------------------- /rendered_images/millions_of_spheres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/millions_of_spheres.png -------------------------------------------------------------------------------- /rendered_images/millions_of_spheres_with_lights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/millions_of_spheres_with_lights.png -------------------------------------------------------------------------------- /rendered_images/raining_on_the_dance_floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/raining_on_the_dance_floor.png -------------------------------------------------------------------------------- /rendered_images/raining_on_the_dance_floor_16_9_aspect_ratio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/raining_on_the_dance_floor_16_9_aspect_ratio.png -------------------------------------------------------------------------------- /rendered_images/rtow_final_lights_with_tone_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/rtow_final_lights_with_tone_mapping.png -------------------------------------------------------------------------------- /rendered_images/rtow_final_lights_without_tone_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/rtow_final_lights_without_tone_mapping.png -------------------------------------------------------------------------------- /rendered_images/rtweekend_final_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaPavonis/cpp_raytracer/78e170001bea720f27c855d0e1117497d6f3dfc4/rendered_images/rtweekend_final_image.png -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "util/rand_util.h" 2 | #include "base/scene.h" 3 | #include "base/material.h" 4 | #include "base/camera.h" 5 | #include "shapes/shapes.h" 6 | 7 | /* Instead of `std::make_shared`, I just need to type `ms` now. */ 8 | template 9 | auto ms(Args&&... args) -> std::shared_ptr { 10 | return std::make_shared(std::forward(args)...); 11 | } 12 | 13 | void rtow_final_image() { 14 | Scene world; 15 | 16 | /* The same code as from the tutorial for their final scene */ 17 | 18 | /* Big gray sphere for the ground */ 19 | auto ground_material = std::make_shared(RGB::from_mag(0.5, 0.5, 0.5)); 20 | world.add(std::make_shared(Point3D(0,-1000,0), 1000, ground_material)); 21 | 22 | /* Generate small spheres */ 23 | for (int a = -11; a < 11; a++) { 24 | for (int b = -11; b < 11; b++) { 25 | auto choose_mat = rand_double(); 26 | Point3D center(a + 0.9*rand_double(), 0.2, b + 0.9*rand_double()); 27 | 28 | if ((center - Point3D(4, 0.2, 0)).mag() > 0.9) { 29 | std::shared_ptr sphere_material; 30 | 31 | if (choose_mat < 0.8) { 32 | // diffuse 33 | auto albedo = RGB::random() * RGB::random(); 34 | sphere_material = std::make_shared(albedo); 35 | world.add(std::make_shared(center, 0.2, sphere_material)); 36 | } else if (choose_mat < 0.95) { 37 | // Metal 38 | auto albedo = RGB::random(0.5, 1); 39 | auto fuzz = rand_double(0, 0.5); 40 | sphere_material = std::make_shared(albedo, fuzz); 41 | world.add(std::make_shared(center, 0.2, sphere_material)); 42 | } else { 43 | // glass 44 | sphere_material = std::make_shared(1.5); 45 | world.add(std::make_shared(center, 0.2, sphere_material)); 46 | } 47 | } 48 | } 49 | } 50 | 51 | /* Three big spheres */ 52 | auto material1 = std::make_shared(1.5); 53 | world.add(std::make_shared(Point3D(0, 1, 0), 1.0, material1)); 54 | 55 | auto material2 = std::make_shared(RGB::from_mag(0.4, 0.2, 0.1)); 56 | world.add(std::make_shared(Point3D(-4, 1, 0), 1.0, material2)); 57 | 58 | auto material3 = std::make_shared(RGB::from_mag(0.7, 0.6, 0.5), 0.0); 59 | world.add(std::make_shared(Point3D(4, 1, 0), 1.0, material3)); 60 | 61 | /* Render image */ 62 | Camera() 63 | .set_image_by_width_and_aspect_ratio(1200, 16. / 9.) 64 | .set_vertical_fov(20) /* Smaller vertical FOV zooms in, also avoids shape stretching */ 65 | .set_camera_center(Point3D{13, 2, 3}) 66 | .set_camera_lookat(Point3D{0, 0, 0}) 67 | .set_camera_up_direction(Vec3D{0, 1, 0}) 68 | .set_defocus_angle(0.6) 69 | .set_focus_distance(10) 70 | .set_samples_per_pixel(500) /* For a high-quality image */ 71 | .set_max_depth(20) /* More light bounces for higher quality */ 72 | .set_background(RGB::from_mag(0.7, 0.8, 1)) 73 | .render(world) 74 | .send_as_ppm("rtweekend_final_image.ppm"); 75 | } 76 | 77 | void rtow_final_lights_with_tone_mapping() { 78 | /* Now with custom fixed seeds. */ 79 | SeedSeqGenerator::get_instance().set_seed(2286021279); 80 | 81 | Scene world; 82 | 83 | /* The same code as from the tutorial for their final scene */ 84 | 85 | /* Big gray sphere for the ground */ 86 | auto ground_material = std::make_shared(RGB::from_mag(0.5, 0.5, 0.5)); 87 | world.add(std::make_shared(Point3D(0,-1000000,0), 1000000, ground_material)); 88 | 89 | /* Generate small spheres */ 90 | for (int a = -11; a < 11; a++) { 91 | for (int b = -11; b < 11; b++) { 92 | auto choose_mat = rand_double(); 93 | Point3D center(a + 0.9*rand_double(), 0.2, b + 0.9*rand_double()); 94 | 95 | if ((center - Point3D(4, 0.2, 0)).mag() > 0.9) { 96 | std::shared_ptr sphere_material; 97 | 98 | if (choose_mat < 0.035) { 99 | auto albedo = RGB::random(); 100 | sphere_material = std::make_shared(albedo, rand_double(30, 100)); 101 | world.add(std::make_shared(center, 0.2, sphere_material)); 102 | } else if (choose_mat < 0.8) { 103 | // diffuse 104 | auto albedo = RGB::random() * RGB::random(); 105 | sphere_material = std::make_shared(albedo); 106 | world.add(std::make_shared(center, 0.2, sphere_material)); 107 | } else if (choose_mat < 0.9) { 108 | // Metal 109 | auto albedo = RGB::random(0.5, 1); 110 | auto fuzz = rand_double(0, 0.5); 111 | sphere_material = std::make_shared(albedo, fuzz); 112 | world.add(std::make_shared(center, 0.2, sphere_material)); 113 | } else { 114 | // glass 115 | sphere_material = std::make_shared(1.5); 116 | world.add(std::make_shared(center, 0.2, sphere_material)); 117 | } 118 | } 119 | } 120 | } 121 | 122 | /* Three big spheres */ 123 | auto material1 = std::make_shared(1.5); 124 | world.add(std::make_shared(Point3D(0, 1, 0), 1.0, material1)); 125 | 126 | auto material2 = std::make_shared(RGB::from_mag(0.4, 0.2, 0.1)); 127 | world.add(std::make_shared(Point3D(-4, 1, 0), 1.0, material2)); 128 | 129 | auto material3 = std::make_shared(RGB::from_mag(0.7, 0.6, 0.5), 0.0); 130 | world.add(std::make_shared(Point3D(4, 1, 0), 1.0, material3)); 131 | 132 | /* Light in the sky (like a moon) */ 133 | auto light_material = std::make_shared( 134 | RGB::from_mag(0.380205, 0.680817, 0.385431), 135 | 150 136 | ); 137 | world.add(std::make_shared(Point3D(0, 2.5, 2.5), 0.2, light_material)); 138 | 139 | Camera() 140 | .set_image_by_width_and_aspect_ratio(1080, 16. / 9.) 141 | .set_vertical_fov(25) /* Smaller vertical FOV zooms in, also avoids shape stretching */ 142 | .set_camera_center(Point3D{13, 2, 3}) 143 | .set_camera_lookat(Point3D{0, 0, 0}) 144 | .set_camera_up_direction(Vec3D{0, 1, 0}) 145 | .set_defocus_angle(0.48) 146 | .set_focus_distance(10) 147 | .set_samples_per_pixel(2000) /* For a high-quality image */ 148 | .set_max_depth(20) /* More light bounces for higher quality */ 149 | .set_background(RGB::zero()) 150 | .render(world) 151 | .send_as_ppm("rtow_final_lights_with_tone_mapping.ppm"); 152 | } 153 | 154 | void millions_of_spheres() { 155 | Scene world; 156 | 157 | /* The same code as from the tutorial for their final scene, except now with a lot more spheres 158 | */ 159 | 160 | /* Big gray sphere for the ground */ 161 | auto ground_material = std::make_shared(RGB::from_mag(0.5, 0.5, 0.5)); 162 | world.add(std::make_shared(Point3D(0,-1000000,0), 1000000, ground_material)); 163 | 164 | /* Generate small spheres */ 165 | for (int a = -1001; a < 1001; a++) { 166 | for (int b = -1001; b < 51; b++) { 167 | auto choose_mat = rand_double(); 168 | Point3D center(a + 0.9*rand_double(), 0.2, b + 0.9*rand_double()); 169 | 170 | if ((center - Point3D(4, 0.2, 0)).mag() > 0.9) { 171 | std::shared_ptr sphere_material; 172 | 173 | if (choose_mat < 0.8) { 174 | // diffuse 175 | auto albedo = RGB::random() * RGB::random(); 176 | sphere_material = std::make_shared(albedo); 177 | world.add(std::make_shared(center, 0.2, sphere_material)); 178 | } else if (choose_mat < 0.95) { 179 | // Metal 180 | auto albedo = RGB::random(0.5, 1); 181 | auto fuzz = rand_double(0, 0.5); 182 | sphere_material = std::make_shared(albedo, fuzz); 183 | world.add(std::make_shared(center, 0.2, sphere_material)); 184 | } else { 185 | // glass 186 | sphere_material = std::make_shared(1.5); 187 | world.add(std::make_shared(center, 0.2, sphere_material)); 188 | } 189 | } 190 | } 191 | } 192 | 193 | /* Three big spheres */ 194 | auto material1 = std::make_shared(1.5); 195 | world.add(std::make_shared(Point3D(0, 1, 0), 1.0, material1)); 196 | 197 | auto material2 = std::make_shared(RGB::from_mag(0.4, 0.2, 0.1)); 198 | world.add(std::make_shared(Point3D(-4, 1, 0), 1.0, material2)); 199 | 200 | auto material3 = std::make_shared(RGB::from_mag(0.7, 0.6, 0.5), 0.0); 201 | world.add(std::make_shared(Point3D(4, 1, 0), 1.0, material3)); 202 | 203 | /* Render image (about 3hr 15min on Dell XPS 8960, 16 cores, 24 threads) */ 204 | Camera() 205 | .set_image_by_width_and_aspect_ratio(2160, 16. / 9.) 206 | .set_vertical_fov(40) /* Smaller vertical FOV zooms in, also avoids shape stretching */ 207 | .set_camera_center(Point3D{0, 10, 50}) 208 | .set_camera_lookat(Point3D{0, 0, 0}) 209 | .set_camera_up_direction(Vec3D{0, 1, 0}) 210 | .set_defocus_angle(0.1) 211 | .set_focus_distance(51) 212 | .set_samples_per_pixel(500) /* For a high-quality image */ 213 | .set_max_depth(50) /* More light bounces for higher quality */ 214 | .render(world) 215 | .send_as_ppm("millions_of_spheres.ppm"); 216 | } 217 | 218 | void millions_of_spheres_with_lights() { 219 | SeedSeqGenerator::get_instance().set_seed(473654968); 220 | 221 | Scene world; 222 | 223 | /* Big gray sphere for the ground */ 224 | auto ground_material = std::make_shared(RGB::from_mag(0.5, 0.5, 0.5)); 225 | world.add(std::make_shared(Point3D(0,-1000000,0), 1000000, ground_material)); 226 | 227 | /* Generate small spheres */ 228 | for (int a = -1001; a < 1001; a++) { 229 | for (int b = -1501; b < 51; b++) { 230 | auto choose_mat = rand_double(); 231 | Point3D center(a + 0.9*rand_double(), 0.2, b + 0.9*rand_double()); 232 | 233 | if ((center - Point3D(4, 0.2, 0)).mag() > 0.9) { 234 | std::shared_ptr sphere_material; 235 | 236 | if (choose_mat < 0.035) { 237 | auto albedo = RGB::random(); 238 | sphere_material = std::make_shared(albedo, rand_double(5, 15)); 239 | world.add(std::make_shared(center, 0.2, sphere_material)); 240 | } else if (choose_mat < 0.8) { 241 | // diffuse 242 | auto albedo = RGB::random() * RGB::random(); 243 | sphere_material = std::make_shared(albedo); 244 | world.add(std::make_shared(center, 0.2, sphere_material)); 245 | } else if (choose_mat < 0.9) { 246 | // Metal 247 | auto albedo = RGB::random(0.5, 1); 248 | auto fuzz = rand_double(0, 0.5); 249 | sphere_material = std::make_shared(albedo, fuzz); 250 | world.add(std::make_shared(center, 0.2, sphere_material)); 251 | } else { 252 | // glass 253 | sphere_material = std::make_shared(1.5); 254 | world.add(std::make_shared(center, 0.2, sphere_material)); 255 | } 256 | } 257 | } 258 | } 259 | 260 | /* Three big spheres */ 261 | auto material1 = std::make_shared(1.5); 262 | world.add(std::make_shared(Point3D(0, 1, 0), 1.0, material1)); 263 | 264 | auto material2 = std::make_shared(RGB::from_mag(0.4, 0.2, 0.1)); 265 | world.add(std::make_shared(Point3D(-4, 1, 0), 1.0, material2)); 266 | 267 | auto material3 = std::make_shared(RGB::from_mag(0.7, 0.6, 0.5), 0.0); 268 | world.add(std::make_shared(Point3D(4, 1, 0), 1.0, material3)); 269 | 270 | /* Big light directly up from the origin */ 271 | auto light_material = std::make_shared( 272 | RGB::from_mag(0.380205, 0.680817, 0.385431), 273 | 150 274 | ); 275 | world.add(std::make_shared(Point3D(0, 12, 0), 3, light_material)); 276 | 277 | Camera() 278 | .set_image_by_width_and_aspect_ratio(1080, 16. / 9.) 279 | .set_vertical_fov(40) /* Smaller FOV means more zoomed in (also avoids stretching) */ 280 | .set_camera_center(Point3D{0, 12.5, 50}) 281 | .set_camera_lookat(Point3D{0, 0, 0}) 282 | .set_camera_up_direction(Vec3D{0, 1, 0}) 283 | .set_defocus_angle(0.1) 284 | .set_focus_distance(51) 285 | .set_samples_per_pixel(1000) /* For a high-quality image */ 286 | .set_max_depth(20) /* More light bounces for higher quality */ 287 | .set_background(RGB::zero()) 288 | .render(world) 289 | .send_as_ppm("millions_of_spheres_with_lights.ppm"); 290 | } 291 | 292 | /* First Parallelogram test (corresponds to the image rendered at the end of Section 6 293 | of The Next Week). */ 294 | void parallelogram_test() { 295 | Scene world; 296 | 297 | auto left_red = std::make_shared(RGB::from_mag(1.0, 0.2, 0.2)); 298 | auto back_green = std::make_shared(RGB::from_mag(0.2, 1.0, 0.2)); 299 | auto right_blue = std::make_shared(RGB::from_mag(0.2, 0.2, 1.0)); 300 | auto upper_orange = std::make_shared(RGB::from_mag(1.0, 0.5, 0.0)); 301 | auto lower_teal = std::make_shared(RGB::from_mag(0.2, 0.8, 0.8)); 302 | 303 | // Quads 304 | world.add(ms(Point3D(-3,-2, 5), Vec3D(0, 0,-4), Vec3D(0, 4, 0), left_red)); 305 | world.add(ms(Point3D(-2,-2, 0), Vec3D(4, 0, 0), Vec3D(0, 4, 0), back_green)); 306 | world.add(ms(Point3D( 3,-2, 1), Vec3D(0, 0, 4), Vec3D(0, 4, 0), right_blue)); 307 | world.add(ms(Point3D(-2, 3, 1), Vec3D(4, 0, 0), Vec3D(0, 0, 4), upper_orange)); 308 | world.add(ms(Point3D(-2,-3, 5), Vec3D(4, 0, 0), Vec3D(0, 0,-4), lower_teal)); 309 | 310 | Camera() 311 | .set_image_by_width_and_aspect_ratio(1000, 1.) 312 | .set_samples_per_pixel(100) 313 | .set_max_depth(50) 314 | .set_vertical_fov(80) 315 | .set_camera_center(Point3D{0, 0, 9}) 316 | .set_camera_direction_towards(Point3D{0, 0, 0}) 317 | .set_camera_up_direction(Point3D{0, 1, 0}) 318 | .turn_blur_off() 319 | .set_background(RGB::from_mag(0.7, 0.8, 1)) /* Light-blueish background */ 320 | .render(world) 321 | .send_as_ppm("parallelograms_test.ppm"); 322 | } 323 | 324 | /* Renders a Cornell Box. If `empty` is true, then no boxes will be present inside 325 | the Cornell Box. */ 326 | void cornell_box_test(bool empty = false) { 327 | Scene world; 328 | 329 | auto red = ms(RGB::from_mag(.65, .05, .05)); 330 | auto white = ms(RGB::from_mag(.73, .73, .73)); 331 | auto green = ms(RGB::from_mag(.12, .45, .15)); 332 | auto light = ms(RGB::from_mag(1, 1, 1), 15); 333 | 334 | /* Walls and light of the standard Cornell Box */ 335 | world.add(ms(Point3D(555,0,0), Vec3D(0,555,0), Vec3D(0,0,555), green)); 336 | world.add(ms(Point3D(0,0,0), Vec3D(0,555,0), Vec3D(0,0,555), red)); 337 | world.add(ms(Point3D(343,554,332), Vec3D(-130,0,0), Vec3D(0,0,-105), light)); 338 | world.add(ms(Point3D(0,0,0), Vec3D(555,0,0), Vec3D(0,0,555), white)); 339 | world.add(ms(Point3D(555,555,555), Vec3D(-555,0,0), Vec3D(0,0,-555), white)); 340 | world.add(ms(Point3D(0,0,555), Vec3D(555,0,0), Vec3D(0,555,0), white)); 341 | 342 | /* If `empty` is false, add the two `Box`es to the standard Cornell Box. The boxes are 343 | unrotated for now.*/ 344 | if (!empty) { 345 | world.add(ms(Point3D(130, 0, 65), Point3D(295, 165, 230), white)); 346 | world.add(ms(Point3D(265, 0, 295), Point3D(430, 330, 460), white)); 347 | } 348 | Camera() 349 | .set_image_by_width_and_aspect_ratio(1000, 1.) 350 | .set_samples_per_pixel(10) 351 | .set_max_depth(1000) 352 | .set_vertical_fov(40) 353 | .set_camera_center(Point3D{278, 278, -800}) 354 | .set_camera_direction_towards(Point3D{278, 278, 0}) 355 | .set_camera_up_direction(Point3D{0, 1, 0}) 356 | .turn_blur_off() 357 | .set_background(RGB::from_mag(0)) /* Black background */ 358 | .render(world) 359 | .send_as_ppm((empty ? "empty_cornell_box.ppm" : "cornell_box_1.ppm")); 360 | } 361 | 362 | /* Renders an image of a scene consisting of a bunch of colored parallelogram lights stretching 363 | away into the distance, above which are suspended numerous glass (and a few metal) "raindrops" 364 | (spheres). */ 365 | void raining_on_the_dance_floor() { 366 | SeedSeqGenerator::get_instance().set_seed(5987634); 367 | 368 | Scene world; 369 | 370 | /* Add the dance floor */ 371 | for (int x = -1000; x <= 1000; ++x) { 372 | for (int z = -1000; z <= 100; ++z) { 373 | world.add(ms( 374 | Point3D{x + 0.1, 0, z + 0.1}, Point3D{0.8, 0, 0}, Point3D{0, 0, 0.8}, 375 | ms(RGB::random(), rand_double(0.5, 2)) 376 | )); 377 | } 378 | } 379 | 380 | /* Add raindrops (and the occasional metal ball for some reason) */ 381 | for (size_t i = 0; i < 25000; ++i) { 382 | auto choose_material = rand_double(); 383 | std::shared_ptr material = ms(rand_double(1.25, 2.5)); 384 | if (choose_material < 0.05) {material = ms(RGB::random(), 0);} 385 | world.add(ms(Point3D{ 386 | rand_double(-1000, 1000), rand_double(2, 40), rand_double(-1000, 50)}, 387 | rand_double(0.25, 0.8), 388 | material)); 389 | } 390 | 391 | /* Add some raindrops closer to the camera center */ 392 | for (size_t i = 0; i < 50; ++i) { 393 | world.add(ms( 394 | Point3D{rand_double(-20, 20), rand_double(1, 8), rand_double(-50, 50)}, 395 | rand_double(0.25, 0.5), 396 | ms(1.5) 397 | )); 398 | } 399 | 400 | Camera() 401 | .set_image_by_width_and_aspect_ratio(2160, 16. / 9.) 402 | .set_samples_per_pixel(50) 403 | .set_max_depth(50) 404 | .set_vertical_fov(40) 405 | .set_camera_center(Point3D{0, 10, 50}) 406 | .set_camera_direction_towards(Point3D{0, 0, 0}) 407 | .set_camera_up_direction(Point3D{0, 1, 0}) 408 | .turn_blur_off() 409 | .set_background(RGB::from_mag(0)) /* Black background */ 410 | .render(world) 411 | .send_as_ppm("raining_on_the_dance_floor.ppm"); 412 | } 413 | 414 | void christmas_tree_made_of_spheres() { 415 | SeedSeqGenerator::get_instance().set_seed(20231225); /* Nice seed */ 416 | 417 | Scene world; 418 | 419 | /* Add flat ground; color close to white to symbolize snow */ 420 | auto ground = ms( 421 | Point3D{-1000000, 0, -1000000}, Vec3D{2000000, 0, 0}, Vec3D{0, 0, 2000000}, 422 | ms(RGB::from_mag(0.25)) 423 | ); 424 | world.add(ground); 425 | 426 | /* Moon toward the top right, above the ground */ 427 | auto moon = ms(Point3D{20, 25, -25}, 2.5, ms(RGB::from_mag(0.8), 500)); 428 | world.add(moon); 429 | 430 | /* The Christmas tree will be a right cone with base on the xz-plane centered at the origin, 431 | and apex at the point (0, cone_apex_y, 0). In other words, `cone_apex_y` is the height of 432 | the Christmas tree. The radius of the cone's circular base will be equal to its height times 433 | `cone_radius_to_height_ratio`. */ 434 | auto cone_apex_y = 20; 435 | auto cone_radius_to_height_ratio = 1. / 3.; 436 | 437 | /* The Christmas tree will be made out of metal ornaments, which will either be red, green, 438 | blue, or gray. */ 439 | const std::array colors{ 440 | RGB::from_rgb(156, 10, 72), 441 | RGB::from_rgb(66, 106, 33), 442 | RGB::from_rgb(41, 119, 133), 443 | /* Gray appears at 3x the probability of red, green, or blue */ 444 | RGB::from_mag(0.5), 445 | RGB::from_mag(0.5), 446 | RGB::from_mag(0.5), 447 | }; 448 | 449 | /* Generate metal ornaments (spheres) on the lateral surface of the cone that is the 450 | Christmas tree. Here, we generate 200 of them. */ 451 | for (int i = 0; i < 200; ++i) { 452 | 453 | /* Add a random ornament (metal sphere) centered on the cone's lateral surface. */ 454 | while (true) { 455 | /* Choose a y-coordinate (which is in [0, `cone_apex_y`]). */ 456 | auto random_y = rand_double(0, cone_apex_y); 457 | /* Reduce excessive clustering of spheres at the top of the Christmas tree, 458 | which happens because the y-coordinates are uniformly random, and the available 459 | surface area for ornaments at the top of the Christmas tree (with larger 460 | y-coordinates) is smaller. */ 461 | if (random_y > 17) {random_y = rand_double(0, cone_apex_y);} 462 | 463 | /* The first ornament will be at the apex of the Christmas tree cone */ 464 | if (i == 0) { 465 | random_y = cone_apex_y; 466 | } 467 | 468 | /* Now, choose any point at the y-coordinate `random_y` on the cone. This 469 | will be the center of the current ornament. To choose a random point with 470 | this y-coordinate, we observe that the set of points on the cone's surface 471 | at a given y-coordinate form a circle, so it suffices to choose a random 472 | angle and then use sin/cos in conjunction with the radius of the cone at 473 | this height (which can be found using `cone_radius_to_height_ratio`). */ 474 | auto radius_at_this_y = (20 - random_y) * cone_radius_to_height_ratio; 475 | auto angle = rand_double(0, 2 * std::numbers::pi); /* Generate random angle */ 476 | Point3D sphere_center{ 477 | /* Use sin/cos on the x/z coordinates. Remember that the cone's base is on 478 | the xz-plane. */ 479 | radius_at_this_y * std::sin(angle), 480 | random_y, 481 | radius_at_this_y * std::cos(angle), 482 | }; 483 | 484 | /* Generate random sphere (ornament) radius */ 485 | auto sphere_radius = rand_double(0.25, 0.45); 486 | 487 | /* If this ornament with the center and radius will come close (here, this means within 488 | 0.1) of intersecting any of the previously-placed ornaments, then reject it and 489 | generate another random ornament in the next iteration of this `while(true)` loop. */ 490 | if (std::any_of(world.begin(), world.end(), [&](const std::shared_ptr &obj) { 491 | auto s = dynamic_pointer_cast(obj); 492 | if (s == nullptr) {return false;} 493 | return (sphere_center - s->center).mag() <= sphere_radius + s->radius + 0.1; 494 | })) { 495 | continue; 496 | } 497 | 498 | /* If this ornament does not come too close to intersecting with any of the previous 499 | ornaments we placed, then we will add it to the scene. First, choose its material. 500 | All ornaments will be metal, with color uniformly chosen from the `colors` array, 501 | and metal fuzz factor very low (randomly chosen in the range [0, 0.1]). */ 502 | std::shared_ptr material; 503 | material = ms( 504 | colors[rand_int(0, static_cast(colors.size() - 1))], 505 | rand_double(0, 0.1) 506 | ); 507 | /* The first ornament, which is located at the very top of the Christmas tree, will 508 | instead be a white `DiffuseLight` with a relative intensity of 10. This is similar to 509 | the star ornament placed at the top of many Christmas trees I've seen. */ 510 | if (i == 0) { 511 | material = ms(RGB::from_mag(1), 10); 512 | } 513 | 514 | /* Add the current ornament to the scene, and we are done, so `break` afterwards. */ 515 | world.add(ms(sphere_center, sphere_radius, material)); 516 | 517 | break; 518 | } 519 | } 520 | 521 | /* Now, generate particles of snow in the scene. Each snow particle will be a very small 522 | white Lambertian sphere. */ 523 | std::vector> snow; 524 | auto snow_material = ms(RGB::from_mag(1)); /* White Lambertian material */ 525 | for (int i = 0; i < 4000; ++i) { /* Generate 4000 snow particles */ 526 | 527 | /* Generate a snow particle */ 528 | while (true) { 529 | 530 | /* Choose random point as the center of the snow particle (which, remember, is a 531 | sphere), and a random radius. */ 532 | Point3D snow_center{rand_double(-30, 30), rand_double(0, 30), rand_double(-50, 50)}; 533 | /* If the snow particle is close to the camera center, then make its size smaller, 534 | otherwise the particles will look strangely large in the final result. */ 535 | auto snow_radius = (snow_center.z > 35 ? 0.015 : (snow_center.z > 20 ? 0.03 : 0.05)); 536 | 537 | /* If the current snow particle comes too close (within 0.1, here) of intersecting any 538 | of the ornaments, then reject it and generate another one in the next iteration of this 539 | `while(true)` loop. 540 | 541 | Note that we do not check if snow particles intersect each other. This is to avoid 542 | this loop taking `O((number of snow particles)^2) time, and because in general the 543 | snow particles are so small that (a) it's very unlikely for them to intersect given 544 | the sheer size of the scene compared to the size of the snow particles, and (b) they 545 | are so small that an intersection would probably be unnoticeable anyways. */ 546 | if (std::any_of(world.begin(), world.end(), [&](const std::shared_ptr &obj) { 547 | auto s = dynamic_pointer_cast(obj); 548 | if (s == nullptr) {return false;} 549 | return (snow_center - s->center).mag() <= snow_radius + s->radius + 0.1; 550 | })) { 551 | continue; 552 | } 553 | 554 | /* Add the snow particle to the list of snow particles (which is separate from the 555 | `Scene` because in the `if`-statement above, the range from `world.begin()` up to 556 | but not includin `world.end()` represents only the ornaments), and `break`. */ 557 | snow.push_back(ms(snow_center, snow_radius, snow_material)); 558 | 559 | break; 560 | } 561 | } 562 | 563 | /* Now, add all the snow particles to the scene. */ 564 | for (const auto &i : snow) {world.add(i);} 565 | 566 | /* Render the image. */ 567 | Camera() 568 | /* Specify the rendered image dimensions and the background color */ 569 | .set_image_by_width_and_aspect_ratio(1080, 16. / 9.) 570 | .set_background(RGB::zero()) 571 | /* Specify the camera itself and its vertical field-of-view */ 572 | .set_camera_center(Point3D{0, 17.5, 50}) // 17.5 573 | .set_camera_direction_towards(Point3D{0, 10, 0}) 574 | .set_camera_up_direction(Point3D{0, 1, 0}) 575 | .set_vertical_fov(35) 576 | /* Specify the quality of the render (number of samples per pixel, and 577 | the maximum number of ray bounces) */ 578 | .set_samples_per_pixel(10000) 579 | .set_max_depth(50) 580 | /* Render the image and send it to a PPM file */ 581 | .render(world) 582 | .send_as_ppm("christmas_tree_of_spheres.ppm"); 583 | } 584 | 585 | void bvh_pathological_test() { 586 | Scene world; 587 | 588 | /* This results in a BVH tree with depth 116. The idea is that if spheres increase 589 | exponentially in size, then the SAH will prefer to partition so that the largest 590 | sphere gets its own node. This means the depth would theoretically be linear, 591 | not logarithmic, in the number of primitives, which is what happens here. */ 592 | int num_spheres = 135; 593 | double pos_scale = 10.7; 594 | double rad_scale = 17.3; 595 | for (int i = 0; i < num_spheres; ++i) { 596 | world.add(ms( 597 | Point3D{std::pow(pos_scale, i), 0, 0}, 598 | std::pow(rad_scale, i), 599 | ms(RGB::zero()) 600 | )); 601 | } 602 | 603 | BVH bvh(world); 604 | 605 | /* Code I used to find the above values. Note that `BVH::build_bvh_tree` needs to be modified 606 | to have a `depth` parameter, and there also needs to be a `max_depth` global variable in 607 | `bvh.h`. 608 | 609 | std::atomic maxmax_depth = 0; 610 | std::mutex mtx2, mtx3; 611 | std::tuple info{0, 0., 0.}; 612 | #pragma omp parallel for schedule(dynamic) 613 | for (int spheres = 100; spheres <= 200; ++spheres) { 614 | { 615 | std::lock_guard guard(mtx2); 616 | std::cout << "Testing " << spheres << " spheres" << std::endl; 617 | } 618 | for (double pos = 10; pos <= 30; pos += 0.1) { 619 | for (double rad = 10; rad <= 20; rad += 0.1) { 620 | Scene world; 621 | 622 | for (int i = 0; i < spheres; ++i) { 623 | world.add(ms( 624 | Point3D{std::pow(pos, i), 0, 0}, 625 | std::pow(rad, i), 626 | ms(RGB::zero()) 627 | )); 628 | } 629 | 630 | max_depth = 0; 631 | BVH bvh(world); 632 | 633 | { 634 | std::lock_guard guard(mtx3); 635 | if (max_depth > maxmax_depth) { 636 | maxmax_depth = max_depth; 637 | info = {spheres, pos, rad}; 638 | } 639 | } 640 | } 641 | } 642 | } 643 | 644 | std::cout << maxmax_depth << " <- result" << std::endl; 645 | 646 | std::cout << std::get<0>(info) << " " << std::get<1>(info) << " " 647 | << std::get<2>(info) << std::endl; 648 | return 0; 649 | */ 650 | } 651 | 652 | int main() 653 | { 654 | switch(4) { 655 | case -10: bvh_pathological_test(); break; 656 | case -4: rtow_final_image(); break; 657 | case -3: rtow_final_lights_with_tone_mapping(); break; 658 | case -2: millions_of_spheres(); break; 659 | case -1: millions_of_spheres_with_lights(); break; 660 | case 0: parallelogram_test(); break; 661 | case 1: cornell_box_test(true); break; 662 | case 2: cornell_box_test(false); break; 663 | case 3: raining_on_the_dance_floor(); break; 664 | case 4: christmas_tree_made_of_spheres(); break; 665 | default: std::cout << "Nothing to do" << std::endl; break; 666 | } 667 | 668 | return 0; 669 | } 670 | --------------------------------------------------------------------------------