《Ray Tracing in One Weekend》阅读笔记 - 5、表面法线和多个物体

对于表面法向量的设计决策,有两个要考虑的问题:

1、是否要用单位长度

首先是这些法线是否为单位长度。这对于阴影很方便,所以我会说yes,但我不会在代码中执行它。这可能会出现一些细微的bug,所以要注意这是个人偏好,就像大多数类似的设计决策一样。

2、是法向量是否要指向球外。

目前,找到的法线总是在中心向交点的方向(法线指向外)。如果射线从外部与球面相交,法线与射线相交(the normal points against the ray。如果射线从内部与球面相交,法线(法线总是指向外的)与射线相交(points with the ray)。或者,我们可以让法线总是指向射线。如果射线在球面外,法线指向外,但如果射线在球面内,法线指向内。

 

5.1 用表面法向量着色   

    为了着色,我们先找到表面法向量。对于一个球体,要着色的点上的表面法向量为:射线与球的交点P减去球心CP-C

    我们现在还没有灯光等东西,所以用颜色图来着色法向量。发现可视化的常用技巧是:假设法向量是单位长度(每个分量在-1和1之间),在将其映射到0到1之间,用x、y、z的值分别表示r、g、b的值。

    对于求解法向量,我们除了知道是否击中球外,还要知道击中的位置。这里假设击中的点是t(较小的一个)对应的点,接下来可视化这些点:

// 我的代码 main.cc

// 返回击中的点对应的t值

double hit_sphere(const ray& r, const point3& center, double radius) {

vec3 oc = r.origin() - center;

auto a = dot(r.direction(), r.direction());

auto b = 2.0 * dot(r.direction(), oc);

auto c = dot(oc, oc) - radius * radius;

auto deta = b * b - 4 * a * c;

if (deta >= 0) {

return (-b - sqrt(deta)) / (2.0 * a);

}

else {

return -1;

}

}

 

color ray_color(const ray& r) {

double t = hit_sphere(r, point3(0, 0, -1), 0.5);

if (t > 0.0) {

// 获得击中点处向外的单位法向量

vec3 unit_normal = unit_vector(r.at(t) - point3(0, 0, -1));

// 将向量各分量映射到 [0,1]

return (0.5 * color(unit_normal.x() + 1, unit_normal.y() + 1, unit_normal.z() + 1));

}

// 获得r的方向向量并转化为单位向量(此时向量的范围为[-1.0, 1.0])

vec3 unit_direction = unit_vector(r.direction());

// 将r的y方向上的向量归一化到[0.0, 1.0],用以线性插值

 t = 0.5 * (unit_direction.y() + 1.0);

// 利用线性插值函数:

// blendedValue = (1−t)⋅startValue + t⋅endValue

// 从 t = 0.0 输出白色,渐变到 t = 1.0输出蓝色

return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);

}

5.2 简化射线-球面的相交代码

    注意到b的方程中有一个因子2。考虑b = 2h时的二次方程:

   所以我们可以将求交代码简化为:

// 我的代码 main.cc

// 返回击中的点对应的t值

double hit_sphere(const ray& r, const point3& center, double radius) {

vec3 oc = r.origin() - center;

auto a = dot(r.direction(), r.direction());

auto half_b = dot(r.direction(), oc);

auto c = dot(oc, oc) - radius * radius;

auto deta = half_b * half_b -  a * c;

if (deta >= 0) {

return (-half_b - sqrt(deta)) / a;

}

else {

return -1;

}

}

 

5.3 Hittable对象的抽象

    对渲染许多个球体,一个非常简洁的方法是为每条光线可能的命中对象创建一个抽象类hittable(或者命名为object类可能更合适,如果我们不说这是“object oriente”),并创建一个球体和一个球体列表。

    hittable抽象类里面有一个hit方法,该方法的参数包括一个ray,t的范围:t_max和t_min,和用来记录射线交点结果的hit_recoder结构。

  • 当射线击中较近物体时,我们将停止搜索,只计算最近的交点处的法线。
  • 只有当求解出的t 在t_min到t_max范围内时,才算hit到目标,否则无效
  • hit_recoder里记录的是:射线与表面的交点p,

                                      交点处单位法向量normal,

                                      交点对应的t。

