Skip to content

Commit 20f2ae1

Browse files
authored
Merge pull request #775 from RayTracing/sphere-uv-2
Corrected sphere UV coordinates
2 parents dbb27f2 + 276d241 commit 20f2ae1

File tree

5 files changed

+88
-40
lines changed

5 files changed

+88
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Change Log -- Ray Tracing in One Weekend
1010
- Fix: Synchronize text and copies of `hittable.h`
1111
- Fix: Synchronize copies of `hittable_list.h`, `material.h`, `sphere.h`
1212
- Change: refactor `sphere::hit()` method to reuse common blocks of code.
13+
- Change: Improved the explanation and calculation of sphere UV coordinates (#533)
1314

1415
### In One Weekend
1516
- 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
@@ -1137,52 +1137,81 @@
11371137
Texture Coordinates for Spheres
11381138
--------------------------------
11391139
<div class='together'>
1140-
For spheres, this is usually based on some form of longitude and latitude, _i.e._, spherical
1141-
coordinates. So if we have a $(\theta,\phi)$ in spherical coordinates, we just need to scale
1142-
$\theta$ and $\phi$ to fractions. If $\theta$ is the angle down from the pole, and $\phi$ is the
1143-
angle around the axis through the poles, the normalization to $[0,1]$ would be:
1140+
For spheres, texture coordinates are usually based on some form of longitude and latitude, _i.e._,
1141+
spherical coordinates. So we compute $(\theta,\phi)$ in spherical coordinates, where $\theta$ is the
1142+
angle up from the bottom pole (that is, up from -Y), and $\phi$ is the angle around the Y-axis (from
1143+
-X to +Z to +X to -Z back to -X).
1144+
1145+
We want to map $\theta$ and $\phi$ to texture coordinates $u$ and $v$ each in $[0,1]$, where
1146+
$(u=0,v=0)$ maps to the bottom-left corner of the texture. Thus the normalization from
1147+
$(\theta,\phi)$ to $(u,v)$ would be:
11441148

11451149
$$ u = \frac{\phi}{2\pi} $$
11461150
$$ v = \frac{\theta}{\pi} $$
11471151
</div>
11481152

11491153
<div class='together'>
1150-
To compute $\theta$ and $\phi$, for a given hitpoint, the formula for spherical coordinates of a
1151-
unit radius sphere on the origin is:
1154+
To compute $\theta$ and $\phi$ for a given point on the unit sphere centered at the origin, we start
1155+
with the equations for the corresponding Cartesian coordinates:
11521156

1153-
$$ x = \cos(\phi) \cos(\theta) $$
1154-
$$ y = \sin(\phi) \cos(\theta) $$
1155-
$$ z = \sin(\theta) $$
1157+
$$ \begin{align*}
1158+
y &= -\cos(\theta) \\
1159+
x &= -\cos(\phi) \sin(\theta) \\
1160+
z &= \quad\sin(\phi) \sin(\theta)
1161+
\end{align*}
1162+
$$
11561163
</div>
11571164

11581165
<div class='together'>
1159-
We need to invert that. Because of the lovely `<cmath>` function `atan2()` which takes any number
1160-
proportional to sine and cosine and returns the angle, we can pass in $x$ and $y$ (the
1161-
$\cos(\theta)$ cancel):
1166+
We need to invert these equations to solve for $\theta$ and $\phi$. Because of the lovely `<cmath>`
1167+
function `atan2()`, which takes any pair of numbers proportional to sine and cosine and returns the
1168+
angle, we can pass in $x$ and $z$ (the $\sin(\theta)$ cancel) to solve for $\phi$:
11621169

1163-
$$ \phi = \text{atan2}(y, x) $$
1170+
$$ \phi = \text{atan2}(z, -x) $$
11641171
</div>
11651172

11661173
<div class='together'>
1167-
The $atan2$ returns values in the range $-\pi$ to $\pi$, so we need to take a little care there.
1168-
The $\theta$ is more straightforward:
1174+
`atan2()` returns values in the range $-\pi$ to $\pi$, but they go from 0 to $\pi$, then flip to
1175+
$-\pi$ and proceed back to zero. While this is mathematically correct, we want $u$ to range from $0$
1176+
to $1$, not from $0$ to $1/2$ and then from $-1/2$ to $0$. Fortunately,
1177+
1178+
$$ \text{atan2}(a,b) = \text{atan2}(-a,-b) + \pi, $$
1179+
1180+
and the second formulation yields values from $0$ continuously to $2\pi$. Thus, we can compute
1181+
$\phi$ as
1182+
1183+
$$ \phi = \text{atan2}(-z, x) + \pi $$
1184+
</div>
11691185

1170-
$$ \theta = \text{asin}(z) $$
1186+
<div>
1187+
The derivation for $\theta$ is more straightforward:
11711188

1172-
which returns numbers in the range $-\pi/2$ to $\pi/2$.
1189+
$$ \theta = \text{acos}(-y) $$
11731190
</div>
11741191

11751192
<div class='together'>
1176-
So for a sphere, the $(u,v)$ coord computation is accomplished by a utility function that expects
1177-
things on the unit sphere centered at the origin:
1193+
So for a sphere, the $(u,v)$ coord computation is accomplished by a utility function that takes
1194+
points on the unit sphere centered at the origin, and computes $u$ and $v$:
11781195

11791196
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
1180-
void get_sphere_uv(const vec3& p, double& u, double& v) {
1181-
auto phi = atan2(p.z(), p.x());
1182-
auto theta = asin(p.y());
1183-
u = 1-(phi + pi) / (2*pi);
1184-
v = (theta + pi/2) / pi;
1185-
}
1197+
class sphere : public hittable {
1198+
...
1199+
private:
1200+
static void get_sphere_uv(const point3& p, double& u, double& v) {
1201+
// p: a given point on the sphere of radius one, centered at the origin.
1202+
// u: returned value [0,1] of angle around the Y axis from X=-1.
1203+
// v: returned value [0,1] of angle from Y=-1 to Y=+1.
1204+
// <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50>
1205+
// <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00>
1206+
// <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
1207+
1208+
auto theta = acos(-p.y());
1209+
auto phi = atan2(-p.z(), p.x()) + pi;
1210+
1211+
u = phi / (2*pi);
1212+
v = theta / pi;
1213+
}
1214+
};
11861215
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11871216
[Listing [get-sphere-uv]: <kbd>[sphere.h]</kbd> get_sphere_uv function]
11881217
</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)