生成相机光线:栅格空间-NDC-屏幕空间-世界

生成相机光线

本文主要参考 文章 Ray-Tracing: Generating Camera Rays

首先,渲染器render)的目的是为帧的每个像素分配颜色。 我们也知道,视野(field of view)等参数会改变我们看到的场景的多少。 我们还知道,光线追踪图像是通过为帧中的每个像素生成一条光线来创建的。 当光线与场景中的对象相交并将像素的颜色设置为相交点处的对象颜色时。这称为后向跟踪(backward-)或eye-tracing(因为我们跟踪光线从相机到物体以及从物体到光源的路径,而不是从光源到物体,从物体到相机)。

在这里插入图片描述

图1. 向后追踪(或eye-tracing)包括追踪从眼睛穿过图像每个像素中心的光线。 如果光线与场景中的对象相交,则光线穿过的像素的颜色被设置为交点处对象的颜色。
 

自然地,创建图像的过程将从构建这些光线开始,我们称之为主光线相机光线(主要是因为这些光线是我们将投射到场景中的第一条光线。二次光线是例如阴影光线的光线)。我们知道它们是从相机的原点开始的。在几乎所有的 3D 应用程序中,相机在创建时的默认位置是世界的原点,由坐标为 (0, 0, 0) 的点定义。在针孔相机模型中,可以将相机的原点视为相机光圈aperture)(也是投影的中心)。现实世界中,针孔相机的胶片(film)位于光圈后面,通过几何构造使光线形成场景的倒影。但是,如果胶片平面与场景位于同一侧(在光圈前面而不是后面),则可以避免这种倒影。按照惯例,在光线追踪中,它通常被放置在距相机原点 1 个单位的位置(这个距离永远不会改变)。

按照惯例,我们还将沿负 z 轴定向相机(相机默认方向由开发人员选择,但通常相机沿正 z 轴或负 z 轴定向。RenderMan、Maya、PBRT 和OpenGL 将相机沿负 z 轴对齐,我们建议开发人员遵循相同的约定)。

最后,我们假设我们渲染的图像是正方形的以像素为单位的图像的宽度和高度相同)。

在这里插入图片描述

图2. 左:一个基本的相机。 原始图像大小为 6x6 像素,眼睛的默认位置是世界的中心 (0, 0, 0)。 请注意相机如何沿负 z 轴指向。 图像平面距离原点正好 1 个单位。 右:y 轴左侧和 x 轴下方的像素具有负世界空间坐标。
 

我们的任务包括为帧的每个像素创建一个主光线。 这可以很容易地通过从相机原点开始并穿过每个像素中心的直线来完成(图1)。我们可以用射线的形式来表示这条线,它的原点是相机的原点,它的方向是从相机原点到像素中心的向量。 为了计算一个点在像素中心的位置,我们需要将最初在栅格空间raster space)中表示的像素坐标(点坐标以像素表示,坐标(0,0)位于帧的左上角) 转换到世界空间world space)。

为什么是世界空间? 因为世界空间基本上是场景中所有对象、几何体、灯光和相机的坐标表达的空间。 例如,如果一个圆盘位于沿负 z 轴距世界原点 5 个单位的位置,则该圆盘的世界空间坐标为 (0, 0, -5)。 如果我们想要计算射线与该圆盘的交点的数学运算,那么射线的原点和方向也需要在同一个空间中定义。 例如,如果一条射线具有原点 (0, 0, 0) 和方向 (0, 0, -1),其中这些数字表示世界空间坐标系中的坐标,那么射线将在 (0, 0, - 5)与圆盘相交。 如图 3 所示。

在这里插入图片描述

图3. 光线-圆盘 相交
 

在这里插入图片描述

图4. 从光栅到屏幕空间
 

