I am following a book "Ray tracer challenge" by Buck, but it doesn't explain anything. The code works, but I don't properly understand, why.
The code is as follows:
struct camera {
unsigned hsize; // in px;
unsigned vsize; // in px;
float fov;
float pixel_size;
float half_width;
float half_height;
tform4 tform;
};
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));
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);
// canvas is at z=-1
tform4 inv_cam_tform = inverse(cam.tform);
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};
}
I left the comments as in pseudocode which I took this from.
So, I was reading about cameras etc. from Lengyel "Rendering", and I cannot match how it's usually done on the GPU with what I'm doing above. Or generally, I cannot match it against anything I've read elsewhere (learnopengl, scratchapixel, songho). He provides this picture:
The problem is understanding how camera transformation matrix and ray generation functions work.
I use left hand coordinate system. So, X grows to the right, Y grows up, Z grows into the screen. Do I understand correctly, based on the code, that in my camera space X grows to the left, Y grows up, Z grows outside of the screen? In other words, seems like the camera is looking at the opposite side of where the scene is, towards -Z.
Now, I generate a new basis in view()
, okay, I understand that. However, this demonstrates filling the transformation matrix by columns, while I'm doing it by rows. Why am I doing it this way? I am using column major matrices. When I need to fill in a transformation matrix, I do it column-wise, not row-wise. Okay, I thought, maybe it is already inverted, because inverse of orthogonal matrix is its transpose. But how does inverting it make any sense? I multiply it by another transformation matrix of translation, which is not inverted. And I also later invert the result of view()
(inv_cam_tform
), so... what is going on here?
Another thing. The forward
vector is negated. In the source above, it is not. Okay, I guess it is because camera is looking towards -Z. But I do not understand it, clear as day.
Ray generation. Okay, I need to adjust the coordinate for the center of the pixel, hence the +0.5. But what is x/yoffset
, ultimately? Coordinates of a concrete point on the canvas (or I guess projection plane) in world space? Why is there no Z coordinate? Did we make a prespective divide at z=-1 or something, so this is already device space? Then where was the clip space? Why is there no projection matrix?
Another picture of camera for reference (also from Lengyel):
And then what is the meaning of world_x/y
? What is half_width/height
? On the picture, half width would be s
, and half height would be 1
, correct? Except that the picture is only for 16:9, while this code can handle any aspect ratio based on width and height. Seems like these are the coordinates of pixel in world space, so a ray can be generated from origin towards these coordinates, to find any intersections with the scene. But why is the origin of the ray at 0? Shouldn't it be at the origin of the camera? Or does the translation transformation in view()
account for that?
I do not have any solid understanding of what is happening. Every resource, be it a blog like learnopengl or scratchapixel, a book like Lengyel's, explain everything differently and never generally (although Lengyel is probably the best so far), just in coherence with their line of thought and conventions. Can you make any sense of all this mess in my head? Sorry for incoherent explanation.