Skip to content

Commit 5cdb229

Browse files
committed
Merge branch 'parallelograms'
2 parents 2f6bfc9 + fae7309 commit 5cdb229

16 files changed

+688
-114
lines changed

.vscode/c_cpp_properties.json

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"name": "Linux",
55
"includePath": [
66
"${workspaceFolder}/**",
7-
"/home/kxiao/devel/competitive/kxlib"
87
],
98
"defines": [],
109
"compilerPath": "/usr/bin/gcc",

TODO.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# TODO
2+
3+
### Features
4+
- [ ] Spatial and surface textures
5+
- [ ] Instancing (This enables easy object translation and/or rotation)
6+
- [ ] Volumes (smoke, fog, etc)
7+
- Idea: Procedurally generate volumes, so that they have a more natural shape. Look into Perlin noise.
8+
9+
### Optimizations
10+
- [ ] Optimize BVH by compressing the binary tree to an array. This improves cache locality.
11+
- Don't forget to build the BVH over a copy of the primitives, as we already do. This is what they do in PBR.
12+
- [ ] Use C++20 concepts to guarantee that `Camera::render()` uses compile-time polymorphism rather than runtime polymorphism and dynamic dispatch, when possible.
13+
14+
### Documentation
15+
- [x] Add comments to the `aabb` fields of Sphere and Parallelogram.

rendered_images/cornell_box_1.png

4.72 MB
Loading

rendered_images/empty_cornell_box.png

4.97 MB
Loading
5.84 MB
Loading

src/aabb.h

+30-11
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,31 @@ class AABB {
125125
}
126126

