Ray Tracer 笔记

这里先简要整理一下 RT in One Weekend 系列 前两本书的原理,为了后面 report 做帮助。

第一本书:基础部分

Ray class

光线从一个地方发出,有一个方向。因此,这个类有两个成员:origin: Point3 和 direction: Vec3。

对于每一个像素,三步走:

  1. 算出从该像素到人眼的光线;
  2. 这条光线与哪个物体交叉;
  3. 算出颜色。

注:本书一般将摄像机 / 人眼作为光源 \((t = 0)\) 处理,尽管这在初中物理里面是经典错误。但是在这个主题中无所谓谁是光源,处理正的 \(t\) 总比负的强。

解读:人眼在原点,视窗是那个矩形,眼通过视窗看世界(在窗的右侧)。

focal_length:原点到视窗的投影长。

Book 1 image 3: 球心坐标 \((0, 0, -1)\),就是解一元二次方程。

List 14 & 15:\(t_\min, t_\max\) 代表允许的最大和最小 \(t\ (\vec r = \bm A + t \bm B)\)

HitRecord:p 是打到的点,normal是法向量,t见上。

里外面问题

有时候我们需要记录里面和外面。HitRecord 类中添加 front_face: bool:true 表示光从外面打进来,false 反之。还可以写一个 set_face_normal 函数来判,将代码重用。

Camera

就是封装了一下上面讲的原点、视窗之类的。

Diffuse Material 漫反射材料

基本原理:出射光线的方向是单位法向量加上一个模长不超过 \(1\) 的向量,光线源点当然是入射光打上去的点。每次散射光强减半。

然后递归可能次数很多,我们限制在 \(50\) 次。不然光强太低了也没意义。

Gamma corrector 略。

HittableList

成员只有一个 Vec<Arc<Hittable>>。创建这个类的目的是:一条光线就只能打到一个位置,所以一堆物体里面只能打到一个,把它封装起来便于操作。

bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
    hit_record temp_rec;
    bool hit_anything = false;
    auto closest_so_far = t_max;

    for (const auto& object : objects) {
        if (object->hit(r, t_min, closest_so_far, temp_rec)) {
            hit_anything = true;
            closest_so_far = temp_rec.t;
            rec = temp_rec;
        }
    }

    return hit_anything;
}

金属材料

每一种材料,需要提供两种功能:提供被散射后的新光线;如果被散射,给出它强度减弱了多少。

注意如果随机到了 -rec.normal 那么出射光线就会很接近 \(\vec 0\),这是不好的。所以如果运气真这么好我们就不让出射光线偏移了。

镜面反射:出射光线 \(\vec v - 2\vec v \cdot \vec n \cdot \vec n\)

Fuzzy Reflection

介于完全的漫反射和完全的镜面反射之间。scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere());

显然 fuzz 最大为 \(1.0\)。

Dielectrics 电介质

兼具折射和反射,如水、玻璃等等。

10.2 节就是用折射定律硬推折射光线的向量表达式,这里先不讲了。

