Skip to content

Commit 10ad3fd

Browse files
committed
Book2: Rework the AABB chapter
In addition, I've changed the way that AABBs are padded. Originally, the primitive constructing an AABB needed to immediately call the aabb::pad() function after construction. Now, the AABB constructors call aabb::pad_to_minimum() automatically to pad any dimension smaller than some delta in size. Resolves #1236
1 parent 2ee8f9a commit 10ad3fd

File tree

6 files changed

+142
-96
lines changed

6 files changed

+142
-96
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ then.
1616

1717
### In One Weekend
1818
- Change - Update reference to "Fundamentals of Interactive Computer Graphics" to "Computer
19-
Graphics: Principles and Practice". This is the name used by newer editions of the book.
19+
Graphics: Principles and Practice". This is the name used by newer editions of the
20+
book.
2021
- Change - New BVH optimization splits the bounds according to the longest bounding box dimension,
2122
yielding a 15-20% speedup (#1007)
2223

2324
### The Next Week
2425
- Change - `perlin::turb()` no longer defaults the value for the depth parameter.
26+
- Change - AABB automatically pads to mininmum size for any dimension; no longer requires
27+
primitives to call aabb::pad() function.
28+
- Change - Reworked the AABB chapter (#1236)
2529
- New - add section on alternative 2D primitives such as triangle, ellipse and annulus (#1204,
2630
#1205)
2731

books/RayTracingTheNextWeek.html

Lines changed: 101 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -374,22 +374,22 @@
374374
sticking it in this chapter so the code can run faster, and because it refactors `hittable` a
375375
little, and when I add rectangles and boxes we won't have to go back and refactor them.
376376

377-
The ray-object intersection is the main time-bottleneck in a ray tracer, and the time is linear with
378-
the number of objects. But it’s a repeated search on the same model, so we ought to be able to make
377+
Ray-object intersection is the main time-bottleneck in a ray tracer, and the run time is linear with
378+
the number of objects. But it’s a repeated search on the same scene, so we ought to be able to make
379379
it a logarithmic search in the spirit of binary search. Because we are sending millions to billions
380-
of rays on the same model, we can do an analog of sorting the model, and then each ray intersection
381-
can be a sublinear search. The two most common families of sorting are to 1) divide the space, and
382-
2) divide the objects. The latter is usually much easier to code up and just as fast to run for most
383-
models.
380+
of rays into the same scene, we can sort the objects in the scene, and then each ray intersection
381+
can be a sublinear search. The two most common methods of sorting are to 1) subdivide the space, and
382+
2) subdivide the objects. The latter is usually much easier to code up, and just as fast to run for
383+
most models.
384384

385385

386386
The Key Idea
387387
-------------
388-
The key idea of a bounding volume over a set of primitives is to find a volume that fully encloses
389-
(bounds) all the objects. For example, suppose you computed a sphere that bounds 10 objects. Any ray
390-
that misses the bounding sphere definitely misses all ten objects inside. If the ray hits the
391-
bounding sphere, then it might hit one of the ten objects. So the bounding code is always of the
392-
form:
388+
The key idea of creating bounding volumes for a set of primitives is to find a volume that fully
389+
encloses (bounds) all the objects. For example, suppose you computed a sphere that bounds 10
390+
objects. Any ray that misses the bounding sphere definitely misses all ten objects inside. If the
391+
ray hits the bounding sphere, then it might hit one of the ten objects. So the bounding code is
392+
always of the form:
393393

394394
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
395395
if (ray hits bounding object)
@@ -398,8 +398,9 @@
398398
return false
399399
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
400400

401-
A key thing is we are dividing objects into subsets. We are not dividing the screen or the volume.
402-
Any object is in just one bounding volume, but bounding volumes can overlap.
401+
Note that we will use these bounding volumes to group the objects in the scene into subgroups. We
402+
are *not* dividing the screen or the scene space. We want any given object to be in just one
403+
bounding volume, though bounding volumes can overlap.
403404

404405