127127
/* Updates (possibly expands) this `AABB` to also bound the `AABB` `other`. */
128-
void merge_with(const AABB &other) {
128+
auto& merge_with(const AABB &other) {
129129
/* Just combine the x-, y-, and z- intervals with those from `other` */
130130
x.merge_with(other.x);
131131
y.merge_with(other.y);
132132
z.merge_with(other.z);
133+
return *this;
133134
}
134135

135136
/* Updates (possibly expands) this `AABB` to also bound the `Point3D` `p`. */
136-
void merge_with(const Point3D &p) {
137+
auto& merge_with(const Point3D &p) {
137138
x.merge_with(p.x);
138139
y.merge_with(p.y);
139140
z.merge_with(p.z);
141+
return *this;
142+
}
143+
144+
/* Setters */
145+
146+
/* Pads all axes with length less than `min_axis_length` to have length exactly
147+
`min_axis_length` (well, "exactly" as far as floating-point arithmetic can give you). */
148+
auto& ensure_min_axis_length(double min_axis_length) {
149+
if (x.size() < min_axis_length) {x.pad_with((min_axis_length - x.size()) / 2);}
150+
if (y.size() < min_axis_length) {y.pad_with((min_axis_length - y.size()) / 2);}
151+
if (z.size() < min_axis_length) {z.pad_with((min_axis_length - z.size()) / 2);}
152+
return *this;
140153
}
141154

142155
/* Overload `operator<<` to allow printing `AABB`s to output streams */
@@ -146,7 +159,10 @@ class AABB {
146159

147160
/* The default constructor contructs an empty `AABB`; that is, the `AABB` where all intervals
148161
are the empty interval `Interval::empty()`. Note: Prefer using the named constructor
149-
`AABB::empty()` instead; its functionality is equivalent, and it is more readable. */
162+
`AABB::empty()` instead; its functionality is equivalent, and it is more readable. This
163+
default constructor exists merely to allow other classes which have an `AABB` as a member
164+
to themselves be default constructible without having to specify a default member initializer
165+
for fields of type `AABB`. */
150166
AABB() : AABB(Interval::empty(), Interval::empty(), Interval::empty()) {}
151167

152168
/* --- NAMED CONSTRUCTORS --- */
@@ -160,17 +176,20 @@ class AABB {
160176

161177
/* Constructs an AABB (Axis-Aligned Bounding Box) consisting of all points with x-coordinate
162178
in the interval `x_`, y-coordinate in the interval `y_`, and z-coordinate in the range `z_`.
163-
In other words, this creates the AABB from the three "slabs" `x_`, `y_`, and `z_`. */
164-
static AABB from_intervals(const Interval &x_, const Interval &y_, const Interval &z_) {
179+
In other words, this creates the AABB from the three "slabs" (axis intervals) `x_`, `y_`, and
180+
`z_`. */
181+
static AABB from_axis_intervals(const Interval &x_, const Interval &y_, const Interval &z_) {
165182
return AABB(x_, y_, z_);
166183
}
167184

168-
/* Constructs an AABB (Axis-Aligned Bounding Box) with extreme points `a` and `b`; that is,
169-
the smallest axis-aligned bounding box that contains the points `a` and `b`. */
170-
static AABB from_extrema(const Point3D &a, const Point3D &b) {
171-
return AABB(Interval(std::fmin(a.x, b.x), std::fmax(a.x, b.x)),
172-
Interval(std::fmin(a.y, b.y), std::fmax(a.y, b.y)),
173-
Interval(std::fmin(a.z, b.z), std::fmax(a.z, b.z)));
185+
/* Constructs the minimum-volume AABB (Axis-Aligned Bounding Box) containing all the points
186+
specified in `points`. */
187+
static AABB from_points(std::initializer_list<Point3D> points) {
188+
auto ret = AABB::empty();
189+
for (const auto &p : points) {
190+
ret.merge_with(p);
191+
}
192+
return ret;
174193
}
175194

176195
/* Returns the minimum-volume `AABB` that contains both of the `AABB`s `a` and `b`.

src/box.h

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#ifndef BOX_SURFACE_H
2+
#define BOX_SURFACE_H
3+
4+
#include <array>
5+
#include "hittable.h"
6+
#include "parallelogram.h"
7+
#include "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> 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<hit_info>` is returned. */
24+
std::optional<hit_info> 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<std::shared_ptr<Hittable>> 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> 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<Parallelogram>(min_coordinates, side_x, side_y, material));
78+
faces.add(std::make_shared<Parallelogram>(min_coordinates, side_x, side_z, material));
79+
faces.add(std::make_shared<Parallelogram>(min_coordinates, side_y, side_z, material));
80+
81+
faces.add(std::make_shared<Parallelogram>(max_coordinates, -side_x, -side_y, material));
82+
faces.add(std::make_shared<Parallelogram>(max_coordinates, -side_x, -side_z, material));
83+
faces.add(std::make_shared<Parallelogram>(max_coordinates, -side_y, -side_z, material));
84+
}
85+
};
86+
87+
#endif

src/bvh.h

+16-11
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ class BVH : public Hittable {
108108
`NUM_BUCKETS` = the number of buckets to test
109109
*/
110110
const size_t MAX_PRIMITIVES_IN_NODE, NUM_BUCKETS;
111-
/* `total_bvh_nodes` = the number of `BVHNode`s in this `BVH` tree. This does not count
112-
the `Scene`s made in `BVHNode::as_leaf_node()`, which the leaf `BVHNodes` point to. */
113-
size_t total_bvh_nodes = 0;
111+
/* `total_bvhnodes` = the number of `BVHNode`s in this `BVH` tree. This does not count
112+
the `Scene`s made in `BVHNode::as_leaf_node()`, which the leaf `BVHNodes` point to. That is,
113+
this does not count the actual number of BVH nodes; this counts the number of `BVHNode`s. */
114+
size_t total_bvhnodes = 0;
114115

115116
/* Used to have "free on memory that was not `malloc`ed" error. Fixed it with this change:
116117
before, `objects` was a vector of `Hittable*`, and in the `start == end - 1` condition seems
@@ -125,7 +126,7 @@ class BVH : public Hittable {
125126
memory, and when the second reference counter drops to 0, it tries to destroy the object again,
126127
leading to the double-free error. */
127128
auto build(std::span<std::shared_ptr<Hittable>> objects) {
128-
++total_bvh_nodes;
129+
++total_bvhnodes;
129130

130131
/* If there is only one primitive left, then we will return a leaf `BVHNode`
131132
(a `BVHNode` whose children are both primitives) that just contains that one primitive. */
@@ -396,18 +397,22 @@ class BVH : public Hittable {
396397
: MAX_PRIMITIVES_IN_NODE{max_primitives_in_node},
397398
NUM_BUCKETS{num_buckets}
398399
{
399-
std::cout << "Building BVH over " << world.size() << " primitives..." << std::endl;
400+
/* Build the Bounding Volume Hierarchy over the primitive components of the `Hittable`
401+
objects in the scene, rather than just the objects themselves. This is because
402+
`Hittable` objects may be compound; they may contain other `Hittable`s. Building a
403+
`BVH` over just the `Hittable` objects themselves could therefore */
404+
auto primitive_components = world.get_primitive_components();
405+
406+
std::cout << "Building BVH over " << world.size() << " objects ("
407+
<< primitive_components.size() << " primitives)..." << std::endl;
400408
auto start = std::chrono::steady_clock::now();
401409

402-
/* Create a copy of the objects list of `world`, because `world` is passed as `const&`, but
403-
we need to modify the objects list when building the `BVH` (specifically, we may need to
404-
rearrange it using `std::partition`) */
405-
std::vector<std::shared_ptr<Hittable>> objects_copy = world;
406-
root = build(objects_copy);
410+
/* Build the `BVH` */
411+
root = build(primitive_components);
407412

408413
std::cout << "Constructed BVH in "
409414
<< ms_diff(start, std::chrono::steady_clock::now())
410-
<< "ms (created " << total_bvh_nodes << " BVHNodes total)\n" << std::endl;
415+
<< "ms (created " << total_bvhnodes << " BVHNodes total)\n" << std::endl;
411416
}
412417
};
413418

src/camera.h

+17-4
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class Camera {
6262
of the defocus disk. Both are determined by `focal_length`, `defocus_angle`, and
6363
`cam_basis_x/y`. */
6464
Vec3D defocus_disk_x, defocus_disk_y;
65-
/* Coordinates of the top-left image pixel */
65+
/* Coordinates of the top-left image pixel (calculated in `init()`) */
6666
Point3D pixel00_loc;
6767
/* Number of rays sampled per pixel, 100 by default */
6868
size_t samples_per_pixel = 100;
@@ -76,6 +76,10 @@ class Camera {
7676
determined from the vertical FOVof 90 degrees and the aspect ratio of the images in `init()`.
7777
*/
7878
std::optional<double> 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)};
7983

8084
/* Set the values of `viewport_w`, `viewport_h`, `pixel_delta_x`, `pixel_delta_y`,
8185
`upper_left_corner`, and `pixel00_loc` based on `image_w` and `image_h`. This function
@@ -238,14 +242,15 @@ class Camera {
238242
we always return `RGB::zero()` when a ray flies into the background). As a result,
239243
all light in the resulting render comes from an actual light source, and not just
240244
from the background. */
245+
return background;
241246
return RGB::zero();
242247

243248
/* If this ray doesn't intersect any object in the scene, then its color is determined
244249
by the background. Here, the background is a blue-to-white gradient depending on the
245250
ray's y-coordinate; bluer for lesser y-coordinates and whiter for larger y-coordinates
246251
(so bluer at the top and whiter at the bottom). */
247-
// return lerp(RGB::from_mag(1, 1, 1), RGB::from_mag(0.5, 0.7, 1),
248-
// 0.5 * ray.dir.unit_vector().y + 0.5);
252+
return lerp(RGB::from_mag(1, 1, 1), RGB::from_mag(0.5, 0.7, 1),
253+
0.5 * ray.dir.unit_vector().y + 0.5);
249254
}
250255
}
251256

@@ -266,7 +271,7 @@ class Camera {
266271
#pragma omp parallel for schedule(dynamic, thread_chunk_size)
267272
for (size_t row = 0; row < image_h; ++row) {
268273
for (size_t col = 0; col < image_w; ++col) {
269-
274+
270275
/* Shoot `samples_per_pixel` random rays through the current pixel.
271276
The average of the resulting colors will be the color for this pixel. */
272277
auto pixel_color = RGB::zero();
@@ -314,6 +319,7 @@ class Camera {
314319
distance. */
315320
auto& set_camera_direction_towards(const Point3D &p) {
316321
camera.dir = p - camera.origin;
322+
camera_lookat.reset();
317323
return *this;
318324
}
319325
/* Set the camera direction to always be towards the point `p`, no matter where the camera
@@ -384,6 +390,13 @@ class Camera {
384390
vertical_fov.reset();
385391
return *this;
386392
}
393+
/* Sets the default background color of the camera to `background_color`. This means that any
394+
ray which hits no object in a scene being rendered by this `Camera` will have color equal to
395+
`background_color`. */
396+
auto& set_background(const RGB &background_color) {
397+
background = background_color;
398+
return *this;
399+
}
387400

388401
/* Prints this `Camera` to the `std::ostream` specified by `os`. */
389402
void print_to(std::ostream &os) {

0 commit comments

Comments
 (0)