The Ray Tracer Challenge: chapter 7 final image is flipped (camera issues) (pp. 105-107)

@jamis

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.