代码如下:

// 网站上的代码 hittable.h

#ifndef HITTABLE_H

 

#include "ray.h"

 

struct hit_record {

point3 p;

vec3 normal;

double t;

};

 

class hittable {

public:

virtual bool hit(const ray& r, double t_min, double t_max, hit_record & rec) const = 0;

};

 

#endif // !HITTABLE_H 

 这里还有一个sphere类,继承了hittable类,有center球心属性和radius半径属性。

利用上面的公式,重写hit方法,代码如下:

// 我的代码 sphere.h

#pragma once

#ifndef SPHERE_H

#define SPHERE_H

 

#include "hittable.h"

#include "vec3.h"

 

class sphere : public hittable {

public:

sphere() {};

sphere(point3 cen, double r) :center(cen), radius(r) {};

 

virtual bool hit(const ray& r, double t_min, double t_max, hit_record & rec)const;

 

private:

point3 center;

double radius;

};

 

bool sphere::hit(const ray& r, double t_min, double t_max, hit_record & rec) const {

vec3 oc = r.origin() - center;

auto a = dot(r.direction(), r.direction());

auto half_b = dot(r.direction(), oc);

auto c = dot(oc, oc) - radius * radius;

auto deta = half_b * half_b - a * c;

 

if(deta > 0) { // 命中

// 先判断较小的t是否命中,如果不命中就判断大的t

auto sqrt_deta = sqrt(deta);

double t = (-half_b - sqrt_deta) / a;

if (t < t_max && t > t_min) { // 有效

rec.t = t;

rec.p = r.at(t);

rec.normal = (rec.p - center) / radius;

return true;

}

t = (-half_b + sqrt_deta) / a;

if (t < t_max && t > t_min) { // 有效

rec.t = t;

rec.p = r.at(t);

rec.normal = (rec.p - center) / radius;

return true;

}

}

// 超出范围,无效 或 没有命中

return false;

 

}

 

#endif // !SPHERE_H

 

5.4 正面和背面 

    我们要从上面的球面法向几何的可能方向中找到一个,因为我们最终要确定射线来自曲面的哪一边。这对于在不同面上渲染的情况不同得到物体很重要,如双面纸上的字、玻璃球等。

    如果我们决定让球面法向始终指向外面,我们就要确定上色时光线在哪边。我们可以通过比较射线和法线算出来。如果射线和法向面方向相同,射线在物体内部,如果射线和法向面方向相反,射线在物体外部。这可以通过取这两个向量的点积来确定,如果它们的点积是正的,射线就在球面内。

// [sphere.h] Comparing the ray and the normal

 

if (dot(ray_direction, outward_normal) > 0.0) {
    // ray is inside the sphere
    ...
} else {
    // ray is outside the sphere
    ...
}

If we decide to have the normals always point against the ray,我们就不能用点积来确定射线在曲面的哪一边。相反,我们需要存储这些信息:

// [sphere.h] Remembering the side of the surface

bool front_face;
if (dot(ray_direction, outward_normal) > 0.0) {
    // ray is inside the sphere
    normal = -outward_normal;
    front_face = false;
}
else {
    // ray is outside the sphere
    normal = outward_normal;
    front_face = true;
}

    (直译)我们可以让法线总是指向外,或者总是指向入射光线。这个决定取决于您是想在几何相交时确定曲面的边(the side of the surface),还是在着色时确定曲面的边。在这本书中,我们的材料类型多于几何类型,所以我们选择做更少的工作,并在几何相交时确定曲面的边。这只是一个偏好问题,您将在文献中看到这两种实现。

    我们在front_face中加入了bool型变量front_face到hit_record中,并且还添加了一个内联函数set_face_normal计算normal。

// [hittable.h] The hittable class with time and side

#ifndef HITTABLE_H
#define HITTABLE_H

#include "ray.h"

struct hit_record {
    point3 p;
    vec3 normal;

        bool front_face;

