How is the ray_color(...) (ray trace function) correct from the "Ray Tracing in One Weekend: Part 1" book?
The trace ray (named ray_color in the book) function from the ray tracing in a weekend book looks like this
color ray_color(const ray& r, int depth, const hittable& world) const {
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
hit_record rec;
if (world.hit(r, interval(0.001, infinity), rec)) {
ray scattered;
color attenuation;
if (rec.mat->scatter(r, rec, attenuation, scattered))
return attenuation * ray_color(scattered, depth-1, world);
return color(0,0,0);
}
vec3 unit_direction = unit_vector(r.direction());
auto a = 0.5*(unit_direction.y() + 1.0);
return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0);
}
which I suspect does not make sense for one specific reason.
Let depth=1
when we start shooting our initial ray through a pixel. Let's say we hit an object and thus we arrive to if(rec.mat->scatter(...))
. Let's say scatter(...)
returns true, thus we return attenuation * ray_color(scattered, depth - 1, world)
. Notice now that the recursive call will immediately return due to the guard if(depth <= 0)
with a retval of color(0,0,0)
. This will make it so the multiplication attenuation * ray_color(...)
will be 0
.
Isn't this incorrect as we did hit something once and shaded that point, yet we will be returning black. I tested this and sure enough all the objects become black. If I set depth = 2
I get this weird result. Notice how we have seemingly gotten one bounce (apparent from the fact that we can see specular reflections)... but the reflections from the bounce are all colored black.
I really can't wrap my head around how this is correct. I've Googled a bunch of people's implementations of the book and they all seem to do the exact same thing. How does this make sense?
EDIT: If I add a guard if (depth == 1) return attenuation; else return attenuation * ray_color(...);
I do get what you would if you have depth=1, and furthermore the reflections seem to be colored more correctly with depth=2, and depth=25. Although, the result still looks odd to me. Notice how I still end up with completely black reflections, this is likely due to multiplication with zero problem initially mentioned. Perhaps there's something else that is also wrong, likely with my own code. I might have to make a new post about that as it's likely a different problem than what this question pertains to.
Color RayTracer::TraceRay(const Ray& ray, int depth, float weight)
{
if (depth <= 0)
return Utils::Colors::BLACK;
HitPayload hp = FindClosestHit(ray);
if (hp.t < 0.0f) {
// We didn't hit anything, just return a sky color of some kind.
const float alpha = 0.5f * (ray.Dir().y + 1.0f);
const Color skyColor = (1.0f - alpha) * Utils::Colors::WHITE + alpha * Utils::Colors::SKY_COLOR;
return skyColor * weight;
}
const Material* mat = m_Scene->GetMaterial(hp.Shape->GetMaterialIdx());
Color colorOut{};
Ray rayOut{};
rayOut.SetOrigin(hp.Position + Utils::Constants::EPSILON * hp.Normal);
// TODO: Cast shadow ray(s)
if (mat->Shade(ray, rayOut, colorOut, hp))
if (depth == 1)
return colorOut;
else
return (weight * colorOut) * TraceRay(rayOut, depth - 1, 0.75f * weight);
// Material is a black body
return Utils::Colors::BLACK;
}
Answers
Your observation about the ray tracing function from the book is correct. The issue arises from the fact that when the depth of recursion is 1, the function doesn't consider the contribution of the bounced ray's color to the final result, leading to incorrect rendering, especially in the case of reflections.
Your addition of a guard to handle the case when depth is 1 is a step in the right direction. This ensures that when the recursion depth is 1, the function returns the color contribution from the bounced ray without further recursion.
However, there's another issue in your implementation. When you multiply colorOut
with weight
in the recursive call, you're effectively reducing the contribution of the bounced ray's color in each successive bounce. This can lead to overly dark reflections, especially for materials with high specular components.
To address this issue, you should maintain the energy conservation principle by keeping the total energy of the reflected rays consistent with the incoming ray. Instead of multiplying colorOut
by weight
before the recursive call, you should multiply it after the recursive call. This way, each successive bounce contributes its full color to the final result.
Here's how you can modify your TraceRay
function to fix these issues:
Color RayTracer::TraceRay(const Ray& ray, int depth, float weight)
{
if (depth <= 0)
return Utils::Colors::BLACK;
HitPayload hp = FindClosestHit(ray);
if (hp.t < 0.0f) {
// We didn't hit anything, just return a sky color of some kind.
const float alpha = 0.5f * (ray.Dir().y + 1.0f);
const Color skyColor = (1.0f - alpha) * Utils::Colors::WHITE + alpha * Utils::Colors::SKY_COLOR;
return skyColor * weight;
}
const Material* mat = m_Scene->GetMaterial(hp.Shape->GetMaterialIdx());
Color colorOut{};
Ray rayOut{};
rayOut.SetOrigin(hp.Position + Utils::Constants::EPSILON * hp.Normal);
// TODO: Cast shadow ray(s)
if (mat->Shade(ray, rayOut, colorOut, hp))
if (depth == 1)
return weight * colorOut; // Apply weight here
else
return TraceRay(rayOut, depth - 1, weight) * colorOut; // Apply weight after the recursive call
// Material is a black body
return Utils::Colors::BLACK;
}
With this modification, each bounced ray contributes its full color to the final result, ensuring more accurate rendering, especially for reflections and other specular effects.