Skip to content

Commit 0ce5b90

Browse files
committed
Corrected sphere UV coordinates
Fixed in text and code. This addresses the following problems: - Confusion between UV space and texture image space. There were places that assumed Y/V grows, down, others where Y/V grow upwards. Correct UV coordinates have Y/V growing upwards, as in regular 2D Cartesian coordinates. The V coordinate is flipped for texture-map lookup, left alone for all other coordinate uses. - The Y and Z coordinates were confused in several equations. - The text had Y = sin(theta), where it should have been Y = -cos(theta). - There was nothing in the text to explain the rearrangment of atan2() arguments in order to get a continuous 0->2pi return value. I introduced the equivalence formula to get continuous return values. - get_sphere_uv() was in sphere.h for book 2, but hittable.h for book 3. This change also moves the function to sphere.h for book 3. - get_sphere_uv() moved to be a private static method on class sphere. - I am used to (and therefore prefer) phi corresponding to latitude, and theta corresponding to longitude, where the text and code flip these two angles. In a quick search of definitions, it looks like this is yet another case of competing conventions with both sides roughly equal in size. Thus, no clear winner, so I'm leaving this as is. Resolves #533
1 parent 7a396c0 commit 0ce5b90

File tree

5 files changed

+89
-40
lines changed

5 files changed