405406
Hierarchies of Bounding Volumes
@@ -432,33 +433,34 @@
432433
To get that all to work we need a way to make good divisions, rather than bad ones, and a way to
433434
intersect a ray with a bounding volume. A ray bounding volume intersection needs to be fast, and
434435
bounding volumes need to be pretty compact. In practice for most models, axis-aligned boxes work
435-
better than the alternatives, but this design choice is always something to keep in mind if you
436-
encounter unusual types of models.
436+
better than the alternatives (such as the spherical bounds mentioned above), but this design choice
437+
is always something to keep in mind if you encounter other types of bounding models.
437438

438-
From now on we will call axis-aligned bounding rectangular parallelepiped (really, that is what they
439-
need to be called if precise) _axis-aligned bounding boxes_, or AABBs. Any method you want to use to
440-
intersect a ray with an AABB is fine. And all we need to know is whether or not we hit it; we don’t
441-
need hit points or normals or any of the stuff we need to display the object.
439+
From now on we will call axis-aligned bounding rectangular parallelepipeds (really, that is what
440+
they need to be called if we're being precise) _axis-aligned bounding boxes_, or AABBs. (In the
441+
code, you will also come across the naming abbreviation "bbox" for "bounding box".) Any method you
442+
want to use to intersect a ray with an AABB is fine. And all we need to know is whether or not we
443+
hit it; we don’t need hit points or normals or any of the stuff we need to display the object.
442444

443445
<div class='together'>
444446
Most people use the “slab” method. This is based on the observation that an n-dimensional AABB is
445-
just the intersection of $n$ axis-aligned intervals, often called “slabs”. An interval is just the
446-
points between two endpoints, _e.g._, $x$ such that $3 < x < 5$, or more succinctly $x$ in $(3,5)$.
447-
In 2D, two intervals overlapping makes a 2D AABB (a rectangle):
447+
just the intersection of $n$ axis-aligned intervals, often called “slabs”. Recall that an interval
448+
is just the points within two endpoints, for example, $x$ such that $3 \leq x \leq 5$, or more
449+
succinctly $x$ in $[3,5]$. In 2D, an AABB (a rectangle) is defined by the overlap two intervals:
448450

449451
![Figure [2d-aabb]: 2D axis-aligned bounding box](../images/fig-2.02-2d-aabb.jpg)
450452

451453
</div>
452454

453-
For a ray to hit one interval we first need to figure out whether the ray hits the boundaries. For
454-
example, again in 2D, this is the ray parameters $t_0$ and $t_1$. (If the ray is parallel to the
455-
plane, its intersection with the plane will be undefined.)
455+
To determine if a ray hits one interval, we first need to figure out whether the ray hits the
456+
boundaries. For example, in 1D, ray intersection with two planes will yield the ray parameters $t_0$
457+
and $t_1$. (If the ray is parallel to the planes, its intersection with any plane will be
458+
undefined.)
456459

457460
![Figure [ray-slab]: Ray-slab intersection](../images/fig-2.03-ray-slab.jpg)
458461

459-
In 3D, those boundaries are planes. The equations for the planes are $x = x_0$ and $x = x_1$. Where
460-
does the ray hit that plane? Recall that the ray can be thought of as just a function that given a
461-
$t$ returns a location $\mathbf{P}(t)$:
462+
How do we find the intersection between a ray and a plane? Recall that the ray is just defined by a
463+
function that--given a parameter $t$--returns a location $\mathbf{P}(t)$:
462464

463465
$$ \mathbf{P}(t) = \mathbf{A} + t \mathbf{b} $$
464466

@@ -467,7 +469,7 @@
467469

468470
$$ x_0 = A_x + t_0 b_x $$
469471

470-
Thus $t$ at that hitpoint is:
472+
So $t$ at the intersection is given by
471473

472474
$$ t_0 = \frac{x_0 - A_x}{b_x} $$
473475

@@ -476,12 +478,17 @@
476478
$$ t_1 = \frac{x_1 - A_x}{b_x} $$
477479