另一种看待我们正在试图解决的问题的方式是从相机开始的。 我们知道,成像平面离世界坐标系原点只有一个单位的距离,并沿负 z 轴对齐。 我们还知道,图像是正方形的因此图像投影在成像平面上的部分也一定是正方形的。 由于我们很快将解释的原因,这个投影区域的尺寸是 2 × 2 单位**(图2**)。我们还知道光栅图像是由像素组成的。 我们需要找到这些像素点栅格空间中的坐标与在世界空间中表示的相同像素点的坐标之间的关系。 这个过程需要几个步骤,如图5所示。

我们首先需要使用帧的尺寸来规范化这个像素位置。 像素的新归一化坐标被称为在 NDC 空间中定义(Normalized Device Coordinates):
P i x e l N D C x = P i x e l x + 0.5 I m a g e W i d t h PixelNDC_x=\frac{Pixel_x+0.5}{ImageWidth} PixelNDCx=ImageWidthPixelx+0.5

P i x e l N D C y = P i x e l y + 0.5 I m a g e H e i g h t PixelNDC_y=\frac{Pixel_y+0.5}{ImageHeight} PixelNDCy=ImageHeightPixely+0.5

请注意,我们向像素位置添加了一个小的偏移 (0.5)因为我们希望最终的相机光线穿过像素的中间

根据上式,针对图5中的 Raster space , P i x e l m a x = 5 Pixel_{max}=5 Pixelmax=5
( P i x e l N D C x ) m a x = 5 + 0.5 6 = 0.92 (PixelNDC_x)max= \frac{5+0.5}{6}=0.92 (PixelNDCx)max=65+0.5=0.92
P i x e l N D C x PixelNDC_x PixelNDCx 最大值是 0.92 而不是1
我觉得问题出在 (PixelNDC_x)max,这个应该是可以取6
也许是在代码实现中使得这个满足要求
我还需要继续探索一下这个点。

在 NDC 空间中表示的像素坐标在 [0,1] 范围内光线追踪中的 NDC 空间与光栅化世界中的 NDC 空间不同,后者通常映射到范围 [-1,1])。

正如在图 2 中看到的那样,胶片或图像平面(film or image plane)以世界原点为中心。 换句话说,位于图像左侧的像素应具有负 x 坐标,而位于右侧的像素应具有正 x 坐标。 位于 x 轴上方的像素应具有正 y 坐标,而位于下方的像素应具有负 y 坐标。 我们可以通过将当前在 [0,1] 范围内的归一化像素坐标重新映射到 [-1,1] 范围来纠正此问题
P i x e l S c r e e n x = 2 × P i x e l N D C x − 1 PixelScreen_x=2\times PixelNDC_x -1 PixelScreenx=2×PixelNDCx1

P i x e l S c r e e n y = 2 × P i x e l N D C y − 1 PixelScreen_y=2\times PixelNDC_y -1 PixelScreeny=2×PixelNDCy1

但是请注意,对于这个等式, P i x e l R e m a p p e d y Pixel\quad Remappedy PixelRemappedy对于位于x轴上方的像素是负的,对于位于x轴下方的像素是正的(事实上应该反过来)。 这应该是由于在 Raster space 中y 轴正方向向下, Screen space 中y 轴正方向向上导致的。

以图 5 中的 NDC space 中位于棕色水平线上方的点(0.75,0.42)举例子: 2 × 0.42 − 1 = − 0.16 2\times 0.42-1=-0.16 2×0.421=0.16。但是该点位于 Screen space x 轴(棕色轴)的上方,它的坐标 y 值理应是正值。

下面的公式将纠正这个问题:
P i x e l S c r e e n y = 1 − 2 × P i x e l N D C y PixelScreen_y=1-2 \times PixelNDC_y PixelScreeny=12×PixelNDCy
该值现在从 1 变化到 -1,因为 P i x e l y Pixel_y Pixely从 0 变化到 I m a g e W i d t h ImageWidth ImageWidth。 以这种方式表达的坐标被称为是在屏幕空间(screen space)中定义的。

在这里插入图片描述