        inline void set_face_normal(const ray& r, const vec3& outward_normal) {
        front_face = dot(r.direction(), outward_normal) < 0;
        normal = front_face ? outward_normal :-outward_normal;
    }

};

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

#endif

// 我的代码 hittable.h

#pragma once

#ifndef HITTABLE_H

 

#include "ray.h"

 

struct hit_record {

point3 p; // 交点

vec3 normal; // 交点处单位法向量

double t; // 交点对应的射线的t值(网站上的代码好像漏了)

bool front_face; // 射线是否在物体外部,true = 是,false = 否

 

inline void set_face_normal(const ray & r, const vec3& outward_normal) {

front_face = dot(r.direction(), outward_normal);

normal = front_face ? outward_normal : -outward_normal;

}

};

 

class hittable {

public:

virtual bool hit(const ray& r, double t_min, double t_max, hit_record & rec) const = 0;

};

 

#endif // !HITTABLE_H

然后我们把surface side的确定加入到类中:

// 我的代码 sphere.h

bool sphere::hit(const ray& r, double t_min, double t_max, hit_record & rec) const {

vec3 oc = r.origin() - center;

auto a = dot(r.direction(), r.direction());

auto half_b = dot(r.direction(), oc);

auto c = dot(oc, oc) - radius * radius;

auto deta = half_b * half_b - a * c;

 

if(deta > 0) { // 命中

// 先判断较小的t是否命中,如果不命中就判断大的t

auto sqrt_deta = sqrt(deta);

double t = (-half_b - sqrt_deta) / a;

if (t < t_max && t > t_min) { // 有效

rec.t = t;

rec.p = r.at(t);

vec3 outward_normal = (rec.p - center) / radius;

rec.set_face_normal(r, outward_normal);

return true;

}

t = (-half_b + sqrt_deta) / a;

if (t < t_max && t > t_min) { // 有效

rec.t = t;

rec.p = r.at(t);

vec3 outward_normal = (rec.p - center) / radius;

rec.set_face_normal(r, outward_normal);

return true;

}

}

// 超出范围,无效 或 没有命中

return false;

}

 

5.5 可命中对象的列表

我们已经拥有了通用的hittable对象用来计算和记录射线和物体相交的结果,现在添加一个类hittable_list,用来存储一系列的hittable。

Hittable_list类继承hittable类,包含的属性是一个存放hittable类型的对象的向量vector<shared_ptr<hittable>> objects,拥有的方法是:

  • 默认初始化,带参数的初始化
  • 清空object里的元素clear()
  • 向object里添加元素add(…)
  • 判断光线是否击中物体hit(…)
    • 设置标志位hit_anything记录objects里的物体是否被击中。
    • 记录到目前为止最近的交点的t的值:closest_so_far
    • 遍历objects里的物体,判断是否有交点,有的话将hit_anything设置为true,并记录交点信息到rec中,更新t_max为closest_so_far。
    • 返回hit_anything

代码如下:

#pragma once

#ifndef HITTABLE_LIST_H

#define HITTABLE_LIST_H

 

#include "hittable.h"

 

#include <memory>

#include <vector>

 

using std::shared_ptr;

using std::make_shared;

 

class hittable_list : public hittable {

public:

hittable_list() {}

hittable_list(shared_ptr<hittable>object) { add(object); }

 

void clear() { objects.clear; }

void add(shared_ptr<hittable> object) { objects.push_back(object); }

// 判断光线是否击中物体,是的话返回的rec是最近的交点的信息

virtual bool hit(const ray&r, double t_min, double t_max, hit_record & rec) const;

 

private:

std::vector<shared_ptr<hittable>> objects;

};

 

bool hittable_list::hit(const ray&r, double t_min, double t_max, hit_record & rec) const {

bool hit_anything = false;

double closest_so_far = t_max;

hit_record temp_rec;

 

for (const auto& object : objects) {

if(object->hit(r,t_min, closest_so_far, temp_rec)){ // 击中物体

closest_so_far = temp_rec.t;

rec = temp_rec;

hit_anything = true;

}

}

 

return hit_anything;

}

 

