The Ray Tracer Challenge: cannot pass scaling test (page 69-70)

The test is as follows:

Scenario: Intersecting a scaled sphere with a ray
Given r ← ray(point(0, 0, -5), vector(0, 0, 1))
And s ← sphere()
When set_transform(s, scaling(2, 2, 2))
And xs ← intersect(s, r)
Then xs.count = 2
And xs[0].t = 3
And xs[1].t = 7

However, I get 1.5 and 3.5 respectively.

Looks like I have some misunderstanding about how to do the calculations, because both on paper and in the code I arrive at the same conclusion. So, I will write out my intermediate calculations.

We start by creating a scaling matrix, which is the transformation attached to the sphere:

2 0 0 0
0 2 0 0
0 0 2 0
0 0 0 1

Then we invert it (since we need to transform the ray by the inverse of sphere’s transformation):

0.5   0   0  0
0   0.5   0  0     = F
0     0 0.5  0
0     0   0  1

Then we multiply original ray’s point component with it and get:

    0       0
F * 0  =    0
   -5    -2.5

Then we multiply original ray’s vector component with it and get:

    0       0
F * 0  =    0
    1     0.5

So, our new Ray is { Point(0,0,-2.5), Vector(0,0,0.5) }, which is a point that lies on Z axis, looking towards the sphere.

To demonstrate that the algorithm for finding intersections between ray and sphere is correct (without writing it out here), we calculate it backwards: we know that the sphere is radius 1, centered at (0,0,0). My intersections are: 1.5, 3.5, which are distances from ray origin (-2.5) to points on the sphere. -2.5 + 1.5 = -1 and -2.5 + 3.5 = 1, 1 - (-1) = 2, which is the sphere diameter, so it makes sense? Or does it?

So, my more experienced friend looked at it and he found the issue: I didn’t convert the t values back into real world space. That means, after 1.5 and 3.5 are calculated, additionally the following must be done:
0. First, to clarify what these two values are. They are the two t scalars that fit into the line equation: point = origin + direction * t, and by themselves don’t mean much. But after you use the line equation (position() in the book), you will get two points, which are the coordinates of the intersections between ray and the object.

  1. Using position(), find the coordinates of intersections between transformed ray (ray2 in the book) and the sphere in the sphere space. This will yield two points, 0 0 -1 and 0 0 1. Remember, these points are still only meaningful in sphere space!
  2. Convert these points from sphere space back to real world space, by multiplying each one with the scaling(2 2 2). So, inverse(inverse(scaling(2 2 2))) (as the book says, to convert between spaces, use inverse). You will get 0 0 -2 and 0 0 2. These points now have meaning in the real world space: scaling a sphere by 2 2 2 means that we just make the sphere twice as big, so you might see how the intersections are at these two points, since we started with a unit sphere.
  3. Use the line equation above, but given point solve for t. Your origin and direction are from the original ray, so, 0 0 -5 and 0 0 1. The solutions are… drum roll3 and 7!

Maybe this should be obvious, or it is implied in the book, but it wasn’t obvious for me. Hopefully, this explanation helps somebody else.

Hey, I’m glad you found a solution! However, I feel like there may be a deeper issue here. The approach described in the book (transforming the ray by the inverse of the sphere’s transformation matrix, and then running the ray/sphere intersection as described) really should have “just worked”. The fact that you got t values that were half what was expected suggests that at some point maybe you were using the original ray origin/direction somewhere, or perhaps using the wrong transformation matrix?

Ultimately, as long as you’re getting the right answer now, it’s all good. I just worry that you may be doing more work than is necessary to obtain those t values, which may come back to bite you later in the book when render times grow longer and longer and every computation counts.

I’d be happy to take a look at your code if its available somewhere, if that would be helpful.

Helo Jamis, thank you for the reply and for the idea for the solution. The problem was with the implementation of quadratic equation solver, in particular, I was normalizing the direction vector of the transformed ray after converting it into object space. Lessons learned: don’t try to be too smart and go to wikipedia to implement line-sphere intersection according to their formula, just implement pseudocode from the book.

1 Like

@jamis

Somewhat of a necrobump, but I finally understood what is happening. So, if the object space ray is normalized, then a simpler formula to find the roots of the quadratic equation can be used (e.g. no need to divide by 2*a, since a is 1). However, this has a disadvantage: the resulting roots (the t parameter of the line equation) can only be used to find the point on the object space ray. After that, the point needs to be transformed back into world space. Then, if t parameter in the world space is required, another calculation of t parameter is necessary via the line equation, in world space. So, it ends up being the same, if not more, computationally costly, as solving the quadratic equation without object space ray normalization. If its direction is not normalized, then, surprisingly or not, the resulting t parameter can be plugged directly into line equation in the world space, without the requirement to convert it from object space first.