图5. 将像素中间点的坐标转换为世界坐标需要几个步骤。 该点的坐标首先表示在光栅空间(像素坐标加上偏移量 0.5),然后转换到 NDC 空间(坐标重新映射到范围 [0,1])然后转换到屏幕空间(NDC 坐标 被重新映射到 [-1,1])。 应用最终的相机到世界变换 4x4 矩阵将屏幕空间中的坐标变换到世界空间。
 

到目前为止,我们一直假设图像正方形的。 现在让我们来看一个图像尺寸为 7 * 5 像素的情况(它是一个小图像,但仍然是一个图像)。

图像的宽度除以高度得到的值是1.4。 当在屏幕空间中定义像素坐标时,它们在[- 1,1]范围内。 然而,沿 x 轴 (7个像素) 的像素比沿 y 轴 (5个像素) 的像素多,因此一个像素的“高度”比“宽度”大(见图 6左图)。为了使它们再次成为正方形(像素应该是这样),我们需要将像素的x坐标乘以图像纵横比(aspect ratio)。注意,这个操作保持y-像素坐标(在屏幕空间中)不变。 它们仍然在[-1,1]范围内,但x像素坐标现在在[-1.4,1.4]范围内(更普遍的是[-aspect ratio,aspect ratio])。

在这里插入图片描述

图6. 左:因为一张图像的宽度和高度不同,所以像素不是正方形的。 为了纠正这个问题,我们需要通过图像纵横比沿 x 轴伸缩图像平面,该纵横比可以通过将图像的宽度除以图像的高度(以像素为单位)计算得出。
 

I m a g e A s p e c t R a t i o = I m a g e W i d t h I m a g e H e i g h t ImageAspectRatio = \frac{ImageWidth}{ImageHeight} ImageAspectRatio=ImageHeightImageWidth

P i x e l C a m e r a x = ( 2 × P i x e l S c r e e n x − 1 ) × I m a g e A s p e c t R a t i o PixelCamera_x = (2\times PixelScreen_x -1)\times ImageAspectRatio PixelCamerax=(2×PixelScreenx1)×ImageAspectRatio

P i x e l C a m e r a y = ( 1 − 2 × P i x e l S c r e e n y ) PixelCamera_y=(1-2\times PixelScreen_y) PixelCameray=(12×PixelScreeny)

最后,我们需要考虑视野(filed of view)。 请注意,到目前为止,屏幕空间中定义的任何点的 y 坐标都在 [-1, 1] 范围内。 我们也知道成像平面距离相机的原点有 1 个单位。 如果我们从侧面看相机的设定,我们可以通过将相机的原点连接到成像平面的顶部和底部边缘来绘制一个三角形。 因为我们知道从相机原点到成像平面的距离(1 个单位)和成像平面的高度(2 个单位,因为它从 y=1 到 y=-1),我们可以使用一些简单的三角函数来求出 ∠ C A B \angle CAB CAB
∠ C A B = α 2 = arctan ⁡ ( O p p o s i t e S i d e A d j a c e n t S i d e ) = a t a n 1 1 = π 4 \angle CAB=\frac{\alpha}{2}=\arctan(\frac{OppositeSide}{AdjacentSide})=atan\frac{1}{1}=\frac{\pi}{4} CAB=2α=arctan(AdjacentSideOppositeSide)=atan11=4π在这里插入图片描述

图7. 相机设定的侧视图。 从相机原点位置到图像平面的距离为 1 个单位(矢量 AB)。 从 B 到 C 的距离也是 1 个单位。 使用简单的三角函数,我们可以很容易地计算出角度 \alpha
 