#endif // !HITTABLE_LIST_H

5.6 一些新的c++特性

上面的hittable_list使用了两个C++新特性:vector和shared_ptr。

vector是一个可以存放任何类型的类数组集合,它的长度随元素的个数自动增长。

shared_ptr是一个指向某些已分配类型的指针,具有引用计数语义。每次将其值赋给另一个共享指针(通常使用简单的赋值)时,引用计数都会递增。当共享指针超出范围(比如在块或函数的末尾)时,引用计数就会递减。一旦计数变为零,对象就会被删除。(直译)

 

5.7 常用常量和实用函数

我们需要一些数学常数,可以方便地将他们定义在头文件中。

现在我们在头文件rtweekend.h中定义:

  • Infinity
  • Pi
  • 角度转弧度函数:degrees_to_redians

#ifndef RTWEEKEND_H
#define RTWEEKEND_H

#include <cmath>
#include <cstdlib>
#include <limits>
#include <memory>

// Usings

using std::shared_ptr;
using std::make_shared;
using std::sqrt;

// Constants

const double infinity = std::numeric_limits<double>::infinity();
const double pi = 3.1415926535897932385;

// Utility Functions

inline double degrees_to_radians(double degrees) {
    return degrees * pi / 180;
}

// Common Headers

#include "ray.h"
#include "vec3.h"

#endif

利用我们写的类,接下来重写一下我们的main函数。我们再来回顾一下ray tracing的思路。

1、设置图片宽高

2、设置屏幕宽高、水平分量和垂直分量

3、在场景中添加物体

4、遍历从相机到图片上每一个像素的射线,计算与物体的相交情况,并着色(用某种规则将向量映射到RGB上),最后打印颜色。

现在可以开始重写了:

// 我的代码  main.cc

#include "rtweekend.h"

#include "hittable_list.h"

#include "sphere.h"

 

color ray_color(const ray& r, const hittable& world) {

// 记录射线与物体的相交结果

hit_record rec;

if (world.hit(r, 0 , infinity, rec)) {

// 将向量各分量映射到 [0,1]

return (0.5 * (color(1,1,1) + rec.normal));

}

 

// 获得r的方向向量并转化为单位向量(此时向量的范围为[-1.0, 1.0])

vec3 unit_direction = unit_vector(r.direction());

// 将r的y方向上的向量归一化到[0.0, 1.0],用以线性插值

auto t = 0.5 * (unit_direction.y() + 1.0);

// 利用线性插值函数:   blendedValue = (1−t)⋅startValue + t⋅endValue

// 从 t = 0.0 输出白色,渐变到 t = 1.0输出蓝色

return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);

}

 

int main() {

// 设置图片宽高比,宽度、高度

const auto aspect_ratio = 16.0 / 9.0;

const int image_width = 384;

const int image_height = static_cast<int>(image_width / aspect_ratio);

 

std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";

// 设置屏幕宽、高、深

auto screen_height = 2.0;

auto screen_width = screen_height * aspect_ratio;

auto deep = 1.0;

 

// 左下角origin、u、v

point3 origin = point3(0, 0, 0);

vec3 horizontal = vec3(screen_width, 0, 0);

vec3 vertical = vec3(0, screen_height, 0);

point3 lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, deep);

 

// 在场景中添加物体

hittable_list world;

world.add(make_shared<sphere>(point3(0, 0, -1), 0.5));

world.add(make_shared<sphere>(point3(0, -100.5, -1), 100));

        

        // 遍历射线,打印颜色

for (int j = image_height - 1; j >= 0; j--) {

std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;

for (int i = 0; i < image_width; i++) {

// 从相机到屏幕上每个像素的射线pixel_pos = u + v + lower_left_corner

auto pixel_pos = horizontal * (double(i) / (image_width - 1))

+ vertical * (double(j) / (image_height - 1))

+ lower_left_corner;

color pixel_color = ray_color(ray(origin, pixel_pos),world);

out_color(std::cout, pixel_color);

}

std::cerr << "\nDone.\n";

}

}

锵锵!结果是这样:

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值