全反射:\(\sin \theta'\) 无解。

注:front_face = true 表示打过去的光线是从外面打过去的。但是如果 front_face == true 就让 ir = 1.0 / ir???是不是反了?但是看起来好像没什么问题。那倒推一下里面才是 front face,奇怪啊。

Schlick's approximation:在能折射的前提下,光线发生反射的 概率。它与入射角和折射率有关。所以我们可以用 reflectance(cos_theta, refraction_ratio) > random_double() 来得出它反射还是折射。

透明球

把半径调成负的,这样会导致折射率变成负的,几乎全部透射。

调整 camera

两个点:lookfrom,摄像机摆在哪里;lookat,摄像机看哪个点。但是在这种情况下摄像机并没有确定,因为它可以绕这个平面转起来。
lookfrom, lookat

因此,我们需要规定一个 vup,表示哪个方向是向上的。我们想要做的是构造一组正交基 \(\vec u, \vec v, \vec w\),其中 \(\vec w\) 是 lookfrom 和 lookat 的连线,\(\vec u, \vec v\) 张出一个垂直于 \(\vec w\) 的平面,\(\vec u\) 表示视窗的水平方向,\(\vec v\) 表示视窗的竖直方向,就像人眼观察的一样。那么,vup 在垂直于 \(\vec w\) 的平面的投影就是视窗的竖直方向。\(\vec u\) 直接叉乘求出即可。

Defocus blur

aperture 是镜头宽度的一种表示。我们打光的时候可以模拟镜头的效果。一个小圆盘上的点都可以作为发射点,而不是只从 lookfrom 一个点出发。

第二本书:进阶

Motion blur

模拟运动的物体的模糊效果。

用 time0 和 time1 表示快门开启的时间,然后 Ray class 里面记录一下这条光线发出的时间。光线发出时指定 random_double(time0, time1)即可。

class MovingSphere

move linearly from center0 at time0 to center1 at time1

Volume hierarchies 层次结构

我们通过这样的方式组织一个个物体:将所有物体分成两组,允许有重合;再在每组下继续分,直到分到每一个物体为止。

这样,我们从最上方开始访问这些组,看能不能打到组里的物体,不能打到直接退出;能打到的话就向下访问,看能打到哪个组,如果都能打到就比较哪一个位置更近(\(t\) 更小)。

接下来看看怎么分组。

Axis-Aligned Bounding Boxes (AABB)

简言之就是边都平行于坐标轴的长方体作为这样的“组”。我们观察到如果能相交,那么蓝色的区间和绿色的区间会有重合。

重合

然后我们要给每一种物体写一个 bounding box。HittableList 的 bounding box 可以通过遍历每一个成员并且对盒子取 \(\max\) 得到。

Bounding volume hierarchy (BVH)

BVH 就是上述的二叉树。BVH 结点有以下三个成员:

shared_ptr<hittable> left;
shared_ptr<hittable> right;
aabb box;

Hit 函数中用这种方式保证把最近的放进 HitRecord 里面:

bool hit_left = left->hit(r, t_min, t_max, rec);bool hit_right = right->hit(r, t_min, hit_left ? rec.t : t_max, rec);

return hit_left || hit_right;

最大的难点是构造 BVH 的结构。我们用一种比较简单的方式实现:随机选取一个坐标轴,按坐标轴对物体排序(比较函数取 bounding box 的左下角点或者右上角点坐标均可),然后分成两组递归构造 bounding box,直到组里面只有一到两个物体递归终止。

Textures 纹理

一般指一种方法,使得这个平面上的颜色可以程序化地生成。

球的处理

我们要把一个球面上的点坐标映射成 \((u, v)\),其中 \(u, v \in [0, 1]\),这是纹理所需要的。那么 \(u = \dfrac \theta {2\pi}, v = \dfrac \phi \pi\)。然后根据 \((x, y, z)\) 求 \((r, \theta, \phi)\) 就是硬解,略。

棋盘纹理

根据 \(\sin\) 函数的周期性处理。按 sines = sin(10*p.x())*sin(10*p.y())*sin(10*p.z()); 的正负区分颜色即可。

Perlin 噪声

一大特点:可重复性。给定一个三维的点,它一定会输出同样的颜色。点相距较近时输出的颜色也相近。

List 29 解读:
ranfloat 就是纯随机数组,一共 point_count 个元素。

perlin_generate_perm 随机产生一个 \(1\sim \rm point\_count\) 的排列。

然后调用接口 noise:参数是一个三维点 p。然后用它的三维坐标乘上 \(4\) 异或一下,相当于 hash 一下;然后输出 ranfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]] 即可。

List 33 是做了一下线性插值,有空填坑。

return color(1,1,1) * noise.noise(scale * p);

给 noise texture 加一个 scale,这个 scale 如果大于 \(1\),可以让 p 的坐标等比例扩大,从而让 perlin 生成的颜色变化更快。

我们的一大目标是让 perlin 变得更随机,现在生成的依然有点像 checker 一样网格状的。我们把 ranfloat 换成 ranvec,改成生成随机的 Vec3。

随机的那一堆规则根本看不懂,他又不说是怎么来的,不写了。

后面的先跳了,不然做不完了,这下成小丑了

upd 2023.07.27:明天 CR + presentation,为了防止 CR 和 pre 成为小丑,把博客更完。

Image Texture Mapping