478480
<div class='together'>
479-
The key observation to turn that 1D math into a hit test is that for a hit, the $t$-intervals need
480-
to overlap. For example, in 2D the green and blue overlapping only happens if there is a hit:
481+
The key observation to turn that 1D math into a 2D or 3D hit test is this: if a ray intersects the
482+
box bounded by all pairs of planes, then all $t$-intervals will overlap. For example, in 2D the
483+
green and blue overlapping only happens if the ray intersects the bounded box:
481484

482485
![Figure [ray-slab-interval]: Ray-slab $t$-interval overlap
483486
](../images/fig-2.04-ray-slab-interval.jpg)
484487

488+
In this figure, the upper ray intervals to not overlap, so we know the ray does _not_ hit the 2D box
489+
bounded by the green and blue planes. The lower ray intervals _do_ overlap, so we know the lower ray
490+
_does_ hit the bounded box.
491+
485492
</div>
486493

487494

@@ -490,41 +497,38 @@
490497
The following pseudocode determines whether the $t$ intervals in the slab overlap:
491498

492499
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
493-
compute (tx0, tx1)
494-
compute (ty0, ty1)
495-
return overlap?( (tx0, tx1), (ty0, ty1))
500+
interval_x &LeftArrow; compute_intersection_x (ray, x0, x1)
501+
interval_y &LeftArrow; compute_intersection_y (ray, y0, y1)
502+
return overlaps(interval_x, interval_y)
496503
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
497504

498505
<div class='together'>
499-
That is awesomely simple, and the fact that the 3D version also works is why people love the slab
500-
method:
506+
That is awesomely simple, and the fact that the 3D version trivially extends the above is why people
507+
love the slab method:
501508

502509
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
503-
compute (tx0, tx1)
504-
compute (ty0, ty1)
505-
compute (tz0, tz1)
506-
return overlap ? ((tx0, tx1), (ty0, ty1), (tz0, tz1))
510+
interval_x &LeftArrow; compute_intersection_x (ray, x0, x1)
511+
interval_y &LeftArrow; compute_intersection_y (ray, y0, y1)
512+
interval_z &LeftArrow; compute_intersection_z (ray, z0, z1)
513+
return overlaps(interval_x, interval_y, interval_z)
507514
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
508515

509516
</div>
510517

511-
There are some caveats that make this less pretty than it first appears. First, suppose the ray is
512-
travelling in the negative $\mathbf{x}$ direction. The interval $(t_{x0}, t_{x1})$ as computed above
513-
might be reversed, _e.g._ something like $(7, 3)$. Second, the divide in there could give us
514-
infinities. And if the ray origin is on one of the slab boundaries, we can get a `NaN`. There are
515-
many ways these issues are dealt with in various ray tracers’ AABB. (There are also vectorization
516-
issues like SIMD which we will not discuss here. Ingo Wald’s papers are a great place to start if
517-
you want to go the extra mile in vectorization for speed.) For our purposes, this is unlikely to be
518-
a major bottleneck as long as we make it reasonably fast, so let’s go for simplest, which is often
519-
fastest anyway! First let’s look at computing the intervals:
518+
There are some caveats that make this less pretty than it first appears. Consider again the 1D
519+
equations for $t_0$ and $t_1$:
520520

521-
$$ t_{x0} = \frac{x_0 - A_x}{b_x} $$
522-
$$ t_{x1} = \frac{x_1 - A_x}{b_x} $$
521+
$$ t_0 = \frac{x_0 - A_x}{b_x} $$
522+
$$ t_1 = \frac{x_1 - A_x}{b_x} $$
523523

