Unfortunately, my final image of chapter 7 is flipped:
All the tests from the chapter pass.
My camera and ray generation are implemented as:
camera::camera(unsigned hsize, unsigned vsize, float fov) noexcept
: hsize{hsize}, vsize{vsize}, fov{fov}, tform{} {
float half_view = std::tan(fov / 2.f);
float aspect = float(hsize) / float(vsize);
if (aspect >= 1.f) {
half_width = half_view;
half_height = half_view / aspect;
} else {
half_width = half_view * aspect;
half_height = half_view;
}
pixel_size = (half_width * 2.f) / hsize;
}
tform4 view(pnt3 const& from, pnt3 const& to, vec3 const& up) noexcept {
vec3 const forward = normalize(to - from);
vec3 const left = normalize(cross(forward, normalize(up)));
vec3 const true_up = normalize(cross(left, forward));
// vec3 const true_up = -normalize(cross(left, forward));
tform4 orientation{left.x, left.y, left.z, 0, //
true_up.x, true_up.y, true_up.z, 0, //
-forward.x, -forward.y, -forward.z, 0};
return orientation * tform4::translate({-from.x, -from.y, -from.z});
}
ray ray_for_pixel(camera const& cam, float px, float py) noexcept {
// offset from edge of canvas to pixel center
float const xoffset = (px + 0.5f) * cam.pixel_size;
float const yoffset = (py + 0.5f) * cam.pixel_size;
// cam looks towards -z, x is to the left
float const world_x = cam.half_width - xoffset;
float const world_y = cam.half_height - yoffset;
assert(-1.f <= world_x && world_x <= 1.f);
assert(-1.f <= world_y && world_y <= 1.f);
tform4 const inv_cam_tform = inverse(cam.tform);
// canvas is at z=-1
pnt3 const pixel = inv_cam_tform * pnt3{world_x, world_y, -1};
pnt3 const origin = inv_cam_tform * pnt3{0, 0, 0};
vec3 const direction = normalize(pixel - origin);
return ray{origin, direction};
}
If I negate the true_up
vector, as commented out, then the image is correct:
Obviously, if I negate it, some tests fail. This is a workaround, or one bug undoing the problem caused by another bug.
If I understand correctly, forward
designates Z axis, which grows into the screen (assuming left hand coordinate system, X growing right and Y growing up). Then left
vector designates X, since it is the result of the left hand coordinate cross product of Z and Y. Then true_up
designates X and Z cross product. Flipping the sign on true_up
cross is the same as making the cross of Z and X, which would point the Y axis downwards. But this is obviously wrong.
My render()
is:
canvas render(camera const& cam, world const& w) noexcept {
canvas image{cam.hsize, cam.vsize};
image.fill({0, 0, 0});
for (int y = cam.vsize - 1; y >= 0; --y) {
for (unsigned x = 0; x < cam.hsize - 1; ++x) {
image(x, y) = color_at(w, ray_for_pixel(cam, x, y));
}
}
return image;
}
My canvas’s (x,y)
addressing differs from the book. In the book, the canvas’s X grows right, Y grows down, like in 2D array. Since this is difficult to reason about, my canvas’s abstraction is that X grows right, and Y grows up, same as the left hand coordinate system. But it is also implemented as 2D array (addressed as 1D array), where Y grows down, so I transform the coordinates inside the canvas implementation:
canvas::canvas(unsigned int w, unsigned int h) noexcept
: canvas_{std::make_unique<color[]>(w * h)}, w_{w}, h_{h} {}
color& canvas::operator()(unsigned int x, unsigned int y) noexcept {
// Internally, X grows right, but Y grows down, hence the conversion
return canvas_[(h_ - y - 1) * w_ + x];
}
This could be the reason, but I don’t think so.
Any ideas what could be going wrong? The full code is here.