Perspective Camera Model
mat4 perspectiveTransform(float fov, float near, float far)
{
float recip = 1.0f / (far - near);
float ctot = 1.0 / tan(radians(fov) * 0.5);
mat4 persp(
ctot, 0.f, 0.f, 0.f,
0.f, ctot, 0.f, 0.f,
0.f, 0.f, far * recip, -far * near / recip,
0.f, 0.f, 1.f, 0.f
);
return persp;
}
perspectiveTransform
将camera space下,在fov(filed of view)中、在near plane和farplane间的点映射到screen space。这里的screen space还没有考虑到width和height不等的因素。例如,如果我们随便拿一个符合上述条件的camera space点,比如(near / ctot, invAspect * near / ctot, near)
, 你会发现其实他被transform到了(1,invAspect,0)
。所以这个screen space是定义在(-1, 1) x (-invAspect, invAspect) x (0, 1)
的坐标范围上的。
float invAspect = height / width;
mat4 persp = perspectiveTransform(fov, near, far);
// homoTransformedPoint = (near, invAspect * near, 0, near);
vec4 homoTransformedPoint = persp * vec4(tanFovAng * near, tanFovAng * near * invAspect, near, 1.f);
// transformedPoint = (1, invAspect, 0)
vec3 transformedPoint = homoTransformedPoint / homoTransformedPoint.w;
而根据pbrt的描述:
// pbrt
// Raster space: This is almost the same as NDC space, except the x and y coordinates range from (0,0) to (w,h).
ScreenToRaster = Scale(film->fullResolution.x,
film->fullResolution.y, 1) *
Scale(1 / (screenWindow.pMax.x - screenWindow.pMin.x),
1 / (screenWindow.pMin.y - screenWindow.pMax.y), 1) *
Translate(Vector3f(-screenWindow.pMin.x, -screenWindow.pMax.y, 0));
raster space定义在(0, w) x (0, h)
上。所以我们先将这个空间平移vec3(1, invAspect, 0)
变换到(0, 2) x (-2 * invAspect, 0) x (0, 1)
。然后再将这个空间进行缩放vec3(0.5f, -0.5f * aspect, 1.f)
变换到(0, 1) x (0, 1) x (0, 1)
,最后再缩放到(w, h)
。
这里一定要注意x和y的变换不太一样,这是因为pbrt使用的是左手系,camera space中,y轴朝上,z轴朝前,x轴朝右。而图像其实左上角为(0,0),因此y需要反一下。但是比较坑爹的是vision那边的人惯用右手系,y轴朝下,z轴朝前,x轴朝右。对于vision的这种情况,则不需要逆转y。
还有一个坑爹的点是用lookat计算worldToCamera。默认实现是这样的:
template <class T>
inline matrix4<T> look_at(const vector3<T>& eye, const vector3<T>& center, const vector3<T>& up)
{
matrix4<T> M;
vector3<T> x, y, z;
// make rotation matrix
// Z vector
z.x = eye.x - center.x;
z.y = eye.y - center.y;
z.z = eye.z - center.z;
z.normalize();
// Y vector
y.x = up.x;
y.y = up.y;
y.z = up.z;
// X vector = Y cross Z
x = cross(y, z);
// Recompute Y = Z cross X
y = cross(z, x);
// cross product gives area of parallelogram, which is < 1.0 for
// non-perpendicular unit-length vectors; so normalize x, y here
x.normalize();
y.normalize();
M.a00 = x.x;
M.a01 = x.y;
M.a02 = x.z;
M.a03 = -x.x * eye.x - x.y * eye.y - x.z * eye.z;
M.a10 = y.x;
M.a11 = y.y;
M.a12 = y.z;
M.a13 = -y.x * eye.x - y.y * eye.y - y.z * eye.z;
M.a20 = z.x;
M.a21 = z.y;
M.a22 = z.z;
M.a23 = -z.x * eye.x - z.y * eye.y - z.z * eye.z;
M.a30 = T(0);
M.a31 = T(0);
M.a32 = T(0);
M.a33 = T(1);
return M;
}
可以看到这里的z轴是从相机看向的点指向相机的位置,而这和我们讨论的相机坐标系是相悖的。所以这里有个坑。
我的解决办法是在z轴上scale -1,如下所示:
mat4 getView() { return nvmath::scale_mat4(vec3(1.f,1.f,-1.f)) * CameraManip.getMatrix(); }