For the past few weeks, I've been working through this (totally awesome!) book with no major problems.
But now that I arrived at chapter 13, there is something about the cone implementation that I can't solve: I seem to get two end caps, one with correct size but wrong lighting, one with correct lighting but constant size (radius 1 according to the stripe pattern), see attached images. At first I thought that the end cap was too small, but with the stripe pattern applied you can see that there is a small disc (which never changes its size, even when scaling the cone) on top of another "cap".
In these images, both objects are unscaled, truncated at y=0 and y=2 and only translated along the x axis. The stripe pattern is scaled to 0.5. In the first image, both objects are open, in the second one both are closed.
I based the cone implementation on my (correctly working) cylinder implementation and checked all formulas at least a couple of times. All tests for both implementations are passing.
Any ideas what might be the reason for this effect?
I experienced this and the cause was two-fold: my tracer was not correctly initializing vectors and points (remember the 4th value of a point is 1.0 and the 4th value of a vector is 0.0), and my tracer was not correctly sorting intersections (the comparator used the round() function to get integers from doubles instead of just using the <, > operators directly on the doubles).
Thanks! I'm sure that my vector/point implementation is fine, but I'll check the intersections again.
It's odd that EVERYTHING else works perfectly (all the tests and even quite complex scenes I built with spheres, planes, cubes, cylinders and lots of patterns, reflections and transparency), but the cone caps don't.
Guess I will step through a simple cone scene with a debugger.
After checking things with a debugger, I believe I found the problem in my local_normal_at implementation for cones.
The book says, "Lastly, for the normal vector, compute the end cap normals just as you did for the cylinder, but change the rest to the following, given in pseudocode: y <-- [...]", so I simply took the cylinder implementation and added the calculation for y.
However, this leads to something like:
if dist < 1 and local_point.y >= self.maximum - EPSILON: # top cap return Vector(0, 1, 0) Since I only check if dist is less than 1, I get the strange additional cap with constant radius 1.
If I replace this with
if dist < self.maximum ** 2 [...] everything looks fine (likewise for the minimum cap).
Can anybody confirm that this is the right way to do it? If yes, I believe the book is a little bit unclear at this point, since "just as you did for the cylinder" didn't make it clear to me that this change is necessary.
accolon -- just a guess, but I wonder if you're not sizing your cylinder end-cap appropriately? You said you're basing it on the cylinder, but the cylinder cap is always a constant radius, regardless of how high the cylinder is, whereas with a cylinder, the cap's radius needs to be the same as the y value at which the cap is placed. That is to say, for a cylinder truncated at y=4, the cap radius will be 4 as well.
def check_cap(ray, t, y): x = ray.origin.x + t * ray.direction.x z = ray.origin.z + t * ray.direction.z return (x ** 2 + z ** 2) <= y ** 2
I added the y parameter and the comparison to y^2 instead of 1 according to the description in the book, intersect_caps is calling this with self.minimum and self.maximum for y.
I've attached an image with the single change I described above (comparing dist < self.maximum ** 2 in local_normal_at) which looks right to me -- at least it looks much better than the "double cap" image. :-)