Skip to content

Commit c6f803f

Browse files
committed
Improve random_unit_vector()
The old method had a floating-point weakness in which all three vector components, when small enough, can yield a vector length that underflows to zero, leading to a bogus [+/- infinity, +/- infinity, +/- infinity] result. This change also eliminates the `random_in_unit_sphere()` function, and does everything inside the `random_unit_vector()` function, which allows us to compute the vector length only once and then re-use it for normalization. Resolves RayTracing#1606
1 parent 3c67b90 commit c6f803f

File tree

5 files changed

+51
-53
lines changed

5 files changed

+51
-53
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Change Log / Ray Tracing in One Weekend
1111
- Fix -- Big improvement to print version listing font size (#1595) and more compact line
1212
height for code listings in both print and browser.
1313
- Change -- Include hittable.h from material.h; drop `hit_record` forward declaration (#1609)
14+
- Fix -- Fixed possible bogus values from `random_unit_vector()` due to underflow (#1606)
1415

1516
### In One Weekend
1617
- Fix -- Fixed usage of the term "unit cube" for a cube of diameter two (#1555, #1603)

books/RayTracingInOneWeekend.html

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,24 +2292,29 @@
22922292
surprisingly complicated to understand, and quite a bit complicated to implement. Instead, we'll use
22932293
what is typically the easiest algorithm: A rejection method. A rejection method works by repeatedly
22942294
generating random samples until we produce a sample that meets the desired criteria. In other words,
2295-
keep rejecting samples until you find a good one.
2295+
keep rejecting bad samples until you find a good one.
22962296

22972297
There are many equally valid ways of generating a random vector on a hemisphere using the rejection
22982298
method, but for our purposes we will go with the simplest, which is:
22992299

2300-
1. Generate a random vector inside of the unit sphere
2301-
2. Normalize this vector
2300+
1. Generate a random vector inside the unit sphere
2301+
2. Normalize this vector to extend it to the sphere surface
23022302
3. Invert the normalized vector if it falls onto the wrong hemisphere
23032303

23042304
<div class='together'>
23052305
First, we will use a rejection method to generate the random vector inside the unit sphere (that is,
23062306
a sphere of radius 1). Pick a random point inside the cube enclosing the unit sphere (that is, where
2307-
$x$, $y$, and $z$ are all in the range $[-1,+1]$). If this point lies outside (or on) the unit
2308-
sphere, then generate a new one until we find one that lies inside the unit sphere.
2307+
$x$, $y$, and $z$ are all in the range $[-1,+1]$). If this point lies outside the unit
2308+
sphere, then generate a new one until we find one that lies inside or on the unit sphere.
23092309

2310-
![Figure [sphere-vec]: Two vectors were rejected before finding a good one
2310+
![Figure [sphere-vec]: Two vectors were rejected before finding a good one (pre-normalization)
23112311
](../images/fig-1.11-sphere-vec.jpg)
23122312

2313+
![Figure [sphere-vec]: The accepted random vector is normalized to produce a unit vector
2314+
](../images/fig-1.12-sphere-unit-vec.jpg)
2315+
2316+
Here's our first draft of the function:
2317+
23132318
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
23142319
...
23152320

@@ -2319,49 +2324,45 @@
23192324

23202325

23212326
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
2322-
inline vec3 random_in_unit_sphere() {
2327+
inline vec3 random_unit_vector() {
23232328
while (true) {
23242329
auto p = vec3::random(-1,1);
2325-
if (p.length_squared() < 1)
2326-
return p;
2330+
auto lensq = p.length_squared();
2331+
if (lensq <= 1)
2332+
return p / sqrt(lensq);
23272333
}
23282334
}
23292335
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2330-
[Listing [random-in-unit-sphere]: <kbd>[vec3.h]</kbd> The random_in_unit_sphere() function]
2336+
[Listing [random-in-unit-sphere]: <kbd>[vec3.h]</kbd> The random_unit_vector() function, version one]
23312337

23322338
</div>
23332339

2334-
<div class='together'>
2335-
Once we have a random vector in the unit sphere we need to normalize it to get a vector _on_ the
2336-
unit sphere.
2340+
Sadly, we have a small floating-point abstraction leak to deal with. Since floating-point numbers
2341+
have finite precision, a very small value can underflow to zero when squared. So if all three
2342+
coordinates are small enough (that is, very near the center of the sphere), the norm of the vector
2343+
will be zero, and thus normalizing will yield the bogus vector $[\pm\infty, \pm\infty, \pm\infty]$.
2344+
To fix this, we'll also reject points that lie inside this "black hole" around the center. With
2345+
double precision (64-bit floats), we can safely support values greater than $10^{-160}$.
23372346

2338-
![Figure [sphere-vec]: The accepted random vector is normalized to produce a unit vector
2339-
](../images/fig-1.12-sphere-unit-vec.jpg)
2347+
Here's our more robust function:
23402348

23412349
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
2342-
...
2343-
2344-
inline vec3 random_in_unit_sphere() {
2350+
inline vec3 random_unit_vector() {
23452351
while (true) {
23462352
auto p = vec3::random(-1,1);
2347-
if (p.length_squared() < 1)
2348-
return p;
2349-
}
2350-
}
2351-
2352-
2353+
auto lensq = p.length_squared();
23532354
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight
2354-
inline vec3 random_unit_vector() {
2355-
return unit_vector(random_in_unit_sphere());
2355+
if (1e-160 < lensq && lensq <= 1)
2356+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
2357+
return p / sqrt(lensq);
2358+
}
23562359
}
23572360
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2358-
[Listing [random-unit-vec]: <kbd>[vec3.h]</kbd> Random vector on the unit sphere]
2359-
2360-
</div>
2361+
[Listing [random-in-unit-sphere]: <kbd>[vec3.h]</kbd> The random_unit_vector() function, version one]
23612362

23622363
<div class='together'>
2363-
And now that we have a random vector on the surface of the unit sphere, we can determine if it is on
2364-
the correct hemisphere by comparing against the surface normal:
2364+
Now that we have a random vector on the surface of the unit sphere, we can determine if it is on the
2365+
correct hemisphere by comparing against the surface normal:
23652366

23662367
![Figure [normal-hor]: The normal vector tells us which hemisphere we need
23672368
](../images/fig-1.13-surface-normal.jpg)
@@ -2377,7 +2378,12 @@
23772378
...
23782379

23792380
inline vec3 random_unit_vector() {
2380-
return unit_vector(random_in_unit_sphere());
2381+
while (true) {
2382+
auto p = vec3::random(-1,1);
2383+
auto lensq = p.length_squared();
2384+
if (1e-160 < lensq && lensq <= 1)
2385+
return p / sqrt(lensq);
2386+
}
23812387
}
23822388

23832389

src/InOneWeekend/vec3.h

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,15 @@ inline vec3 random_in_unit_disk() {
121121
}
122122
}
123123

124-
inline vec3 random_in_unit_sphere() {
124+
inline vec3 random_unit_vector() {
125125
while (true) {
126126
auto p = vec3::random(-1,1);
127-
if (p.length_squared() < 1)
128-
return p;
127+
auto lensq = p.length_squared();
128+
if (1e-160 < lensq && lensq <= 1.0)
129+
return p / sqrt(lensq);
129130
}
130131
}
131132

132-
inline vec3 random_unit_vector() {
133-
return unit_vector(random_in_unit_sphere());
134-
}
135-
136133
inline vec3 random_on_hemisphere(const vec3& normal) {
137134
vec3 on_unit_sphere = random_unit_vector();
138135
if (dot(on_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal

src/TheNextWeek/vec3.h

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,15 @@ inline vec3 random_in_unit_disk() {
121121
}
122122
}
123123

124-
inline vec3 random_in_unit_sphere() {
124+
inline vec3 random_unit_vector() {
125125
while (true) {
126126
auto p = vec3::random(-1,1);
127-
if (p.length_squared() < 1)
128-
return p;
127+
auto lensq = p.length_squared();
128+
if (1e-160 < lensq && lensq <= 1.0)
129+
return p / sqrt(lensq);
129130
}
130131
}
131132

132-
inline vec3 random_unit_vector() {
133-
return unit_vector(random_in_unit_sphere());
134-
}
135-
136133
inline vec3 random_on_hemisphere(const vec3& normal) {
137134
vec3 on_unit_sphere = random_unit_vector();
138135
if (dot(on_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal

src/TheRestOfYourLife/vec3.h

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,15 @@ inline vec3 random_in_unit_disk() {
121121
}
122122
}
123123

124-
inline vec3 random_in_unit_sphere() {
124+
inline vec3 random_unit_vector() {
125125
while (true) {
126126
auto p = vec3::random(-1,1);
127-
if (p.length_squared() < 1)
128-
return p;
127+
auto lensq = p.length_squared();
128+
if (1e-160 < lensq && lensq <= 1.0)
129+
return p / sqrt(lensq);
129130
}
130131
}
131132

132-
inline vec3 random_unit_vector() {
133-
return unit_vector(random_in_unit_sphere());
134-
}
135-
136133
inline vec3 random_on_hemisphere(const vec3& normal) {
137134
vec3 on_unit_sphere = random_unit_vector();
138135
if (dot(on_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal

0 commit comments

Comments
 (0)