+89
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Change Log -- Ray Tracing in One Weekend
99
- Fix: `random_unit_vector()` was incorrect (#697)
1010
- Fix: Synchronize text and copies of `hittable.h`
1111
- Fix: Synchronize copies of `hittable_list.h`, `material.h`, `sphere.h`
12+
- Change: refactor `sphere::hit()` method to reuse common blocks of code.
13+
- Change: Improved the explanation and calculation of sphere UV coordinates (#533)
1214

1315
### In One Weekend
1416
- Change: Wrote brief explanation waving away negative t values in initial normal sphere

books/RayTracingTheNextWeek.html

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,52 +1148,81 @@
11481148
Texture Coordinates for Spheres
11491149
--------------------------------
11501150
<div class='together'>
1151-
For spheres, this is usually based on some form of longitude and latitude, _i.e._, spherical
1152-
coordinates. So if we have a $(\theta,\phi)$ in spherical coordinates, we just need to scale
1153-
$\theta$ and $\phi$ to fractions. If $\theta$ is the angle down from the pole, and $\phi$ is the
1154-
angle around the axis through the poles, the normalization to $[0,1]$ would be:
1151+
For spheres, texture coordinates are usually based on some form of longitude and latitude, _i.e._,
1152+
spherical coordinates. So we compute $(\theta,\phi)$ in spherical coordinates, where $\theta$ is the
1153+
angle up from the bottom pole (that is, up from -Y), and $\phi$ is the angle around the Y-axis (from
1154+
-X to +Z to +X to -Z back to -X).
1155+
1156+
We want to map $\theta$ and $\phi$ to texture coordinates $u$ and $v$ each in $[0,1]$, where
1157+
$(u=0,v=0)$ maps to the bottom-left corner of the texture. Thus the normalization from
1158+
$(\theta,\phi)$ to $(u,v)$ would be:
11551159

11561160
$$ u = \frac{\phi}{2\pi} $$
11571161
$$ v = \frac{\theta}{\pi} $$
11581162
</div>
11591163

11601164
<div class='together'>
1161-
To compute $\theta$ and $\phi$, for a given hitpoint, the formula for spherical coordinates of a
1162-
unit radius sphere on the origin is:
1165+
To compute $\theta$ and $\phi$ for a given point on the unit sphere centered at the origin, we start
1166+
with the equations for the corresponding Cartesian coordinates:
11631167

1164-
$$ x = \cos(\phi) \cos(\theta) $$
1165-
$$ y = \sin(\phi) \cos(\theta) $$
1166-
$$ z = \sin(\theta) $$
1168+
$$ \begin{align*}
1169+
x &= -\cos(\phi) \sin(\theta) \\
1170+
y &= -\cos(\theta) \\
1171+
z &= \sin(\phi) \sin(\theta)
1172+
\end{align*}
1173+
$$
11671174
</div>
11681175

11691176
<div class='together'>
1170-
We need to invert that. Because of the lovely `<cmath>` function `atan2()` which takes any number
1171-
proportional to sine and cosine and returns the angle, we can pass in $x$ and $y$ (the
1172-
$\cos(\theta)$ cancel):
1177+
We need to invert these equations to solve for $\theta$ and $\phi$. Because of the lovely `<cmath>`
1178+
function `atan2()`, which takes any pair of numbers proportional to sine and cosine and returns the
1179+
angle, we can pass in $x$ and $z$ (the $\sin(\theta)$ cancel) to solve for $\phi$:
11731180

1174-
$$ \phi = \text{atan2}(y, x) $$
1181+
$$ \phi = \text{atan2}(z, -x) $$
11751182
</div>
11761183

11771184
<div class='together'>
1178-
The $atan2$ returns values in the range $-\pi$ to $\pi$, so we need to take a little care there.
1179-
The $\theta$ is more straightforward:
1185+
`atan2()` returns values in the range $-\pi$ to $\pi$, but they go from 0 to $\pi$, then flip to
1186+
$-\pi$ and proceed back to zero. While this is mathematically correct, we want $u$ to range from $0$
1187+
to $1$, not from $0$ to $1/2$ and then from $-1/2$ to $0$. Fortunately,
1188+
1189+
$$ \text{atan2}(a,b) = \text{atan2}(-a,-b) + \pi, $$
1190+
1191+
and the second formulation yields values from $0$ continuously to $2\pi$. Thus, we can compute
1192+
$\phi$ as
1193+
1194+
$$ \phi = \text{atan2}(-z, x) + \pi $$
1195+
</div>
11801196

1181-
$$ \theta = \text{asin}(z) $$
1197+
<div>
1198+
The derivation for $\theta$ is more straightforward:
11821199

1183-
which returns numbers in the range $-\pi/2$ to $\pi/2$.
1200+
$$ \theta = \text{acos}(-y) $$
11841201
</div>
11851202

11861203
<div class='together'>
1187-
So for a sphere, the $(u,v)$ coord computation is accomplished by a utility function that expects
1188-
things on the unit sphere centered at the origin:
1204+
So for a sphere, the $(u,v)$ coord computation is accomplished by a utility function that takes
1205+
points on the unit sphere centered at the origin, and computes $u$ and $v$:
11891206

11901207
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
1191-
void get_sphere_uv(const vec3& p, double& u, double& v) {
1192-
auto phi = atan2(p.z(), p.x());
1193-
auto theta = asin(p.y());
1194-
u = 1-(phi + pi) / (2*pi);
1195-
v = (theta + pi/2) / pi;
1196-
}
1208+
class sphere : public hittable {
1209+
...
1210+
private:
1211+
static void get_sphere_uv(const point3& p, double& u, double& v) {
1212+
// p: a given point on the sphere of radius one, centered at the origin.
1213+
// u: returned value [0,1] of angle around the Y axis from X=-1.
1214+
// v: returned value [0,1] of angle from Y=-1 to Y=+1.
1215+
// <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50>
1216+
// <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00>
1217+
// <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
1218+
1219+
auto theta = acos(-p.y());
1220+
auto phi = atan2(-p.z(), p.x()) + pi;
1221+
1222+
u = phi / (2*pi);
1223+
v = theta / pi;
1224+
}
1225+
};
11971226
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11981227
[Listing [get-sphere-uv]: <kbd>[sphere.h]</kbd> get_sphere_uv function]
11991228
</div>

src/TheNextWeek/sphere.h

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ class sphere : public hittable {
3232
point3 center;
3333
double radius;
3434
shared_ptr<material> mat_ptr;
35+
36+
private:
37+
static void get_sphere_uv(const point3& p, double& u, double& v) {
38+
// p: a given point on the sphere of radius one, centered at the origin.
39+
// u: returned value [0,1] of angle around the Y axis from X=-1.
40+
// v: returned value [0,1] of angle from Y=-1 to Y=+1.
41+
// <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50>
42+
// <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00>
43+
// <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
44+
45+
auto theta = acos(-p.y());
46+
auto phi = atan2(-p.z(), p.x()) + pi;
47+
48+
u = phi / (2*pi);
49+
v = theta / pi;
50+
}
3551
};
3652

3753

@@ -43,14 +59,6 @@ bool sphere::bounding_box(double time0, double time1, aabb& output_box) const {
4359
}
4460

4561

46-
void get_sphere_uv(const point3& p, double& u, double& v) {
47-
auto phi = atan2(p.z(), p.x());
48-
auto theta = asin(p.y());
49-
u = 1-(phi + pi) / (2*pi);
50-
v = (theta + pi/2) / pi;
51-
}
52-
53-
5462
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
5563
vec3 oc = r.origin() - center;
5664
auto a = r.direction().length_squared();

src/TheRestOfYourLife/hittable.h

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@
1818

1919
class material;
2020

21-
void get_sphere_uv(const point3& p, double& u, double& v) {
22-
auto phi = atan2(p.z(), p.x());
23-
auto theta = asin(p.y());
24-
u = 1-(phi + pi) / (2*pi);
25-
v = (theta + pi/2) / pi;
26-
}
27-
2821

2922
struct hit_record {
3023
point3 p;

src/TheRestOfYourLife/sphere.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ class sphere : public hittable {
3535
point3 center;
3636
double radius;
3737
shared_ptr<material> mat_ptr;
38+
39+
private:
40+
static void get_sphere_uv(const point3& p, double& u, double& v) {
41+
// p: a given point on the sphere of radius one, centered at the origin.
42+
// u: returned value [0,1] of angle around the Y axis from X=-1.
43+
// v: returned value [0,1] of angle from Y=-1 to Y=+1.
44+
// <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50>
45+
// <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00>
46+
// <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
47+
48+
auto theta = acos(-p.y());
49+
auto phi = atan2(-p.z(), p.x()) + pi;
50+
51+
u = phi / (2*pi);
52+
v = theta / pi;
53+
}
3854
};
3955

4056
double sphere::pdf_value(const point3& o, const vec3& v) const {
@@ -64,6 +80,7 @@ bool sphere::bounding_box(double time0, double time1, aabb& output_box) const {
6480
return true;
6581
}
6682

83+
6784
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
6885
vec3 oc = r.origin() - center;
6986
auto a = r.direction().length_squared();

0 commit comments

Comments
 (0)