graphics, rendering
Home

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(); }