1. 主页
  2. 文档
  3. 从零开始的计算机图形_程序员的3D渲染介绍教程
  4. 从零开始的计算机图形_程序员的3D渲染介绍 基本的光线追踪
  5. 渲染我们的第一个球体

渲染我们的第一个球体

回顾一下,对于画布上的每个像素,我们可以在视口中计算相应的点。给定摄像机的位置,我们可以表达出从摄像机处出发,经过视口中那个点的光线的方程。给定一个球面,我们可以计算出射线与这个球面的交点。

因此,我们需要做的就是计算射线和每个球体的交点,保留最靠近摄像机的交点,并在画布上用适当的颜色涂抹该像素。我们几乎已经准备好渲染我们的第一个球体了!

不过,参数t值得特别注意。让我们回到射线方程:

P = O + t(V – O)

由于射线的原点和方向是固定的,在所有的实数上改变t将得到这个射线上的每一个点P。注意,对于t=0,我们得到P=O,对于t=1,我们得到P=V。因此,我们可以把参数空间分为三个部分,如表2-1所示。图2-9是参数空间的示意图。

表2-1: 参数空间的划分


t < 0 摄像机后面
0 < t < 1 在摄像机和投影平面/视口之间
t > 1 在投影平面/视口的前面


图2-9:参数空间中的几个点

请注意,相交方程中没有说球体必须在摄像机前面;方程会很高兴地产生摄像机后面的相交的解。显然,这不是我们想要的,所以我们应该忽略任何t<0的解。为了避免进一步的数学上的不愉快,我们将把解限制在t>1;也就是说,我们将渲染超出投影平面的内容。

另一方面,我们不想给t的值设定一个上限;我们希望看到相机前面的所有物体,不管它们有多远。但是,因为在后面的阶段我们想要缩短射线的长度,所以现在我们将引入这种形式,并给t一个+∞的上值(对于不能直接表示“无穷大”的语言,一个非常非常大的数字就可以做到这一点)。

我们现在可以用一些伪代码来正式说明我们到目前为止所做的一切。一般来说,我们会假设代码可以访问它所需要的任何数据,所以我们不会麻烦地明确传递诸如画布之类的参数,而会专注于真正必要的参数。
main方法现在看起来像清单2-2。


O = (0, 0, 0)
for x = -Cw/2 to Cw/2 {
    for y = -Ch/2 to Ch/2 {
        D = CanvasToViewport(x, y)
        color = TraceRay(O, D, 1, inf)
        canvas.PutPixel(x, y, color)
    }
} 

清单2-2:main方法

CanvasToViewport函数非常简单,如清单2-3所示。常数d代表相机和投影平面之间的距离。


CanvasToViewport(x, y) {
    return (x*Vw/Cw, y*Vh/Ch, d)
}

清单2-3:CanvasToViewport函数

TraceRay方法(清单2-4)计算射线与每个球体的交点,并返回t的要求范围内最近的交点的球体颜色。


TraceRay(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in [t_min, t_max] and t1 < closest_t {
            closest_t = t1
            closest_sphere = sphere
        }
        if t2 in [t_min, t_max] and t2 < closest_t {
            closest_t = t2
            closest_sphere = sphere
        } 
    }
    if closest_sphere == NULL {
        ❶ return BACKGROUND_COLOR
    }
    return closest_sphere.color
}

清单2-4:TraceRay方法

在清单2-4中,O代表射线的原点;虽然我们是从相机追踪射线,而相机是放在原点的,但在后面的阶段不一定是这样的,所以它必须是一个参数。这同样适用于t_min和t_max。

请注意,当射线不与任何球体相交时,我们仍然需要返回某种颜色❶–在这些例子中,大部分都选择白色。
最后,IntersectRaySphere(清单 2-5)只是解决了二次方程的问题。


IntersectRaySphere(O, D, sphere) {
    r = sphere.radius
    CO = O - sphere.center
    a = dot(D, D)
    b = 2*dot(CO, D)
    c = dot(CO, CO) - r*r
    discriminant = b*b - 4*a*c
    if discriminant < 0 {
        return inf, inf
    }
    t1 = (-b + sqrt(discriminant)) / (2*a)
    t2 = (-b - sqrt(discriminant)) / (2*a)
    return t1, t2
} 

清单2-5:IntersectRaySphere方法

为了将所有这些付诸实践,让我们定义一个非常简单的场景,如图2-10所示

图2-10:一个非常简单的场景,从上面(左)和右边(右)看。

用伪代码来说,是这样的。


viewport_size = 1 x 1
projection_plane_d = 1
sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0) # Red
}
sphere {
    center = (2, 0, 4)
    radius = 1
    color = (0, 0, 255) # Blue
}
sphere {
    center = (-2, 0, 4)
    radius = 1
    color = (0, 255, 0) # Green
}

当我们在这个场景中运行我们的算法时,我们最终得到了一个非常棒的光线追踪场景(图2-11)。

图2-11:一个非常棒的光线追踪场景

您可以在https://gabrielgambetta.com/cgfs/basic-rays-demo上找到该算法的实时实现。

我知道,这有点让人失望,不是吗?反射、阴影和抛光效果在哪里?别担心,我们会成功的。这是很好的第一步。球体看起来像圆圈,这比它们看起来像猫要好。它们看起来不太像球体的原因是,我们缺少了人类决定物体形状的一个关键组成部分:物体与光的交互方式。我们将在下一章讨论这个问题。

这篇文章对您有用吗?