换言之,特定情况下的视野或角度 α \alpha α 是90度。 现在请注意,要计算 BC 线的长度,我们只需要计算 α 2 \frac{\alpha}{2} 2α 的正切值:
∥ B C ∥ = tan ⁡ ( α 2 ) \Vert{BC}\Vert = \tan(\frac{\alpha}{2}) BC=tan(2α)
我们还可以观察到,对于 α \alpha α大于 9 0 ∘ 90^{\circ} 90的情况, ∥ B C ∥ \Vert{BC}\Vert BC大于 1,对于小于 9 0 ∘ 90^{\circ} 90的情况, ∥ B C ∥ \Vert{BC}\Vert BC小于 1。例如,如果 α = 6 0 ∘ \alpha=60^{\circ} α=60,则 tan ⁡ ( 60 / 2 ) ∘ = 0.57 \tan(60/2)^{\circ}=0.57 tan(60/2)=0.57,如果 α = 11 0 ∘ \alpha=110^{\circ} α=110,则 tan ⁡ ( 110 / 2 ) ∘ = 1.43 \tan(110/2)^{\circ}=1.43 tan(110/2)=1.43。 因此,我们可以将屏幕像素坐标(目前包含在 [-1, 1] 范围内)乘以这个数字来放大或缩小它们。

你可能已经猜到了,这个操作改变了我们看到的场景的多少,相当于放大(当视野减小时我们看到的场景更少)和缩小(当视野值增加时,可以看到更多的场景)。 总之,我们可以根据角度 α \alpha α 定义相机的视野,并将屏幕像素坐标乘以 tan ⁡ α 2 \tan \frac{\alpha}{2} tan2α的结果(如果该角度以度数表示,请不要忘记 将其转换为弧度):
P i x e l C a m e r a x = ( 2 × P i x e l S c r e e n x − 1 ) × I m a g e A s p e c t R a t i o × tan ⁡ ( α 2 ) PixelCamera_x=(2\times PixelScreen_x-1)\times ImageAspectRatio\times \tan(\frac{\alpha}{2}) PixelCamerax=(2×PixelScreenx1)×ImageAspectRatio×tan(2α)

P i x e l C a m e r a y = ( 1 − 2 × P i x e l S c r e e n y ) × tan ⁡ ( α 2 ) PixelCamera_y = (1-2\times PixelScreen_y)\times\tan(\frac{\alpha}{2}) PixelCameray=(12×PixelScreeny)×tan(2α)

此时,原始像素坐标以相对于相机的图像平面的方式表达。 它们已被归一化,在 [-1,1] 之间重新映射,乘以图像纵横比,并乘以视场角 α \alpha α除以 2 的正切。这个点被称为在相机空间(camera space)中,因为它的坐标是关于相机的坐标系表示的。 当相机在其默认位置时,相机的坐标系和世界坐标系是对齐的。 该点位于距离相机原点 1 个单位的成像平面上,但请记住,相机也沿负 z 轴对齐。 因此我们可以将像素在成像平面上的最终坐标表示为:
P c a m e r a S p a c e = ( P i x e l C a m e r a x , P i x e l C a m e r a y , − 1 ) P_{cameraSpace}=(PixelCamera_x,PixelCamera_y,-1) PcameraSpace=(PixelCamerax,PixelCameray,1)
这为我们提供了相机图像平面上图像中像素的位置 P ( P c a m e r a S p a c e P_{cameraSpace} PcameraSpace)。 从那里,我们可以通过将光线的原点定义为相机的原点(让我们将此点称为 O)并将光线的方向定义为归一化向量 OP(图 8)来计算该像素的光线。 矢量 OP 只是成像平面上点的位置减去相机原点。 当相机处于默认位置时,相机原点和世界笛卡尔坐标系相同,因此点 O 就是 (0, 0, 0)。

伪代码如下:

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 

最后,我们希望能够从任何特定的角度渲染场景的图像。 将相机从其原始位置(以世界坐标系的原点为中心并沿负 z 轴对齐)移动后,您可以使用 4x4 矩阵表示相机的平移和旋转。 通常这个矩阵被称为相机到世界矩阵的变换矩阵(它的逆矩阵被称为世界到相机的变换矩阵)。 如果我们将这个相机到世界的变换矩阵应用于我们的点 O 和 P,那么向量 O ′ P ′ O'P' OP (其中 O ′ O' O 是由点 O, P ′ P' P是由点 P,在经过相机到世界变换矩阵分别得到的)表示光线在世界空间中的归一化方向(图 8)。 将相机到世界变换应用于 O 和 P 将这两个点从相机空间变换到世界空间。 另一种方法是在相机处于其默认位置(向量 OP)时计算光线方向,并将相机到世界变换矩阵应用于该向量。