524-
One troublesome thing is that perfectly valid rays will have $b_x = 0$, causing division by zero.
525-
Some of those rays are inside the slab, and some are not. Also, the zero will have a ± sign when
526-
using IEEE floating point. The good news for $b_x = 0$ is that $t_{x0}$ and $t_{x1}$ will both be +∞
527-
or both be -∞ if not between $x_0$ and $x_1$. So, using min and max should get us the right answers:
524+
First, suppose the ray is traveling in the negative $\mathbf{x}$ direction. The interval $(t_{x0},
525+
t_{x1})$ as computed above might be reversed, like $(7, 3)$ for example. Second, the denominator
526+
$b_x$ could be zero, yielding infinite values. And if the ray origin lies on one of the slab
527+
boundaries, we can get a `NaN`, since both the numerator and the denominator can be zero. Also, the
528+
zero will have a ± sign when using IEEE floating point.
529+
530+
The good news for $b_x = 0$ is that $t_{x0}$ and $t_{x1}$ will be equal: both +∞ or -∞, if not
531+
between $x_0$ and $x_1$. So, using min and max should get us the right answers:
528532

529533
$$ t_{x0} = \min(
530534
\frac{x_0 - A_x}{b_x},
@@ -536,25 +540,29 @@
536540
\frac{x_1 - A_x}{b_x})
537541
$$
538542

539-
The remaining troublesome case if we do that is if $b_x = 0$ and either $x_0 - A_x = 0$ or $x_1 -
540-
A_x = 0$ so we get a `NaN`. In that case we can probably accept either hit or no hit answer, but
541-
we’ll revisit that later.
543+
The remaining troublesome case if we do that is if $b_x = 0$ and either $x_0 - A_x = 0$ or
544+
$x_1 - A_x = 0$ so we get a `NaN`. In that case we can arbitrarily interpret that as either hit or
545+
no hit, but we’ll revisit that later.
542546

543-
Now, let’s look at that overlap function. Suppose we can assume the intervals are not reversed (so
544-
the first value is less than the second value in the interval) and we want to return true in that
545-
case. The boolean overlap that also computes the overlap interval $(f, F)$ of intervals $(d, D)$ and
546-
$(e, E)$ would be:
547+
Now, let’s look at the pseudo-function `overlaps`. Suppose we can assume the intervals are not
548+
reversed, and we want to return true when the intervals overlap. The boolean `overlaps()` function
549+
computes the overlap of the $t$ intervals `t_interval1` and `t_interval2`, and uses that to
550+
determine if that overlap is non-empty:
547551

548552
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
549-
bool overlap(d, D, e, E, f, F)
550-
f = max(d, e)
551-
F = min(D, E)
552-
return (f < F)
553+
bool overlaps(t_interval1, t_interval2)
554+
t_min &LeftArrow; max(t_interval1.min, t_interval2.min)
555+
t_max &LeftArrow; min(t_interval1.max, t_interval2.max)
556+
return t_min < t_max
553557
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
554558

555-
If there are any `NaN`s running around there, the compare will return false so we need to be sure
556-
our bounding boxes have a little padding if we care about grazing cases (and we probably should
557-
because in a ray tracer all cases come up eventually). Here's the implementation:
559+
If there are any `NaN`s running around there, the compare will return false, so we need to be sure
560+
our bounding boxes have a little padding if we care about grazing cases (and we probably _should_
561+
because in a ray tracer all cases come up eventually).
562+
563+
<div class='together'>
564+
To accomplish this, we'll first add a new `interval` function `expand`, which pads an interval by a
565+
given amount:
558566

559567
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
560568
class interval {
@@ -577,6 +585,10 @@
577585
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
578586
[Listing [interval-expand]: <kbd>[interval.h]</kbd> interval::expand() method]
579587

588+
</div>
589+
590+
<div class='together'>
591+
Now we have everything we need to implment the new AABB class.
580592

581593
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
582594
#ifndef AABB_H
@@ -591,14 +603,19 @@
591603
aabb() {} // The default AABB is empty, since intervals are empty by default.
592604

593605
aabb(const interval& ix, const interval& iy, const interval& iz)
594-
: x(ix), y(iy), z(iz) { }
606+
: x(ix), y(iy), z(iz)
607+
{
608+
pad_to_minimums();
609+
}
595610

596611
aabb(const point3& a, const point3& b) {
597612
// Treat the two points a and b as extrema for the bounding box, so we don't require a
598613
// particular minimum/maximum coordinate order.
599614
x = interval(fmin(a[0],b[0]), fmax(a[0],b[0]));
600615
y = interval(fmin(a[1],b[1]), fmax(a[1],b[1]));
601616
z = interval(fmin(a[2],b[2]), fmax(a[2],b[2]));
617+
618+
pad_to_minimums();
602619
}
603620

604621
const interval& axis(int n) const {
@@ -620,12 +637,25 @@
620637
}
621638
return true;
622639
}
640+
641+
private:
642+
643+
void pad_to_minimums() {
644+
// Adjust the AABB so that no side is narrower than some delta, padding if necessary.
645+
646+
double delta = 0.0001;
647+
if (x.size() < delta) x = x.expand(delta);
648+
if (y.size() < delta) y = y.expand(delta);
649+
if (z.size() < delta) z = z.expand(delta);
650+
}
623651
};
624652

