-
Notifications
You must be signed in to change notification settings - Fork 0
07 Sphere
Spheres are possibly the simplest 3D shape to model in raytracing, since they are the epitome of symmetry, and can be specified with just one vector (the centre) and one float (the radius/diameter).
For detailed and alternative approaches, check out the page on Wikipedia.
Any point on a sphere can be represented as:
|| P - C || = r
The magnitude of the vector from the centre of the sphere to the point (or the other way) will be equal to the radius of the sphere. If a ray intersects a sphere, there must be at least one point on both objects (possibly two), satisfying both equations. Substituting the ray's parametric form into the equation of the sphere gives us:
|| (O + tD) - C || = r
Where O
is the origin of the ray, D
its direction as a unit vector, and tD
the distance along the ray from O
(t
is the unknown here). Denoting O - C
as CO
:
|| td - CO || = r
(td - CO) . (td - CO) = r * r
t^2 - 2t(d.CO) + CO.CO - r * r = 0
This is a quadratic equation with a = 1
, b = 2(d.CO)
, c = CO.CO - r^2
. It will only have real roots if the discriminant (b^2 - 4ac
) is positive (since its square root needs to be taken). We can use this information to test if our work so far is correct, before solving for t
itself.
To solve for t
, we simply need to take the positive minima of the two roots obtained via (-b +- sqrt(discriminant)) / 2a
to obtain the smallest positive t
at which the ray collides with the sphere. Plugging this t
into the ray's equation gives us that point. Another quantity that's useful to compute at this point is the surface normal at the point of collision: in this case, quite simply a (unit) vector from the centre of the sphere to this point.
Add a new header:
sphere.hpp
struct Sphere {
fvec3 centre{};
float radius{};
};
Starting to code collisions is tricky, because it is opinionated: since Ray
needs to interact with Sphere
, where does the common code go?
- In
Ray
- In
Sphere
- In another file entirely, perhaps as free functions / operator structs
I choose another file, but feel free to use your own preferred approach instead. Starting with just collision detection:
hit.hpp
bool hit(Ray const& ray, Sphere const& sphere);
hit.cpp
bool hit(Ray const& ray, Sphere const& sphere) {
auto const co = ray.origin - sphere.centre;
float const b = 2.0f * dot(ray.direction.vec(), co);
float const c = dot(co, co) - sphere.radius * sphere.radius;
float const discriminant = b * b - 4 * c;
return discriminant > 0.0f;
}
Using it in main:
main.cpp
auto const sphere = Sphere{.centre = {0.0f, 0.0f, -5.0f}, .radius = 1.0f};
for (std::uint32_t row = 0; row < image.extent().y(); ++row) {
auto const yt = static_cast<float>(row) / static_cast<float>(image.extent().y() - 1);
for (std::uint32_t col = 0; col < image.extent().x(); ++col) {
auto const xt = static_cast<float>(col) / static_cast<float>(image.extent().x() - 1);
auto const dir = top_left + xt * horizontal - yt * vertical - origin;
auto const ray = Ray{origin, dir};
if (hit(ray, sphere)) {
image[{row, col}] = Rgb::from_hex(0xff00ff);
continue;
}
auto const t = 0.5f * (ray.direction.vec().y() + 1.0f);
image[{row, col}] = Rgb::from_f32(lerp(gradient[0], gradient[1], t));
}
}
Incorporating computing the collision point and normal:
hit.hpp
struct Hit {
fvec3 point{};
nvec3 normal{};
bool operator()(Ray const& ray, Sphere const& sphere);
};
hit.cpp
namespace {
constexpr float smallest_positive_root(std::span<float const, 2> roots) {
if (roots[0] < 0.0f) { return roots[1]; }
if (roots[1] < 0.0f) { return roots[0]; }
return std::min(roots[0], roots[1]);
}
}
bool Hit::operator()(Ray const& ray, Sphere const& sphere) {
auto const co = ray.origin - sphere.centre;
float const b = 2.0f * dot(ray.direction.vec(), co);
float const c = dot(co, co) - sphere.radius * sphere.radius;
float const discriminant = b * b - 4 * c;
if (discriminant < 0.0f) { return false; }
auto const sqd = std::sqrt(discriminant);
float const roots[] = {0.5f * (-b - sqd), 0.5f * (-b + sqd)};
auto const t = smallest_positive_root(roots);
if (t < 0.0f) { return false; }
point = ray.at(t);
normal = point - sphere.centre;
return true;
}
We can visualize this work by parameterizing the colour on the collision normal:
main.cpp
auto hit = Hit{};
if (hit(ray, sphere)) {
image[{row, col}] = Rgb::from_f32(0.5f * (hit.normal.vec() + 1.0f));
continue;
}
Note: Feel free to increase the image extent by 2x or use any other desired resolution.