在这里插入图片描述

图8. 我们可以在空间中移动相机以根据需要构建场景。 相机的最终位置和朝向可以用4x4矩阵表示,我们通常称之为相机到世界的变换矩阵。 如果我们知道O(摄像机的原点,也是世界坐标系的原点)和P(光线经过的像素在相机空间中的位置),我们可以通过将O和P分别乘以摄像机到世界的变换矩阵,很容易地得到0'和P'。 最后射线方向可以用P'-O'来计算。
 

请注意相机坐标系如何随相机移动。 我们的伪代码可以很容易地修改以考虑相机变换(旋转和平移,不推荐缩放相机):

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 

源代码

本文的源代码只是一个简单的示例,说明如何为图像的每个像素生成光线。代码循环遍历图像的所有像素(第 13-14 行),并计算当前像素的射线。我们将本文中描述的所有重映射(remapping)步骤组合在一行代码中。 原始 x 像素坐标除以图像宽度以将初始坐标重新映射到范围 [0,1]。 然后将结果值重新映射到范围 [-1,1],按scale变量(第 9 行)和图像纵横比(第 10 行)进行伸缩。像素 y 坐标以类似的方式转换,但请记住,y 归一化坐标需要翻转(第 16 行)。 在这个过程结束时,我们可以使用转换后的点 x 和 y 坐标创建一个向量。 该向量的 z 坐标设置为负一(第 18 行):默认情况下,相机向下看负 z 轴。 结果向量最终由相机到世界相机进行转换并归一化。 相机的原点也被相机到世界矩阵转换(第 12 行)。 我们最终可以将转换到世界空间的光线方向和原点传递给 rayCast 函数。

void render( 
    const Options &options, 
    const std::vector<std::unique_ptr<object>> &objects, 
    const std::vector<std::unique_ptr<light>> &lights) 
{ 
    Matrix44f cameraToWorld; 
    Vec3f *framebuffer = new Vec3f[options.width * options.height]; 
    Vec3f *pix = framebuffer; 
    float scale = tan(deg2rad(options.fov * 0.5)); 
    float imageAspectRatio = options.width / (float)options.height; 
    Vec3f orig; 
    cameraToWorld.multVecMatrix(Vec3f(0), orig); 
    for (uint32_t j = 0; j < options.height; ++j) { 
        for (uint32_t i = 0; i < options.width; ++i) { 
            float x = (2 * (i + 0.5) / (float)options.width - 1) * imageAspectRatio * scale; 
            float y = (1 - 2 * (j + 0.5) / (float)options.height) * scale; 
            Vec3f dir; 
            cameraToWorld.multDirMatrix(Vec3f(x, y, -1), dir); 
            dir.normalize(); 
            *(pix++) = castRay(orig, dir, objects, lights, options, 0); 
        } 
    } 
 
    // Save result to a PPM image (keep these flags if you compile under Windows)
    std::ofstream ofs("./out.ppm", std::ios::out | std::ios::binary); 
    ofs << "P6\n" << options.width << " " << options.height << "\n255\n"; 
    for (uint32_t i = 0; i < options.height * options.width; ++i) { 
        char r = (char)(255 * clamp(0, 1, framebuffer[i].x)); 
        char g = (char)(255 * clamp(0, 1, framebuffer[i].y)); 
        char b = (char)(255 * clamp(0, 1, framebuffer[i].z)); 
        ofs << r << g << b; 
    } 
 
    ofs.close(); 
 
    delete [] framebuffer; 
}</std::unique_ptr<light></std::unique_ptr<object> 

其他

在计算机图形学中,通常是存在不同的方法,但是获得相同结果。 如果您查看其他渲染引擎的源代码,您很可能会发现将光线从图像空间转换到世界空间的问题可以通过多种不同的方式来解决。 然而,无论采用何种方法,结果当然应该始终相同。

