@@ -26,79 +26,108 @@ class Parallelogram : public Hittable {
26
26
/* `material` = The material of this `Parallelogram` object. */
27
27
std::shared_ptr<Material> material;
28
28
29
- /* `unit_plane_normal` = n.unit_vector(), where `n = cross(side1, side2)`, a normal vector
30
- to the plane containing this `Parallelogram`. We precompute this quantity, because it is
31
- returned as a part of the `hit_info` for every ray-`Parallelogram` intersection in
32
- `Parallelogram::hit_by()` as the `outward_unit_surface_normal`.
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.
33
35
34
- Note that for a flat object, "outside" and "inside" is arbitrary. We just keep it simple
35
- here and say that the direction of `cross(side1, side2)` is the outside normal. You could
36
- say instead that the direction of `cross(side1, side2)` is the INSIDE normal; it'd make
37
- no difference, except for flipping the `hit_from_outside` for all `hit_info`s returned
38
- from `Parallelogram::hit_by()`. */
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. */
39
42
Vec3D unit_plane_normal;
40
43
/* `scaled_plane_normal` = n / dot(n, n), where `n = cross(side1, side2)`, a normal vector
41
44
to the plane containing this `Parallelogram`. We precompute this quantity to be used in
42
45
`Parallelogram::hit_by()`. */
43
46
Vec3D scaled_plane_normal;
44
-
45
47
/* `aabb` = The AABB (Axis-Aligned Bounding Box) for this `Parallelogram`. */
46
48
AABB aabb;
47
49
48
50
public:
49
51
50
52
/* There are three steps to perform a ray-parallelogram intersection check:
51
- 1. Find the plane that contains the parallelogram
52
- 2. Find the intersection point of the ray with that parallelogram-containing plane
53
- 3. Determine if the hitpoint of the ray on the plane lies within the parallelogram itself.
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.
54
56
55
- STEP 1: Find the plane that intersects the parallelogram.
56
- Remember that a plane is determined by a normal vector `n` to that plane, as well as a
57
- point on the plane. We want to find the plane that contains the parallelogram. Now, a
58
- normal vector to the parallelogram (and so also a normal vector to the plane containing
59
- that parallelogram) can be found by taking the cross product of the two side vectors;
60
- `cross(side, side2)`. Then, we need an arbitrary point on the plane; for this, we can just
61
- take the given vertex `vertex`, which we know to be on the parallelogram (and so we know
62
- it to be on the parallelogram-containing plane as well).
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).
63
66
64
- Now, we have that a normal to the plane that contains this parallelogram is
65
- `n = cross(side1, side2)`, and a point on the plane is `vertex`. As a result, the plane
66
- equation can be written as `dot(n, P - vertex) = 0` for points `P (so a point `P` is on
67
- the plane iff the vector from Q to it is normal to the plane's normal vector). An equivalent
68
- and more useful formulation of this equation that we will use is `dot(n, P) = dot(n, vertex)`.
69
- This completes the first step.
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.
70
81
71
82
STEP 2: Find the intersection point of the ray with the parallelogram-containing plane.
72
- Let the ray be defined by R(t) = O + td. First, we solve for the hit time of the ray
73
- with the plane; that is, we find the `t` that satisfies `dot(n, R(t)) = dot(n, vertex)`.
74
- Because R(t) = O + td, this becomes `dot(n, O + td) = dot(n, vertex)`, and so
75
- `dot(n, O) + dot(n, td) = dot(n, vertex)`. Solving, we find that
76
- `t = dot(n, vertex - O) / dot(n, d)`.
77
-
78
- Observe that it is possible for a ray to not intersect the plane; this happens when the
79
- ray is parallel to the plane and the ray's origin is not on the plane. When the ray is
80
- parallel to the plane, then it will be perpendicular to the plane's normal vector, and
81
- so `dot(n, d)` will equal 0, allowing us to detect such cases. In practice, we will
82
- just reject all rays where `dot(n, d)` is small, to avoid numerical issues. In my
83
- implementation below, I reject all rays where `|dot(n, d)| < 1e-9`.
84
-
85
- We now have shown that the hit time is `t = dot(n, vertex - O) / dot(n, d)` (assuming
86
- that `dot(n, d)` is not too small; if it is, then again, we just assume the ray does
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
87
116
not hit this parallelogram to avoid numerical issues). To find the hit point, just
88
- find `O + td ` with that value of `t`. This completes the second step.
117
+ find `R(t) = O + tD ` with that value of `t`. This completes the second step.
89
118
90
119
STEP 3: Determine if the hit point of the ray with the parallelogram-containing plane also
91
120
lies within the parallelogram itself.
92
121
93
122
First, observe that {`side1`, `side2`} forms a basis for the parallelogram-containing plane,
94
123
because `side1` and `side2` are linearly independent (since they are not parallel), and because
95
- they are two vectors in a space (a plane) of dimension two. Now, we choose the "origin" of
124
+ they are two vectors in a space of dimension two (the plane) . Now, we choose the "origin" of
96
125
the parallelogram-containing plane to be `vertex`. Then, we know that
97
126
(a) Because {`side1`, `side2`} is a basis of the plane, there exist unique scalars alpha/beta
98
127
such that `hit_point = vertex + alpha * side1 + beta * side2`.
99
128
(b) Because of the definition of a parallelogram, and because we chose the origin of the plane
100
129
to be `vertex` itself, we know that the hit point is inside the parallelogram if and only if
101
- 0 <= alpha, beta <= 1, where `hit_point = vertex + alpha * side1 + beta * side`, as stated
130
+ 0 <= alpha, beta <= 1, where `hit_point = vertex + alpha * side1 + beta * side2` as stated
102
131
above.
103
132
104
133
Thus, it remains to solve the equation `hit_point = vertex + alpha * side1 + beta * side2`.
@@ -147,22 +176,38 @@ class Parallelogram : public Hittable {
147
176
object is returned. */
148
177
std::optional<hit_info> hit_by (const Ray3D &ray, const Interval &ray_times) const override {
149
178
150
- /* The hit time is equal to dot(n, vertex - ray.origin) / dot(n, ray.dir), where `n` is
151
- any normal to the parallelogram-containing plane. Since we have already precomputed
152
- `scaled_plane_normal`, we just use that. All that matters is that the `n` used in the
153
- numerator and denominator of the fraction is the same `n`.
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.
154
195
155
- Now, as explained above, we first check if the ray is parallel (or very close to parallel)
156
- to the parallelogram-containing plane. If so, we just return an empty
157
- `std::optional<hit_info>`, signifying that the ray did not intersect this parallelogram. */
158
- auto hit_time_denominator = dot (scaled_plane_normal, ray.dir );
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<hit_info>` 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` */
159
201
if (std::fabs (hit_time_denominator) < 1e-9 ) {
160
202
return {};
161
203
}
162
204
163
205
/* If the ray is not parallel/very close to parallel to the plane, compute the hit time.
164
206
First, make sure that the hit time is in the desired time range `ray_times`. */
165
- auto hit_time = dot (scaled_plane_normal, vertex - ray.origin ) / hit_time_denominator;
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. */
166
211
if (!ray_times.contains_exclusive (hit_time)) { /* Remember that `ray_times` is exclusive */
167
212
return {};
168
213
}
@@ -189,8 +234,8 @@ class Parallelogram : public Hittable {
189
234
return hit_info (hit_time, hit_point, unit_plane_normal, ray, material);
190
235
}
191
236
192
- /* The ray hit the plane, but the hitpoint was not in the parallelogram itself, so we will
193
- return an empty `std::optional<hit_info>`.*/
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<hit_info>`.*/
194
239
return {};
195
240
}
196
241
@@ -235,12 +280,13 @@ class Parallelogram : public Hittable {
235
280
/* The `AABB` for a `Parallelogram` is simply the minimum-size `AABB` that contains all the
236
281
vertices of the parallelogram; that is, the `AABB` containing `vertex`, `vertex + side1`,
237
282
`vertex + side2`, and the opposite vertex (which, remember, is calculated by
238
- `vertex + side1 + side2` for parallelograms).
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.
239
285
240
286
Then, because parallelograms are 2D, the resulting AABB may have zero thickness in one
241
- of its dimensions if it is parallel to the xy-/xz-/yz-plane , which can result in numerical
242
- issues. To avoid this, we pad the axis intervals of the AABB, making sure that every
243
- axis interval has length at least some small constant (1e-4 here). */
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). */
244
290
aabb = AABB::from_points ({
245
291
vertex, /* The given vertex of this parallelogram */
246
292
vertex + side1, /* The vertex opposite to `vertex` along the first side */
0 commit comments