625653
#endif
626654
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
627655
[Listing [aabb]: <kbd>[aabb.h]</kbd> Axis-aligned bounding box class]
628656

657+
</div>
658+
629659

630660
An Optimized AABB Hit Method
631661
-----------------------------
@@ -659,7 +689,7 @@
659689
...
660690
};
661691
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
662-
[Listing [aabb-hit]: <kbd>[aabb.h]</kbd> Axis-aligned bounding box hit function]
692+
[Listing [aabb-hit]: <kbd>[aabb.h]</kbd> Optional optimized AABB hit function]
663693

664694

665695
Constructing Bounding Boxes for Hittables

src/TheNextWeek/aabb.h

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ class aabb {
2121
aabb() {} // The default AABB is empty, since intervals are empty by default.
2222

2323
aabb(const interval& ix, const interval& iy, const interval& iz)
24-
: x(ix), y(iy), z(iz) { }
24+
: x(ix), y(iy), z(iz)
25+
{
26+
pad_to_minimums();
27+
}
2528

2629
aabb(const point3& a, const point3& b) {
2730
// Treat the two points a and b as extrema for the bounding box, so we don't require a
2831
// particular minimum/maximum coordinate order.
2932
x = interval(fmin(a[0],b[0]), fmax(a[0],b[0]));
3033
y = interval(fmin(a[1],b[1]), fmax(a[1],b[1]));
3134
z = interval(fmin(a[2],b[2]), fmax(a[2],b[2]));
35+
36+
pad_to_minimums();
3237
}
3338

3439
aabb(const aabb& box0, const aabb& box1) {
@@ -37,16 +42,6 @@ class aabb {
3742
z = interval(box0.z, box1.z);
3843
}
3944

40-
aabb pad() {
41-
// Return an AABB that has no side narrower than some delta, padding if necessary.
42-
double delta = 0.0001;
43-
interval new_x = (x.size() >= delta) ? x : x.expand(delta);
44-
interval new_y = (y.size() >= delta) ? y : y.expand(delta);
45-
interval new_z = (z.size() >= delta) ? z : z.expand(delta);
46-
47-
return aabb(new_x, new_y, new_z);
48-
}
49-
5045
const interval& axis(int n) const {
5146
if (n == 1) return y;
5247
if (n == 2) return z;
@@ -83,6 +78,17 @@ class aabb {
8378
}
8479

8580
static const aabb empty, universe;
81+
82+
private:
83+
84+
void pad_to_minimums() {
85+
// Adjust the AABB so that no side is narrower than some delta, padding if necessary.
86+
87+
double delta = 0.0001;
88+
if (x.size() < delta) x = x.expand(delta);
89+
if (y.size() < delta) y = y.expand(delta);
90+
if (z.size() < delta) z = z.expand(delta);
91+
}
8692
};
8793

8894
const aabb aabb::empty = aabb(interval::empty, interval::empty, interval::empty);

src/TheNextWeek/quad.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class quad : public hittable {
2828
}
2929

3030
virtual void set_bounding_box() {
31-
bbox = aabb(Q, Q + u + v).pad();
31+
bbox = aabb(Q, Q + u + v);
3232
}
3333

3434
aabb bounding_box() const override { return bbox; }

0 commit comments

Comments
 (0)