在这里插入图片描述

例如,我们可以这样看问题:像素坐标不需要进行归一化(换句话说,从像素坐标转换为 NDC 再转换到屏幕空间)。

我们可以使用以下等式计算光线方向 d = [ d x , d y , d Z ] T \mathbf{d}=[d_x,d_y,d_Z]^T d=[dx,dy,dZ]T
d x = x − w i d t h / 2 d_x = x-width/2 dx=xwidth/2

d y = h e i g h t / 2 − y d_y = height/2-y dy=height/2y

d z = − h e i g h t / 2 tan ⁡ ( f o v / 2 ) d_z = -\frac{height/2}{\tan(fov / 2)} dz=tan(fov/2)height/2

其中 xy像素的坐标fov 是垂直视野。 请记住, d z d_z dz 是负数,因为默认情况下相机沿负 z 轴定向。

然后,如果我们对这个向量进行归一化,我们会得到与通过光栅到 NDC 以及 NDC 到屏幕变换相同的结果。

一般形式下的相机到世界坐标系的变换矩阵的形式如下:
[ u x v x w x t 1 u y v y w y t 2 u z v z w z t 3 0 0 0 1 ] \begin{bmatrix} u_{x} && v_{x}&& w_{x}&&t_{1} \\ u_{y} && v_{y}&& w_{y}&&t_{2}\\ u_{z} && v_{z}&& w_{z}&&t_{3}\\ 0 && 0&& 0&&1 \end{bmatrix} uxuyuz0vxvyvz0wxwywz0t1t2t31
其中 t 1 , t 2 , t 3 t_1,t_2,t_3 t1,t2,t3 是 变换矩阵对应的平移向量 T T T 的三个分量,而 T T T 左上角的部分对应旋转矩阵 R R R

在这里,假设没有平移。则 t 1 = t 2 = t 3 = 0 t_1=t_2=t_3=0 t1=t2=t3=0

如果我们将此光线方向转换到世界空间(我们将向量 d \mathbf{d} d 乘以相机到世界矩阵)
[ d x ′ d y ′ d z ′ 1 ] = [ u x v x w x 0 u y v y w y 0 u z v z w z 0 0 0 0 1 ] [ d x d y d z 1 ] \begin{bmatrix} d_x' \\ d_y' \\ d_z' \\ 1 \end{bmatrix}= \begin{bmatrix} u_{x} && v_{x}&& w_{x}&&0 \\ u_{y} && v_{y}&& w_{y}&&0\\ u_{z} && v_{z}&& w_{z}&&0\\ 0 && 0&& 0&&1 \end{bmatrix} \begin{bmatrix} d_x \\ d_y \\ d_z \\ 1 \end{bmatrix} dxdydz1 = uxuyuz0vxvyvz0wxwywz00001 dxdydz1

我们得到:
d x ′ = ( x − w i d t h / 2 ) u x + ( h e i g h t / 2 − y ) v x − ( ( h e i g h t / 2 ) / tan ⁡ ( f o v / 2 ) ) w x d_x' = (x-width/2)u_x+(height/2-y)v_x-((height/2)/\tan(fov/2))w_x dx=(xwidth/2)ux+(height/2y)vx((height/2)/tan(fov/2))wx

d y ′ = ( x − w i d t h / 2 ) u y + ( h e i g h t / 2 − y ) v y − ( ( h e i g h t / 2 ) / tan ⁡ ( f o v / 2 ) ) w y d_y' = (x-width/2)u_y+(height/2-y)v_y-((height/2)/\tan(fov/2))w_y dy=(xwidth/2)uy+(height/2y)vy((height/2)/tan(fov/2))wy

d x ′ = ( x − w i d t h / 2 ) u z + ( h e i g h t / 2 − y ) v z − ( ( h e i g h t / 2 ) / tan ⁡ ( f o v / 2 ) ) w z d_x' = (x-width/2)u_z+(height/2-y)v_z-((height/2)/\tan(fov/2))w_z dx=(xwidth/2)uz+(height/2y)vz((height/2)/tan(fov/2))wz

