Ray-tracing (2)—Generating Camera Rays

Ray-tracing (2)—Generating Camera Rays

ps: Thumbnail 《月色真美》

定义光线

如上图要定义一根光线,我们只需要用一个点和一根向量就可以了,即光线发射的原点以及光线的方向(注意方向要normalize)。同时我们可以给出光线的数学定义: $$P = orig + t * dir.$$

光线的作用

在定义清楚这根光线之后不妨来看看可以使用它来干嘛,从Ray-tracing (1)—Overview中可以知道,从原点直接发射出来的光线叫做camera rays (or primary rays),当该光线打到了物体之后,从交点位置可以产生secondary rays。

camera rays: 它可以用来解决visibility的问题,camera rays得到的交点就是我们将在屏幕对应像素上看到的东西,我们会计算出交点的颜色之后返还给对应像素。

secondary rays: 主要用来计算shading。我们可以把secondary rays分为不同的类型,每种类型对应的方向计算方式是不同的。比如对于shadow类型的secondary rays,如上图所示它的方向就是交点指向光源的方向;对于reflection类型它的方向就是反射的方向;对于refraction类型,我们可以使用snell’s law来计算它的方向。

光线的生成

下面会涉及一些数学推导,在此之前有下面这些点是我们需要注意的:

  • 如上图所示摄像机原点在世界坐标(0,0,0)位置,image plane在距离原点1个单位远的地方,摄像机方向指向-z轴的地方。
  • 对于光线追踪我们这里使用的是pinhole camera模型,也就是说我们的摄像机其实是一个针孔摄像机,我们可以把原点看做pinhole camera的aperture;真实世界的射线机由上图可以知道(其实根据中学的小孔成像原理也知道),aperture在pinhole camera的前方,但是这样成像到 plane的时候就会得到一个倒像,而在这里我们认为aperture在后方,这样成像得到的就是一个正像。
  • 不同的图形学的API定义的坐标系可能会有不同,下面计算使用的这一套坐标系,可能和其它图形学的API略有不同,但是这不是特别的关键,我们只用知道这种具体的计算流程是怎样的即可,在其它图形学API中生成光线的时候,只用根据具体的坐标系灵活变通即可。
  • 数学推导

    把摄像机和像素中心连线,这样就生成了一条光线,但是像素是在raster space(就是坐标是像素的空间)下的(2维空间),但是物体,光源,摄像机等等都是在3维空间中的(这里的3维空间我们考虑的是世界坐标系),直接把在两个不同坐标系的点连接起来显然是错的并且其它的物体都在世界空间,所以我们需要把像素中心的坐标从raster space转换到世界坐标系下。

    raster space NDC space
    第一步我们首先要把像素中心从raster space转换到NDC space。 \begin{array}{l} PixelNDC_x = \dfrac{(Pixel_x + 0.5)}{ImageWidth},\\ PixelNDC_y = \dfrac{(Pixel_y + 0.5)}{ImageHeight}. \end{array}

    +0.5是为了得到像素中心的坐标。这里的NDC space就是x,y的坐标范围都在[0 , 1]之间的坐标系。

    NDC space screen space
    接着我们需要把该点坐标从NDC space转换到screen space,即坐标范围在[-1,1]之间的坐标系。 \begin{array}{l} PixelScreen_x = 2 * {PixelNDC_x} - 1,\\ PixelScreen_y = 2 * {PixelNDC_y} - 1. \end{array}
    如右图所示的情况,根据上面的公式NDC space下的坐标(0.75,0.42)转换到scree space是(0.5,-0.16)而不是(0.5,0.16),因此我们要重新计算y轴坐标: \begin{array}{l} PixelScreen_y = 1 - 2 * {PixelNDC_y}. \end{array} 之所以造成这样的问题,是跟像素坐标的定义有关,不同的API定义像素的坐标并不相同,比如OpenGL定义y轴向上为正,x向右为正;而DirectX定义y轴向下为正,x向右为正,因此在进行坐标转换的时候这些细节需要注意。

    screen space correct screen space
    以上的计算中我们假设了照片是正方形的,但实际上很多情况下是长方形的照片。如上图所示原来长为7像素,宽为5像素的照片被压缩到[-1,1]之后每个像素不再是正方形了,相当于我们把原来的照片给挤扁了,因此我们需要把它放缩回去: \begin{array}{l} ImageAspectRatio = \dfrac{ImageWidth}{ImageHeight},\\ PixelCamera_x = (2 * {PixelNDC_x} - 1) * {ImageAspectRatio},\\ PixelCamera_y = (1 - 2 * {PixelNDC_y}). \end{array}

    修正之后y的坐标还是和之前的screen space坐标一样,但是x的坐标变到了[ -ImageAspectRatio , ImageAspectRatio ]之间。


    correct screen space camera space

    我们知道相机的Fov可以控制摄像机能够拍到的场景的范围,因此我们可以乘\(tan(\dfrac{\alpha}{2})\)来控制显示范围,因此再完善一下公式:

    \begin{array}{l} PixelCamera_x = (2 * {PixelNDC_x } - 1) * ImageAspectRatio * tan(\dfrac{\alpha}{2}),\\ PixelCamera_y = (1 - 2 * {PixelNDC_y }) * tan(\dfrac{\alpha}{2}). \end{array}
    到现在我们已经把最开始的像素坐标转换到了camera space下。如上图所示,由于我们的屏幕隔摄像机1个单位(对应图中的AB边),再加上之前已经把y轴缩放到了[-1,1]之间(对应于图中的BC边),x为了追求原始比例缩放到了[-ImageAspectRatio , ImageAspectRatio]之间(侧视图没有画出来),因此根据AB和BC的比例可以知道目前image plane的比例刚好对应到了3维场景中的大小(这估计也是为什么屏幕要放在离摄像机1个单位远的地方吧,否则还要乘上一些奇怪的系数才能保证比例。)
  • 如果此时摄像机在默认位置,那么camera space是自动对齐于世界坐标的。

  • 得到光线的方向坐标
    经过上面的一番推导,现在我们已经知道了像素中心在世界坐标下的位置了,我们只用把像素中心的坐标减去摄像机的坐标就可以得到光线的方向 了: \begin{array}{l} P_{cameraSpace} = (PixelCamera_x, PixelCamera_y, -1) \end{array} 注意方向还要归一化!
    假如不想在原点拍摄场景,即移动了相机的默认位置,那么这个时候我们要想转换到世界坐标,还要多做一步即要从camera space转换到world space我们要乘一个camera-to-world matrix矩阵。

    伪代码

    相机默认在原点拍摄的情况

    相机在原点
    1
    2
    3
    4
    5
    6
    float imageAspectRatio = imageWidth / (float)imageHeight; // assuming width > height 
    float Px = (2 * ((x + 0.5) / imageWidth) - 1) * tan(fov / 2 * M_PI / 180) * imageAspectRatio;
    float Py = (1 - 2 * ((y + 0.5) / imageHeight) * tan(fov / 2 * M_PI / 180);
    Vec3f rayOrigin(0);
    Vec3f rayDirection = Vec3f(Px, Py, -1) - rayOrigin; // note that this just equal to Vec3f(Px, Py, -1);
    rayDirection = normalize(rayDirection); // it's a direction so don't forget to normalize

    相机不在原点

    相机不在原点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    float imageAspectRatio = imageWidth / imageHeight; // assuming width > height 
    float Px = (2 * ((x + 0.5) / imageWidth) - 1) * tan(fov / 2 * M_PI / 180) * imageAspectRatio;
    float Py = (1 - 2 * ((y + 0.5) / imageHeight) * tan(fov / 2 * M_PI / 180);
    Vec3f rayOrigin = Point3(0, 0, 0);
    Matrix44f cameraToWorld;
    cameraToWorld.set(...); // set matrix
    Vec3f rayOriginWorld, rayPWorld;
    cameraToWorld.multVectMatrix(rayOrigin, rayOriginWorld);
    cameraToWorld.multVectMatrix(Vec3f(Px, Py, -1), rayPWorld);
    Vec3f rayDirection = rayPWorld - rayOriginWorld;
    rayDirection.normalize(); // it's a direction so don't forget to normalize

    评论