Tizeng's blog Ordinary Gamer

光线追踪学习笔记3——添加球

2019-03-17
Tizeng

前面定义了vec3这个类来储存三维向量的空间坐标,但它其实还有另一个用处,就是储存彩色像素点的RGB值,它同样是三维的,因此看到vec3定义的变量即有可能是空间点的坐标,又有可能是像素值,注意不要混淆。

Chapter 4: Adding a sphere

ray中有一个参数t,它不是其中的成员变量,因为起始点和照射方向两点确定了一条直线(或者说一个向量),t是用来衡量这束光线途径位置的变量,可以理解为“时间”,即发出光线后经过时间t后所到达的位置点。

那么我们可以用t作为一个参数,来判断光线是否到达了球体。在三维空间中球体的方程为:

(x-cx) * (x-cx) + (y-cy) * (y-cy) + (z-cz) * (z-cz) = R * R

圆心为(cx, cy, cz)。我们把它记为向量C,把空间中任意一点(x, y, z)记为p,则上面的方程可以用向量的点乘来表示:

dot( (p - C), (p - C) ) = R*R

由于光线在之前的定义中用向量表示为A + t * B,因此我们可以得到落在球体的光线的向量关系:

dot( (A + t * B - C), (A + t * B - C) ) = R * R

根据点积的性质,我们很容易可以将其写为如下形式:

t * t * dot(B, B) + 2t * dot(B, A - C) + dot(A - C, A - C) - R*R = 0

那么现在就得到了一个t的一元二次方程方程,ABC皆已知,通过计算delta,可以得知此时光线是否到达了球体。

bool hit_sphere(const vec3& center, float radius, const ray& r) {
    vec3 oc = r.origin() - center;
    float a = dot(r.direction(), r.direction());
    float b = 2.0 * dot(r.direction(), oc);
    float c = dot(oc, oc) - radius * radius;
    float delta = b * b - 4 * a *c;
    return delta > 0;
}

vec3 color(const ray& r) {
    // return RGB values using vec3
    if (hit_sphere(vec3(0, 0, -1), 0.5, r)) {
        return vec3(1, 0, 0);
    }
    vec3 unit_direction = unit_vector(r.direction());
    float t = 0.5 * (unit_direction.y() + 1.0);
    return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}

main函数不变,上色函数color加入对球的填色代码,通过调用hit_sphere函数,判断相应的像素点是否应该显示球的颜色。在加入其他代码之前这个球的颜色根据color函数中给定的颜色来决定。

Chapter 5: Surface normals and multiple objects

这里 normal 表示法线,为了加入阴影,我们需要得到球面的法线坐标。这里我们定义法线的长度始终为单位长度,方向向外,具体来说就是球面点的向量减去球心向量。

在加入更复杂的阴影系统前,我们用色盘来显示法线信息,若光线和球有交点,先计算该法线的方向并将其单位化,此时各个坐标的范围落在[-1, 1],为了将它们直接映射成RGB值,需要先将范围改到[0, 1]:

float hit_sphere(const vec3& center, float radius, const ray& r) {
    vec3 oc = r.origin() - center;
    float a = dot(r.direction(), r.direction());
    float b = 2.0 * dot(r.direction(), oc);
    float c = dot(oc, oc) - radius * radius;
    float delta = b * b - 4 * a *c;
    if (delta >= 0)
        return (-b - sqrt(delta)) / (2.0 * a);
    else
        return -1;
}

vec3 color(const ray& r) {
    // return RGB values using vec3
    float t =  hit_sphere(vec3(0, 0, -1), 0.5, r);
    if (t > 0)) {
        ve3 N = unit_vector(r.point_at_parameter(t) - vec3(0, 0, -1));
        return 0.5 * vec3(N.x() + 1, N.y() + 1, N.z() + 1);
    }
    vec3 unit_direction = unit_vector(r.direction());
    float t = 0.5 * (unit_direction.y() + 1.0);
    return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}

输出结果如下:

sphere1

多个物体

如果我们需要多个物体都显示在摄像机的范围内,就需要创建一个抽象类hitable,任何需要显示的类都继承自这个类,还需要一个结构体来储存法线和坐标信息。

struct hit_record {
    float t;
    vec3 p;
    vec3 normal;
};

class hitable {
public:
    virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const = 0;
};