如果我们扩展并重新组合,得:
d x ′ = x u x − w i d t h / 2 u x + h e i g h t / 2 v x − y v x − ( h e i g h t / 2 ) tan ⁡ ( f o v / 2 ) w x = x u x + y ( − v x ) + ( − w i d t h / 2 u x + h e i g h t / 2 v x − ( h e i g h t / 2 ) / tan ⁡ ( f o v / 2 ) w x ) \begin{aligned} d_x'=& xu_x -width/2u_x+height/2v_x-yv_x-(height/2)\tan(fov/2)w_x \\ =& xu_x+y(-v_x)+(-width/2u_x+height/2v_x-(height/2)/\tan(fov/2)w_x) \end{aligned} dx==xuxwidth/2ux+height/2vxyvx(height/2)tan(fov/2)wxxux+y(vx)+(width/2ux+height/2vx(height/2)/tan(fov/2)wx)

d y ′ = x u y − w i d t h / 2 u y + h e i g h t / 2 v y − y v y − ( h e i g h t / 2 ) tan ⁡ ( f o v / 2 ) w y = x u y + y ( − v y ) + ( − w i d t h / 2 u y + h e i g h t / 2 v y − ( h e i g h t / 2 ) / tan ⁡ ( f o v / 2 ) w y ) \begin{aligned} d_y'=& xu_y -width/2u_y+height/2v_y-yv_y-(height/2)\tan(fov/2)w_y \\ =& xu_y+y(-v_y)+(-width/2u_y+height/2v_y-(height/2)/\tan(fov/2)w_y) \end{aligned} dy==xuywidth/2uy+height/2vyyvy(height/2)tan(fov/2)wyxuy+y(vy)+(width/2uy+height/2vy(height/2)/tan(fov/2)wy)

d z ′ = x u z − w i d t h / 2 u z + h e i g h t / 2 v z − y v z − ( h e i g h t / 2 ) tan ⁡ ( f o v / 2 ) w z = x u z + y ( − v z ) + ( − w i d t h / 2 u z + h e i g h t / 2 v z − ( h e i g h t / 2 ) / tan ⁡ ( f o v / 2 ) w z ) \begin{aligned} d_z'=& xu_z -width/2u_z+height/2v_z-yv_z-(height/2)\tan(fov/2)w_z \\ =& xu_z+y(-v_z)+(-width/2u_z+height/2v_z-(height/2)/\tan(fov/2)w_z) \end{aligned} dz==xuzwidth/2uz+height/2vzyvz(height/2)tan(fov/2)wzxuz+y(vz)+(width/2uz+height/2vz(height/2)/tan(fov/2)wz)

上式可以重写为:
d = x u + y ( − v ) + w ′ (1) d = xu+y(-v)+w' \tag{1} d=xu+y(v)+w(1)
其中,
w ′ = ( − w i d t h / 2 ) u + ( h e i g h t / 2 ) v − ( ( h e i g h t / 2 ) / tan ⁡ ( f o v / 2 ) ) w w'=(-width/2)u+(height/2)v-((height/2)/\tan(fov/2))w w=(width/2)u+(height/2)v((height/2)/tan(fov/2))w
换句话说,如果我们知道相机到世界的变换矩阵,我们可以预先计算 w ′ w' w向量,并使用等式 1 计算世界空间中的光线方向,然后对结果向量进行归一化。 在伪代码中:

Vec3f w_p = (-width / 2 ) * u + (height / 2) * v - ((height / 2) / tan(fov_rad * 0.5); 
Vec3f ray_dir = normalize(x * u + y * (-v) + w_p); 

向量 w’ 只需要计算一次,并且每次我们需要计算新的光线方向时都需要重新使用。

向量 u、v 和 w 只是相机到世界变换矩阵的第一个、第二个和第三个向量(如果使用行主矩阵,则为前三行)。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

培之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值