Rust 光线追踪 03: 渲染一个圆

238 阅读4分钟

下面的公式有点多,只是把原教程中的公式详细展开了,一步一步来,很简单,相信我。

给定一个点,判断一个点是否在一个半径为 RR 的圆或者圆外、圆内,我们可以使用下面的公式来判断。这里假设圆心在 (000)(0,0,0) 点,半径为 RR

如果一个点 (x,y,z)(x, y, z) 在圆上,则 x2+y2+z2=R2x^2 + y^2 + z^2 = R^2

如果一个点 (x,y,z)(x, y, z) 在圆内,则 x2+y2+z2<R2x^2 + y^2 + z^2 < R^2

如果一个点 (x,y,z)(x, y, z) 在圆外,则 x2+y2+z2>R2x^2 + y^2 + z^2 > R^2

假设圆心在点 C=(Cx,Cy,Cz)C = (C_x, C_y, C_z),半径为 r,则根据上面的第一个公式,可以得到

(xCx)2+(yCy)2+(zCz)2=r2(x - C_x)^2 + (y - C_y)^2 + (z - C_z)^2 = r^2

我们可以将点 (x,y,z)(x, y, z) 使用之前的 Vec3 向量表示,例如设 P=(x,y,z)点P = (x, y, z),则可以将上面的公式,换一种表达形式,也就是

(PC)(PC)=r2(P - C) \cdot (P - C) = r^2

还记得我们的光线公式吗?P(t)=A+tbP(t) = A + tb,A是光线的起点,b是光线的方向,t是一个缩放参数,而得到的 P(t)P(t) 则是光线打到的点。我们想知道一条光线所打到的点,是否在圆上或者圆内。是不是只要将光线公式,代入上面的公式,就可以了?也就是将原来的点P,换成了光线函数 P(t),就会得到下面的公式

(P(t)C)(P(t)C)=r2(P(t) - C) \cdot (P(t) - C) = r^2

由于 P(t)=A+tbP(t) = A + tb,所以我们展开上面的公式,可以得到

(A+tbC)(A+tbC)=r2(A + tb - C) \cdot (A + tb - C) = r^2

而上面的公式,又可以换成另一种表达形式

(A+tbC)2=r2(A + tb - C)^2 = r^2

这个公式就是我们以前很熟悉的 (a+bc)2(a + b - c)^2 的形式,展开形式就是 a2+2ab2ac+b22bc+c2a^2 + 2ab - 2ac + b^2 - 2bc + c^2

那么,我们将公式 (A+tbC)2=r2(A + tb - C)^2 = r^2 用同样的方式展开,则可以得到

A2+2Atb2AC+(tb)22tbC+C2=r2A^2 + 2Atb - 2AC + (tb)^2 - 2tbC + C^2 = r^2

整理一下,我们可以看出,上面的第1项、第3项、第6项,可以组合到一起组成一个公式,也就是 A22AC+C2=(AC)(AC)A^2 - 2AC + C^2 = (A - C) \cdot (A - C),修改上面的式子,可以得到

2Atb+(tb)22tbC+(AC)(AC)=r22Atb + (tb)^2 - 2tbC + (A - C) \cdot (A - C) = r^2

上面的式子中 2Atb2Atb2tbC2tbC 都有一个 2tb2tb,可以组合一下,得到 2tb(AC)2tb \cdot (A - C),用这种形式修改上面的公式,得到

(tb)2+2tb(AC)+(AC)(AC)=r2(tb)^2 + 2tb \cdot (A - C) + (A - C) \cdot (A - C) = r^2

(tb)2(tb)^2 可以换成一种形式,表示成 t2b2t^2 \cdot b^2,再换一种形式可以为 t2bbt^2b \cdot b,用这种形式修改上面的公式,可以得到最终的公式如下

t2bb+2tb(AC)+(AC)(AC)=r2t^2b \cdot b + 2tb \cdot (A - C) + (A - C) \cdot (A - C) = r^2

r2r^2 移到公式左边,可以得到

t2bb+2tb(AC)+(AC)(AC)r2=0t^2b \cdot b + 2tb \cdot (A - C) + (A - C) \cdot (A - C) - r^2 = 0

这就是最终的公式了,为什么要绕这么多换成这样一个形式呢,我猜作者是为了计算方便,写代码方便。

对于上面的公式,我们已经知道 A 是光线的起点,C 是圆心,而 b 是光线的方向,r 是圆的半径,唯一不知道的变量就是 t,而上面的方程,是一个一元二次方程,根据上学时学的知识,我们知道一元二次方程的解,可能有0个,1个,或2个。对于上面公式,也就是我们渲染圆的目标而已,只要有 > 0 个解,则说明光线打到了圆。

0301.jpg

接下来还没完,我们如何知道上面的公式解的个数呢?根据一元二次方程的形式

ax2+bx+c=0ax^2 + bx + c = 0

它的根可以表示为 b±b24ac2a\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}

对于上面公式中的 b24acb^2 - 4ac,如果结果大于 0,则方程有两个不相等的实数根,等于 0,则方程有两个相等的实数根,小于 0,则方程无实数根。

所以,我们只需要将之前的公式

t2bb+2tb(AC)+(AC)(AC)r2=0t^2b \cdot b + 2tb \cdot (A - C) + (A - C) \cdot (A - C) - r^2 = 0

表示成

ax2+bx+c=0ax^2 + bx + c = 0 的形式,则可以很方便的知道方程有几个解。

对于我们而言,上面的公式,除了 tt 之外,其他的变量都是已知道的,我们直接在代码中去转换。这次的代码沿用上一小节的,然后在 main.rs 中添加一个函数 hit_sphere

fn hit_sphere(center: Vec3, radius: f64, r: Ray) -> bool {
    // 公式中的 (A - C)
    let oc = r.origin - center;

    // 公式中第1项的 b*b
    let a = Vec3::dot(r.direction, r.direction);

    // 公式中第2项的内容,忽略 t
    let b = 2.0 * Vec3::dot(oc, r.direction);

    // 公式中的 (A - C) * (A - C) - r^2
    let c = Vec3::dot(oc, oc) - radius * radius;

    // 计算出了 a, b, c,判断 b^2 - 4ac 解的个数
    let result = b * b - 4.0 * a * c;

    // 解的个数 >= 0,则打到了圆
    return result >= 0.0;
}

然后在 ray_color 函数中添加是否打到圆的判断,如果打到了圆,我们就返回当前像素为红色

fn ray_color(r: Ray) -> Color {
    if hit_sphere(Vec3::new(0.0, 0.0, -1.0), 0.5, r) {
        return Color::new(1.0, 0.0, 0.0);
    }

    // 将光线的方向标准化,保证其值在 -1 到 1 之间
    let unit_direction = Vec3::unit_vector(r.direction);

    // 为了计算方便,我们将方向的 y 值,从 [-1,1] 映射到 [0, 1]
    let t = 0.5 * (unit_direction.y + 1.0);

    // 做一个蓝白渐变,当 t 为 0 时,就是白色,将 t 为 1 时,就是蓝色
    return (1.0 - t) * Color::one() + t * Color::new(0.5, 0.7, 1.0);
}

cargo run > sphere.ppm 最终效果图如下

0302.png

任何技术问题,欢迎加微信 ifloop 互相交流,或者关注我的微信公众号 萌一小栈

600x219.png


### 另外 另外,也欢迎体验我的2个 独立游戏 ,直接点击下载连接,可试玩

>>> 音乐与僵尸:ZombieRhythm

>>> 诗仙与诗魔