接下来我们将之前的代码修改为显示的球继承自hitable的实现方法,每个子类都要重写hit函数,用以之后的着色判断,在方程有解,也就是坐标在球上时,判断其t是否在[t_min, t_max]范围内,如果在,那么更新法线信息等至rec,返回true

class sphere :public hitable {
public:
    sphere() {}
    sphere(vec3 cen, float t): center(cen), radius(t) {};
    virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
private:
    vec3 center;
    float radius;
};

bool sphere::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
    vec3 oc = r.origin() - center;
    float a = dot(r.direction(), r.direction());
    float b = dot(r.direction(), oc);
    float c = dot(oc, oc) - radius * radius;
    float delta = b * b - a * c;
    if (delta > 0) {
        float temp = (-b - sqrt(b*b - a * c)) / a;
        if (temp < t_max && temp > t_min) {
            rec.t = temp;
            rec.p = r.point_at_parameter(rec.t);
            rec.normal = (rec.p - center) / radius;
            return true;
        }
        temp = (-b + sqrt(b*b - a * c)) / a;
        if (temp < t_max && temp > t_min) {
            rec.t = temp;
            rec.p = r.point_at_parameter(rec.t);
            rec.normal = (rec.p - center) / radius;
            return true;
        }
    }
    return false;
}

除此之外还需要一个储存当前需显示物体信息的类hitable_list,它的主要功能除了存储对象之外,还在重写的hit函数中,通过更新一个最小值cloest_so_far,在储存的所有物体中,计算离摄像机最近(t最小)的物体是哪一个对象,同时调用对象本身的hit函数:

class hitable_list :public hitable {
public:
    hitable_list() {}
    hitable_list(hitable **l, int n) : list(l), list_size(n) {};
    virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
private:
    hitable **list;
    int list_size;
};

bool hitable_list::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
    hit_record temp_rec;
    bool is_hit = false;
    double cloest_so_far = t_max;
    for (int i = 0; i < list_size; i++) {
        if (list[i]->hit(r, t_min, cloest_so_far, temp_rec)) {
            is_hit = true;
            cloest_so_far = temp_rec.t;
            rec = temp_rec;
        }
    }
    return is_hit;
}

最后是修改过的主函数和着色函数,在着色前先建立两个球体,并保存信息至一个hitable_list对象world,挨个对像素点着色时,调用color函数,此时接受的变量是光线r和视野中的物体列表world,对其调用hit,只要其不为空就会返回true,并将物体的颜色等信息保存至rec,最后调用rec中的法线信息对其进行着色:

vec3 color(const ray& r, hitable *world) {
    // return RGB values using vec3
    hit_record rec;
    if (world->hit(r, 0.0, FLT_MAX, rec)) {
        return 0.5 * vec3(rec.normal.x() + 1, rec.normal.y() + 1, rec.normal.z() + 1);
    }
    else {
        vec3 unit_direction = unit_vector(r.direction());
        float t = 0.5 * (unit_direction.y() + 1.0);
        return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
    }
}

int main() {
    int nx = 200;
    int ny = 100;
    fstream fs;
    fs.open("test.ppm", std::fstream::in | std::fstream::out | fstream::trunc);
    //cout << "P3\n" << nx << " " << ny << "\n255\n";
    fs << "P3\n" << nx << " " << ny << "\n255\n";
    vec3 lower_left_corner(-2.0, -1.0, -1.0);
    vec3 horizontal(4.0, 0.0, 0.0);
    vec3 vertical(0.0, 2.0, 0.0);
    vec3 origin(0.0, 0.0, 0.0);
    hitable *list[2];
    list[0] = new sphere(vec3(0, 0, -1), 0.5);
    list[1] = new sphere(vec3(0, -100.5, -1), 100);
    hitable_list *world = new hitable_list(list, 2);
    for (int j = ny - 1; j >= 0; j--) {
        for (int i = 0; i < nx; i++) {
            float u = float(i) / float(nx);
            float v = float(j) / float(ny);
            ray r(origin, lower_left_corner + u * horizontal + v * vertical);

            vec3 p = r.point_at_parameter(2.0);
            vec3 col = color(r, world);
            int ir = int(255.99 * col[0]);
            int ig = int(255.99 * col[1]);
            int ib = int(255.99 * col[2]);
            //cout << ir << " " << ig << " " << ib << "\n";
            fs << ir << " " << ig << " " << ib << "\n";
        }
    }
    fs.close();
    return 0;
}

修改后的输出结果如下,可以看到屏幕中有上下两个一大一小的球体:

sphere2


Comments

Content