首先,为了防止分辨率差异给这个过程带来的影响,我们用分数坐标来表达像素:

\[\begin{aligned} u &= \dfrac{i}{N_x - 1}\\ v &= \dfrac{j}{N_y - 1} \end{aligned} \]

然后把图片当作 texture,联想到 texture 就已经给出了 u, v 坐标的接口。然后 texture 封装成 material,再做成一个球即可。

Emitting 发光

向 DiffuseLight 中加入这样一个接口:

virtual color emitted(double u, double v, const point3& p) const override {
    return emit->value(u, v, p);
}

由于不是所有材料都会发光,所以我们默认返回黑色。

然后书上加一个小 feature: Background。就是没打到物体的时候返回的默认值。

长方形

第三本书:PDF method

PDF 全称为 probability density function 概率密度函数,表示一个连续随机变量在某点附近出现的概率。对于一个区间 \([l, r]\),变量落在这个区间内的概率为 \(\int_l^r p(x){\rm d} x\)。这是一个在高中统计学就接触过的概念。现在我们将这个概念用于计算积分值和光线追踪中。

基本性质

设概率密度函数为 \(p(x)\),它的反导数为 \(P(x)\),则

\[\int_{-\infty}^{\infty} p(x) {\rm d}x = {P(x)}\big |_{-\infty}^\infty = 1 \]

因为所有变量出现概率相加一定是 \(1\)。

运用均匀随机数生成符合特定概率密度函数的随机变量

假设 rand() 函数能够均匀随机产生 \([0, 1]\) 区间内的实数,再给定一个概率密度函数 \(p(x)\),如何产生满足 \(p(x)\) 的随机变量?

注意到 rand() 函数的值域是 \([0, 1]\),我们可以把这个函数的意义理解成:如果生成了 \(x \in [0, 1]\),这意味着生成比 \(x\) 小的数的概率是 \(x\),那么我们应该建立一个映射 \(g: x \mapsto y\),使得生成的满足该函数的随机变量小于 \(y\) 的概率是 \(x\)。也就是 \(P(x)\big |_ {-\infty}^y = x\)。假设这样的 \(x\) 有界,那么 \(P(x)\big |_ {-\infty} = 0\),即 \(P(y) = x\)。则 \(y = g(x) = P^{-1}(x)\)。

用 PDF 估算积分值

我们可以运用它来估算积分值。\(\int_a^b f(x){\rm d} x\) 可以看成 \((b - a) \cdot {\rm average}(f(x))\),其中 average 可以通过采很多次样,对结果求和后除以采样次数的方法求出。但是有一个问题:在采非常多次样之后,和一定会收敛,但是如果我们想用尽量少的采样次数让和收敛更快呢?我们希望函数值(的绝对值)大的时候采样次数多一些,函数值小的时候可以少采样,这样收敛就会更快。这时 pdf 就要派上用场了。然而之前的平均数需要改成加权平均。假设我们按照原来的方法均匀采 \(n\) 次样,那么积分可以表示成

\[\sum \dfrac{f(x_i)}{\frac{n}{b - a}} \]

联想到均匀采样时 \(p(x) \equiv \dfrac{1}{b - a}\),那么积分值可以表示成

\[\dfrac 1 n \sum \dfrac{f(x_i)}{p(x_i)} \]

PDF 在光线追踪中的应用

说了这么多数学方法,现在我们来考察它在光线追踪的应用。

本书中经常用到曲面上的积分,一般默认为第一类曲面积分(后面跟一个 \({\rm d}A = \sin \theta {\rm d} \theta {\rm d} \varphi\))(书上的 \(\theta\) 和 \(\varphi\) 跟平常用法是反过来的)

我们考察前面提到的得出散射光线颜色的方法。传统的方法会用到随机向量来表示散射光线。现在我们希望用一个概率密度函数来表征各个方向上散射光线出现的概率,我们用 \(s({\rm direction}): \mathbb R^3 \to \mathbb R\) 来表示它。然后,每次散射都会使得 R, G, B 的光强按一定比例减弱,这就是之前的 attenuation。对于每个方向,我们也要考察这个方向的 ray_color,相当于递归调用。将连续求和表达成积分形式,得到

\[Color = A\int s({\rm direction})\cdot {\rm color}({\rm direction}) \]

联系到上述估算积分的方法,得到

\[Color = A\sum \dfrac{s({\rm direction})\cdot {\rm color}({\rm direction})}{p({\rm direction})} \]

Lambertian 的 scattering pdf

lambertian 的散射结果是由 normal + random_unit_vector 生成的。我们给出 lambertian 材料的散射概率密度函数 \(s(\theta, \varphi) \propto \cos \theta\)。

我们对它做第一类曲面积分:

\[\int \cos \theta {\rm d} A = \int_{[0, \pi] \times [0, 2\pi]} \cos \theta \sin \theta {\rm d}\theta {\rm d}\varphi = \pi \]

则 \(s(\theta, \varphi) = \dfrac 1 \pi \cos \theta\)。

如果我们取 \(p(\theta) = s(\theta)\),那么 \(Color = A\sum {\rm color}({\rm direction})\),跟未采用 pdf 的方法无异。但是我们还是希望能够给更重要的方向(例如,指向光源)更多权重,从而减小噪点。

反射光的分布可以用 BRDF (bidirectional reflectance direction function) \(\frac{A \cdot s({\rm direction})}{\cos(\theta)}\) 表示,则对于 lambertian 材料,\(BRDF = \dfrac A \pi\)。

Code:

// in func. ray_color
let mut pdf = 0.0;
    let mut albedo: Color3 = Color3::new();
    if !rec
        .mat_ptr
        .as_ref()
        .unwrap()
        .scatter(r, &rec, &mut albedo, &mut scattered, &mut pdf)
    {
        return emitted;
    }
    emitted
        + albedo
            * rec
                .mat_ptr
                .as_ref()
                .unwrap()
                .scattering_pdf(r, &rec, &scattered)
            * ray_color(&scattered, background, world, depth - 1)
            / pdf
			
	// in impl Material for Lambertian
	fn scatter(
        &self,
        r_in: &Ray,
        rec: &HitRecord,
        alb: &mut Color3,
        scattered: &mut Ray,
        pdf: &mut f64,
    ) -> bool {
        let direction = random_in_hemisphere(&rec.normal);
        *scattered = Ray::construct(&rec.p, &direction.unit(), r_in.time());
        *alb = self.albedo.value(rec.u, rec.v, &rec.p);
        *pdf = 0.5 / PI;
        true
    }
    fn scattering_pdf(&self, _r_in: &Ray, rec: &HitRecord, scattered: &Ray) -> f64 {
        let cosine: f64 = dot(&rec.normal, &scattered.direction().unit());
		// 即为求 pdf,上文已提到 cos theta / pi
        if cosine < 0.0 {
            0.0
        } else {
            cosine / PI
        }
    }

对光源采样以及混合 PDF

注意到将多个概率密度函数线性组合后,它依然是一个合法的 pdf,所以我们可以采用以下策略:

总的 pdf 由 \(\dfrac 1 2\) 的对向光源的 pdf 与 \(\dfrac 1 2\) 的与上文的 \(s({\rm direction})\) pdf 相加得到。下面我们依然以 lambertian 为例求出对光源采样的 pdf,从而得到混合 pdf。

这里的光源是一个矩形,但是我们要生成一个与 \(\theta, \varphi\) 相关的 pdf,因此我们需要转化一下。

change pdf

在这个矩形中打到 \({\rm d}A\) 和在(半)球上打到 \({\rm d}w\) 的概率是一样的,又

\[{\rm d}w = \frac{{\rm d}A \cdot \cos(\alpha)}{\left|PQ\right|} \]

其中 \(\alpha\) 为光线方向与发光面法向量(这里是 \(y\) 轴方向)的夹角。

又

\[p({\rm direction}) {\rm d} w = p_q(q) {\rm d} A = \dfrac {{\rm d}A}{A} \]

得

\[p({\rm direction}) = \dfrac{\left|PQ\right|}{A\cos \alpha} \]

参考资料

Ray Tracing: The Next Week
Ray Tracing: The Rest of Your Life

posted @ 2023-07-21 14:20  gznpp  阅读(153)  评论(0)    收藏  举报