在线查看器

参考教程

项目代码

Chapter 1:Motion Blur

1.1 运动模糊

  • 光线多了一个属性time,用于记录该光线射入相机的时间

    class Ray {
    public:
    Ray() {};
    Ray(const Point3& orig, const Vec3& dir, const double time = 0) :origin(orig), direction(dir), time(time) {}
    Ray(const Ray& r) :origin(r.origin), direction(r.direction) {}

    Point3 Origin() const { return origin; }
    Vec3 Direction() const { return direction; }
    double Time() const { return time; }

    // P(t) = origin + t * direction
    Point3 At(double t) const {
    return origin + t * direction;
    }


    private:
    Point3 origin;
    Vec3 direction;
    double time;
    };
  • time的取值由Camera的曝光时间决定,为曝光时间内的一个随机值

    Ray Camera::GetRay(double u, double v) const {
    // 光圈随机偏移
    Vec3 rd = lens_radius * Random::rand_unit_sphere();
    Vec3 offset = u * rd.x() + v * rd.y();

    // 光线的起点为原点, 方向指向观察平面上的当前像素
    // 当前时间为 [time1, time2] 之间的随机值
    Vec3 start = position + offset;
    Vec3 target = low_left_corner + u * horizontal + v * vertical;
    double time = time1 + Random::rand01() * (time2 - time1);
    return Ray(start, target - start, time);
    }

1.2 Moving Sphere

  • 通过球心位置,确定球的位置
  • 需要给定两个时间分别对应的球心位置,某个确定时间球心所在位置,即为两者插值
  • 对于当前时刻,根据之前球的求根公式,计算交点
bool SphereMoving::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
// 根据公式判断是否相交
// (1) 球 : (P - center)^2 = radius^2
// (2) 光线 : P = origin + t * direction
// 带入求t: t^2 * direction^2 + 2t * direction * (origin - center) + (origin - center)^2 - radius^2 = 0
Point3 center = GetCenter(r.Time());
Vec3 oc = r.Origin() - center;
auto a = r.Direction().dot(r.Direction());
auto b = 2.0 * oc.dot(r.Direction());
auto c = oc.dot(oc) - radius * radius;
auto discrimination = b * b - 4 * a * c;

if (discrimination < 0) return false;

auto sqrtd = sqrt(discrimination);
auto t = (-b - sqrtd) / (2 * a);
if (t < t_min || t > t_max) {
t = (-b + sqrtd) / (2 * a);
if (t < t_min || t > t_max) return false;
}

info.t = t;
info.position = r.At(t);
info.material = material;

Vec3 outward_normal = (info.position - center) / radius; // 法线: 球心指向相交点
info.set_face_normal(r, outward_normal);

return true;
}

Point3 SphereMoving::GetCenter(double time) const {
return center1 + ((time - time1) / (time2 - time1)) * (center2 - center1);
}

1.3 场景中的物体&相机配置

namespace {
/* 图片设置 */
const int Image_Width = 400; // 图片宽度
const int Image_Height = 225; // 图片高度
Color data[Image_Width][Image_Height]; // 图片数据
int total_completed = 0; // 已完成的列数
const double aspect = 1.0f * Image_Width / Image_Height; // 宽高比

/* 世界设置 */
Color background(0.5, 0.7, 1.0);// 背景颜色
ObjectWorld world(background); // 世界中的物体

/* 相机设置 */
Vec3 from(13, 2, 3);
Vec3 at(0, 0, 0);
double dist_to_focus = 10;
double aperture = 0.0;
double time_start = 0, time_end = 1.0;
Camera main_camera(from, at, Vec3(0, 1, 0), 20, aspect, aperture, 0.7 * dist_to_focus, time_start, time_end); // 主相机
}

void AddObjects() {
// 地面
world.Add(New<Sphere>(
Point3(0, -1000, 0),
1000,
New<Lambertian>(Color(0.5, 0.5, 0.5)))
);
// 小球
for(int a = -2; a < 2; a++)
for (int b = -2; b < 2; b++) {
double choose_material = Random::rand01();
Point3 center(a + 0.9 * Random::rand01(), 0.2, b + 0.9 * Random::rand01());
if ((center - Point3(4, 0.2, 0)).length() > 0.9) {
if (choose_material < 0.55) {
world.Add(New<SphereMoving>(
center, center + Vec3(0, 0.5 * Random::rand01(), 0),
0.0, 1.0,
0.2,
New<Lambertian>(Color(Random::rand01(), Random::rand01(), Random::rand01())))
);
}
else if (choose_material < 0.85) {
world.Add(New<Sphere>(
center,
0.2,
New<Metal>(Color(0.5 * (1 + Random::rand01()), 0.5 * (1 + Random::rand01()), 0.5 * Random::rand01())))
);
}
else {
world.Add(New<Sphere>(
center,
0.2,
New<Dielectric>(1.5)));
}
}
}
// 大球
world.Add(New<Sphere>(
Point3(0, 1, 0),
1.0,
New<Dielectric>(1.5))
);
world.Add(New<Sphere>(
Point3(-4, 1, 0),
1.0,
New<Lambertian>(Color(0.4, 0.2, 0.1)))
);
world.Add(New<Sphere>(
Point3(4, 1, 0),
1.0,
New<Metal>(Color(0.7, 0.6, 0.5), 0.0))
);
}

Chapter 2:Bounding Volume Hierarchies

2.1 AABB包围盒

img

img

img

bool AABB::hit(const Ray& r, double t_min, double t_max) const {
//static int cnt = 0;
//std::cerr << "AABB hit: " << ++cnt << "\n";

// 计算XYZ三个轴的 t 值
// t1 = (min.x - origin.x) / direction.x
// t2 = (max.x - origin.x) / direction.x
for (int i = 0; i < 3; i++) {
double div = 1.0 / r.Direction()[i];
double t1 = (min[i] - r.Origin()[i]) / r.Direction()[i];
double t2 = (max[i] - r.Origin()[i]) / r.Direction()[i];
if(div < 0) std::swap(t1, t2);
if (std::min(t2, t_max) <= std::max(t1, t_min)) return false;
}
return true;
}

AABB AABB::Merge(const AABB& box1, const AABB& box2) {
Vec3 Min{
std::min(box1.Min().x(), box2.Min().x()),
std::min(box1.Min().y(), box2.Min().y()),
std::min(box1.Min().z(), box2.Min().z())
};
Vec3 Max{
std::max(box1.Max().x(), box2.Max().x()),
std::max(box1.Max().y(), box2.Max().y()),
std::max(box1.Max().z(), box2.Max().z())
};
return AABB(Min, Max);
}

2.2 构建BVH

  • 将场景中的物体编号,0~n-1
  • 类似线段树,每个阶段管辖一个区间[L,R],并根据区间内的物体建立AABB包围盒
  • 递归构建每一个节点
void ObjectWorld::build(Ref<BVHnode> u, int L, int R, int deep) {
/*std::sort(objects.begin() + L, objects.begin() + R + 1, [&](const Ref<ObjectBase>& a, const Ref<ObjectBase>& b) {
return a->GetBox().Min()[deep % 3] < b->GetBox().Min()[deep % 3];
});*/
u->L = L; u->R = R;
int size = R - L + 1;
if (size == 1) {
u->left = u->right = nullptr;
u->object = objects[L];
}
else {
int mid = (L + R) >> 1;
u->left = New<BVHnode>();
u->right = New<BVHnode>();
u->object = nullptr;
build(u->left, L, mid);
build(u->right, mid + 1, R);
}
u->Update();
}

2.3 碰撞检测

  • 如果到达叶节点,则与当前节点对应的物体进行碰撞检测
  • 首先与当前节点的AABB包围盒进行碰撞检测,如果没有碰撞,则忽略该子树
  • 然后递归判断左右子树,并返回两者中,t更小的那个
bool BVHnode::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
// 叶节点, 则判断与当前物体是否碰撞
if (left == nullptr && right == nullptr)
return object->hit(r, t_min, t_max, info);

// 内部节点, 判断与当前包围盒是否碰撞, 进行剪枝
if (!box.hit(r, t_min, t_max)) return false;

// 递归判断左右儿子
HitInfo left_info, right_info;
bool left_hit = (left != nullptr) && left->hit(r, t_min, t_max, left_info);
bool right_hit = (right != nullptr) && right->hit(r, t_min, t_max, right_info);

if (left_hit && right_hit) {
if (left_info.t < right_info.t) info = left_info;
else info = right_info;
return true;
}
else if (left_hit) {
info = left_info;
return true;
}
else if (right_hit) {
info = right_info;
return true;
}
else return false;
}

Chapter 3:Solid Textures

3.1 Texture的功能

  • 给定:纹理坐标(u,v),坐标p
  • 返回:颜色
class Texture {
public:
/*
* @brief 获取纹理颜色
* @param u 纹理坐标u
* @param v 纹理坐标v
* @param p 坐标
* @return 纹理颜色
*/
virtual Color Value(double u, double v, const Point3& p) const = 0;
};

3.2 常量纹理:见Texture/TextureConstant.h

  • Value函数返回定值
class TextureConstant : public Texture {
public:
/*
* @param color 纹理颜色
*/
TextureConstant(const Color& color) : color(color) {}

/*
* @brief 获取纹理颜色
* @param u 纹理坐标u
* @param v 纹理坐标v
* @param p 坐标
* @return 纹理颜色
*/
virtual Color Value(double u, double v, const Point3& p) const override;

private:
Color color;
};

Color TextureConstant::Value(double u, double v, const Point3& p) const {
return color;
}
  • 修改材质类:attenuation的值由纹理返回
class Lambertian : public Material {
public:
/*
* @param albedo 反射率
*/
Lambertian(Ref<Texture> albedo) : albedo(albedo) {}

/*
* @brief 生成散射光线
* @param r_in 入射光线
* @param info 碰撞信息
* @param attenuation 当发生散射时, 光强如何衰减, 分为rgb三个分量
* @param r_out 散射光线
* @return 是否得到了散射光线
*/
virtual bool scatter(const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const override;

private:
Ref<Texture> albedo;
};

bool Lambertian::scatter(const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const {
// 漫反射: 在单位圆内随机取一点, 作为反射方向
Vec3 target = info.position + info.normal + Random::rand_unit_sphere();
r_out = Ray(info.position, target - info.position);
attenuation = albedo->Value(0, 0, info.position);
return true;
}

3.3 棋盘纹理,见:Texture/TextureChecker.h

  • 棋盘纹理就是交错的双色格子,呈现一定的规律性
  • 使用正弦函数,将位置映射为[0/1]
Color TextureChecker::Value(double u, double v, const Point3& p) const {
double sines = sin(30 * p.x()) * sin(30 * p.y()) * sin(30 * p.z());
if (sines < 0) return odd->Value(u, v, p);
else return even->Value(u, v, p);
}

Chapter 4:Perlin Noise

4.1 柏林噪声

柏林噪声是用来生成一些看似杂乱无章其实有些变换规律的图形(更加贴近自然),比如海水、地形、雾等

例如,2D柏林噪声可以生成

img

柏林噪声有2个关键的特点:

  • 输入相同的3D点,总能返回相同的随机值
  • 简单快捷,使用一些hack的方法,达到快速近似的效果

4.2 柏林噪声的类,见Math/Perlin.h

  • 噪声函数noise(p):给定一个位置p,返回该位置对应的噪声函数值

    double Perlin::noise(const Point3& p) const {
    int i = floor(p.x());
    int j = floor(p.y());
    int k = floor(p.z());

    double u = p.x() - i;
    double v = p.y() - j;
    double w = p.z() - k;

    Point3 list[2][2][2];
    for(int a = 0; a < 2; a++)
    for(int b = 0; b < 2; b++)
    for(int c = 0; c < 2; c++)
    list[a][b][c] = random_value[
    perm_x[(i + a) & 255] ^
    perm_y[(j + b) & 255] ^
    perm_z[(k + c) & 255]
    ];
    return trilinear_interp(list, u, v, w);
    }
  • 噪声函数turb(p, depth):给定一个位置p,返回该位置对应的复合噪声函数值

    double Perlin::turb(const Point3& p, int depth) const {
    double accumulate = 0;
    Point3 t = p;
    double weight = 1.0;
    for (int i = 0; i < depth; i++) {
    accumulate += weight * noise(t);
    weight *= 0.5;
    t *= 2;
    }
    return abs(accumulate);
    }
  • 三线性插值函数trilinear_interp(list, u, v, w):根据uvw进行插值

    double Perlin::trilinear_interp(Point3 list[2][2][2], double u, double v, double w) const {
    double uu = u * u * (3 - 2 * u);
    double vv = v * v * (3 - 2 * v);
    double ww = w * w * (3 - 2 * w);

    double accumulate = 0;
    for (int i = 0; i < 2; i++)
    for (int j = 0; j < 2; j++)
    for (int k = 0; k < 2; k++) {
    Point3 weight(u - i, v - j, w - k);
    accumulate +=
    (i * uu + (1 - i) * (1 - uu)) *
    (j * vv + (1 - j) * (1 - vv)) *
    (k * ww + (1 - k) * (1 - ww)) * list[i][j][k].dot(weight);
    }
    return accumulate;
    }

4.3 噪声纹理,见Texture/TextureNoise.h

class TextureNoise : public Texture {
public:
/*
* @param scale 噪声缩放
*/
TextureNoise(double scale = 1.0) : scale(scale) {}

/*
* @brief 获取纹理颜色
* @param u 纹理坐标u
* @param v 纹理坐标v
* @param p 坐标
* @return 纹理颜色
*/
virtual Color Value(double u, double v, const Point3& p) const override;

private:
Perlin noise;
double scale;
};

Color TextureNoise::Value(double u, double v, const Point3& p) const {
return Vec3(1, 1, 1) * 0.5 * (1 + sin(scale * p.z() + 10 * noise.turb(p)));
}

Chapter 5:Image Texture Mapping

5.1 基础知识

利用2D纹理坐标(u,v),确定当前点的attenuation(u,v)需要保存到HitInfo

  • nx*ny图像上的某个像素点(i,j)的纹理坐标: \[ u=\frac{i}{nx-1}, \ v=\frac{j}{ny-1} \]

  • 球面上的某点(x,y,z) / (r,θ,φ)的纹理坐标: \[ \begin{aligned} x&=\cos θ \cos Φ \\ z&=\cos θ \sin Φ \\ y&=\sin θ \end{aligned} \]

    • θ,φ规格化到[0,1]

    \[ u=\frac{Φ}{2\pi}, \ v=\frac{θ}{\pi}\\ \]

    img

5.2 对Sphere的碰撞的修改

bool Sphere::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
// 根据公式判断是否相交
// (1) 球 : (P - center)^2 = radius^2
// (2) 光线 : P = origin + t * direction
// 带入求t: t^2 * direction^2 + 2t * direction * (origin - center) + (origin - center)^2 - radius^2 = 0
Vec3 oc = r.Origin() - center;
auto a = r.Direction().dot(r.Direction());
auto b = 2.0 * oc.dot(r.Direction());
auto c = oc.dot(oc) - radius * radius;
auto discrimination = b * b - 4 * a * c;

if (discrimination < 0) return false;

auto sqrtd = sqrt(discrimination);
auto t = (-b - sqrtd) / (2 * a);
if (t < t_min || t > t_max) {
t = (-b + sqrtd) / (2 * a);
if (t < t_min || t > t_max) return false;
}

info.t = t;
info.position = r.At(t);
info.material = material;
GetUV((info.position - center) / radius, info.u, info.v); // 计算uv(球心为原点)

Vec3 outward_normal = (info.position - center) / radius; // 法线: 球心指向相交点
info.set_face_normal(r, outward_normal);

return true;
}

void Sphere::GetUV(const Point3& p, double& u, double& v) const {
// x = cos(theta) * cos(phi)
// z = cos(theta) * sin(phi)
// y = sin(theta)
double phi = atan2(p.z(), p.x()) + PI;
double theta = asin(p.y()) + PI / 2;
u = phi / (2 * PI);
v = theta / PI;
}

5.3 图片纹理,见Texture/TextureImage

class TextureImage : public Texture {
public:
/*
* @param path 图片路径
*/
TextureImage(std::string path);

~TextureImage();

/*
* @brief 获取纹理颜色
* @param u 纹理坐标u
* @param v 纹理坐标v
* @param p 坐标
* @return 纹理颜色
*/
virtual Color Value(double u, double v, const Point3& p) const override;

private:
Color* image_data;
size_t size_x, size_y;
};
#include "TextureImage.h"
#include <algorithm>
#define STB_IMAGE_IMPLEMENTATION
#include "../3rdpatry/stb_image.h"

TextureImage::TextureImage(std::string path) {
int nx, ny, nn;
unsigned char* tex_data = stbi_load(path.c_str(), &nx, &ny, &nn, 0);
if(tex_data == nullptr) {
std::cerr << "ERROR: 图片加载失败, 路径为: " << path << ".\n";
exit(-1);
}

size_x = nx;
size_y = ny;
image_data = new Color[size_x * size_y];
for(int i = 0; i < size_x; i++)
for (int j = 0; j < size_y; j++) {
double r = tex_data[nn * (i + j * size_x)] / 255.0;
double g = tex_data[nn * (i + j * size_x) + 1] / 255.0;
double b = tex_data[nn * (i + j * size_x) + 2] / 255.0;
image_data[i + j * size_x] = Color(r, g, b);
}
}

TextureImage::~TextureImage() {
delete[] image_data;
}

Color TextureImage::Value(double u, double v, const Point3& p) const {
int i = std::clamp<int>(u * size_x, 0, size_x - 1);
int j = std::clamp<int>(v * size_y, 0, size_y - 1);
return image_data[i + j * size_x];
}

Chapter 6:Rectangles and Lights

6.1 发光材质,见Material/Emit

class Emit : public Material {
public:
/*
* @param emit 自发光纹理
*/
Emit(Ref<Texture> emit) : emit(emit) {}

/*
* @brief 生成散射光线
* @param r_in 入射光线
* @param info 碰撞信息
* @param attenuation 当发生散射时, 光强如何衰减, 分为rgb三个分量
* @param r_out 散射光线
* @return 是否得到了散射光线
*/
virtual bool scatter(const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const override;

/*
* @brief 自发光
* @param u uv坐标
* @param v uv坐标
* @param p 碰撞点
*/
virtual Color emitted(double u, double v, const Point3& p) const override;

private:
Ref<Texture> emit;
};
bool Emit::scatter(const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const {
// 光线到达光源之后, 就不再散射了
return false;
}

Color Emit::emitted(double u, double v, const Point3& p) const {
return emit->Value(u, v, p);
}

相应的,修改GetColor()

Color ObjectWorld::GetColor(const Ray& r, int depth) {
HitInfo record;

// 如果碰撞到了, 则根据材质计算反射光线
if (this->hit(r, 0, INFINITY, record)) {
Ray scattered;
Color attenuation;
Color emit = record.material->emitted(record.u, record.v, record.position);
if (depth < max_depth && record.material->scatter(r, record, attenuation, scattered))
return emit + attenuation * GetColor(scattered, depth + 1);
else
return emit;
}
// 如果不相交, 则返回黑色
else {
return Color(0.0f);
}
}

6.2 长方形物体(平行于坐标轴轴)

以平行于z轴为例,平行于x、y轴同理

img \[ p(t)=position+t*direction\\ p(t).z=position.z+t*direction.z=k\\ t=\frac{k-position.z}{direction.z} \]

class RectXY : public Object {
public:
/*
* @param x1 x 轴最小值
* @param x2 x 轴最大值
* @param y1 y 轴最小值
* @param y2 y 轴最大值
* @param k z 轴值
* @param material 材质
*/
RectXY(double x1, double x2, double y1, double y2, double k, Ref<Material> material)
: x1(x1), x2(x2), y1(y1), y2(y2), k(k), material(material) {}

/*
* @brief 判断光线是否与当前对象碰撞
* @param r 光线
* @param t_min 光线的最小 t 值
* @param t_max 光线的最大 t 值
* @param info 碰撞点信息
* @return 是否碰撞
*/
virtual bool hit(const Ray& r, double t_min, double t_max, HitInfo& info) const override;

/*
* @brief 获取当前对象的包围盒
*/
virtual AABB GetBox() const override;

private:
Ref<Material> material;
double x1, x2, y1, y2, k;
};
bool RectXY::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
double t = (k - r.Origin().z()) / r.Direction().z();
if (t < t_min || t > t_max) return false;

Point3 p = r.At(t);
if (p.x() < x1 || p.x() > x2) return false;
if (p.y() < y1 || p.y() > y2) return false;

info.t = t;
info.position = p;
info.set_face_normal(r, Vec3(0, 0, 1));
info.material = material;
info.u = (p.x() - x1) / (x2 - x1);
info.v = (p.y() - y1) / (y2 - y1);
return true;
}

AABB RectXY::GetBox() const {
return AABB(
Vec3(x1, y1, k - 0.0001),
Vec3(x2, y2, k + 0.0001)
);
}

6.3 Cornell box

void AddObjects() {
Ref<Material> red = New<Lambertian>(New<TextureConstant>(Color(0.65, 0.05, 0.05)));
Ref<Material> white = New<Lambertian>(New<TextureConstant>(Color(0.73, 0.73, 0.73)));
Ref<Material> green = New<Lambertian>(New<TextureConstant>(Color(0.12, 0.45, 0.15)));
Ref<Material> light = New<Emit>(New<TextureConstant>(Color(20, 20, 20)));

world.Add(New<RectXZ>(213, 343, 227, 332, 554, light));
world.Add(New<RectYZ>(0, 555, 0, 555, 0, red)); // 右红墙
world.Add(New<RectYZ>(0, 555, 0, 555, 555, green)); // 左绿墙
world.Add(New<RectXZ>(0, 555, 0, 555, 0, white)); // 下白墙
world.Add(New<RectXZ>(0, 555, 0, 555, 555, white)); // 上白墙
world.Add(New<RectXY>(0, 555, 0, 555, 555, white)); // 后白墙
return;
}
/* 相机设置 */
Vec3 from(278, 278, -800);
Vec3 at(278, 278, 0);
double dist_to_focus = 10;
double aperture = 0.0;
double vfov = 40.0;
double time_start = 0, time_end = 1.0;
Camera main_camera(from, at, Vec3(0, 1, 0), vfov, aspect, aperture, 0.7 * dist_to_focus, time_start, time_end); // 主相机

Chapter 7:Instance

7.1 轴对齐 Box

用六个长方形,拼成一个长方体,注意法线的朝向

class Box : public Object {
public:
/*
* @param point_min 左下顶点
* @param point_max 右上顶点
* @param material 材质
*/
Box(const Vec3& point_min, const Vec3& point_max, Ref<Material> material);

/*
* @brief 判断光线是否与当前对象碰撞
* @param r 光线
* @param t_min 光线的最小 t 值
* @param t_max 光线的最大 t 值
* @param info 碰撞点信息
* @return 是否碰撞
*/
virtual bool hit(const Ray& r, double t_min, double t_max, HitInfo& info) const override;

/*
* @brief 获取当前对象的包围盒
*/
virtual AABB GetBox() const override;
private:
Vec3 point_min, point_max;
Ref<ObjectWorld> sides;
};
Box::Box(const Vec3& point_min, const Vec3& point_max, Ref<Material> material) 
:point_min(point_min), point_max(point_max) {
sides = New<ObjectWorld>();
sides->Add(New<RectXY>(point_min.x(), point_max.x(), point_min.y(), point_max.y(), point_max.z(), material));
sides->Add(New<FlipNormal>(New<RectXY>(point_min.x(), point_max.x(), point_min.y(), point_max.y(), point_min.z(), material)));
sides->Add(New<RectXZ>(point_min.x(), point_max.x(), point_min.z(), point_max.z(), point_max.y(), material));
sides->Add(New<FlipNormal>(New<RectXZ>(point_min.x(), point_max.x(), point_min.z(), point_max.z(), point_min.y(), material)));
sides->Add(New<RectYZ>(point_min.y(), point_max.y(), point_min.z(), point_max.z(), point_max.x(), material));
sides->Add(New<FlipNormal>(New<RectYZ>(point_min.y(), point_max.y(), point_min.z(), point_max.z(), point_min.x(), material)));
}

bool Box::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
return sides->hit(r, t_min, t_max, info);
}

AABB Box::GetBox() const {
return AABB(point_min, point_max);
}

7.2 平移

在计算碰撞点的时候把 ray 往反方向移动

class Translate : public Object {
public:
Translate(Ref<Object> object, const Vec3& offset) : object(object), offset(offset) {}

/*
* @brief 判断光线是否与当前对象碰撞
* @param r 光线
* @param t_min 光线的最小 t 值
* @param t_max 光线的最大 t 值
* @param info 碰撞点信息
* @return 是否碰撞
*/
virtual bool hit(const Ray& r, double t_min, double t_max, HitInfo& info) const override;

/*
* @brief 获取当前对象的包围盒
*/
virtual AABB GetBox() const override;
private:
Ref<Object> object;
Vec3 offset;
};
bool Translate::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
Ray moved_r(r.Origin() - offset, r.Direction(), r.Time());
if (object->hit(moved_r, t_min, t_max, info)) {
info.position += offset;
return true;
}
return false;
}

AABB Translate::GetBox() const {
auto box = object->GetBox();
return AABB(box.Min() + offset, box.Max() + offset);
}

7.3 旋转

绕Z轴转: \[ x'=\cos θ * x - \sin θ * y\\ y'=\sin θ * x + \cos θ * y \] 绕Y轴转: \[ x'=\cos θ * x + \sin θ * z\\ z'=-\sin θ * x + \cos θ * z \] 绕X轴转: \[ y'=\cos θ * y - \sin θ * z\\ z'=\sin θ * y + \cos θ * z \] 在计算碰撞点的时候把 ray 往反方向旋转

以绕Y轴旋转为例

class RotateY : public Object {
public:
RotateY(double angle, Ref<Object> object);

/*
* @brief 判断光线是否与当前对象碰撞
* @param r 光线
* @param t_min 光线的最小 t 值
* @param t_max 光线的最大 t 值
* @param info 碰撞点信息
* @return 是否碰撞
*/
virtual bool hit(const Ray& r, double t_min, double t_max, HitInfo& info) const override;

/*
* @brief 获取当前对象的包围盒
*/
virtual AABB GetBox() const override;
private:

Ref<Object> object;
double sin_theta, cos_theta;
AABB box;
};
RotateY::RotateY(double angle, Ref<Object> object) : object(object) {
double radians = angle * PI / 180.0;
sin_theta = std::sin(radians);
cos_theta = std::cos(radians);

// 计算包围盒
box = object->GetBox();
Vec3 min(INFINITY, INFINITY, INFINITY);
Vec3 max(-INFINITY, -INFINITY, -INFINITY);
for(int i = 0; i < 2; i++)
for(int j = 0; j < 2; j++)
for (int k = 0; k < 2; k++) {
double x = i * box.Max().x() + (1 - i) * box.Min().x();
double y = j * box.Max().y() + (1 - j) * box.Min().y();
double z = k * box.Max().z() + (1 - k) * box.Min().z();
double newx = cos_theta * x + sin_theta * z;
double newz = -sin_theta * x + cos_theta * z;
Vec3 tester(newx, y, newz);
for (int c = 0; c < 3; c++) {
if (tester[c] > max[c]) max[c] = tester[c];
if (tester[c] < min[c]) min[c] = tester[c];
}
}
box = AABB(min, max);
}

bool RotateY::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
// 将光线的原点和方向旋转 -theta 度
Vec3 origin = r.Origin().rotateY(-sin_theta, cos_theta);
Vec3 direction = r.Direction().rotateY(-sin_theta, cos_theta);
Ray rotatedRay(origin, direction, r.Time());
if (object->hit(rotatedRay, t_min, t_max, info)) {
info.position = info.position.rotateY(sin_theta, cos_theta);
info.normal = info.normal.rotateY(sin_theta, cos_theta);
return true;
}
return false;
}

AABB RotateY::GetBox() const {
return box;
}

Chapter 8:Volumes

8.1 次表面散射材质,见Material/Isotropic.h

  • 对于一个恒定密度体,一条光线通过其中的时候,在烟雾体中传播的时候也会发生散射
  • 光线在烟雾体中能传播多远,是由烟雾体的密度决定的
    • 密度越高,光线穿透性越差,光线传播的距离也越短

img

  • 当光线通过体积时,它可能在任何点散射。 光线在单位距离\(dL\)中散射的概率为: \[ P=C*dL \]

    • 其中\(C\)与体积的光密度成比例
  • 对于恒定体积,我们只需要密度\(C\)和边界

class Isotropic : public Material {
public:
/*
* @brief 次表面散射材质
* @param albedo 散射系数
*/
Isotropic(Ref<Texture> albedo) : albedo(albedo) {}

/*
* @brief 生成散射光线
* @param r_in 入射光线
* @param info 碰撞信息
* @param attenuation 当发生散射时, 光强如何衰减, 分为rgb三个分量
* @param r_out 散射光线
* @return 是否得到了散射光线
*/
virtual bool scatter(const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const override;

private:
Ref<Texture> albedo;
};
bool Isotropic::scatter(const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const {
r_out = Ray(info.position, Random::rand_unit_sphere());
attenuation = albedo->Value(info.u, info.v, info.position);
return true;
}
  • 这个材质的散射原理和漫反射材质的大同小异,均属于碰撞点转换为新视点,沿任意方向发射新的视线,区别就在于
    • 漫反射的散射光线不可能指到物体内部,它一定是散射到表面外部(视线方向指向外切球体表面)
    • isotropic材质的散射光线可以沿原来的方向一往前,以此视线透光性
  • 因为烟雾内部只是颗粒而不存在真正不可穿透的几何实体,所以漫反射实体不可穿透,只能散射到表面外部,而烟雾可穿透

8.2 烟雾体,见Object/Transform/ConstantMedium.h

class ConstantMedium : public Object {
public:
/*
* @brief 恒定密度介质
* @param object 介质的形状
* @param density 介质的密度
* @param albedo 介质的反射率
*/
ConstantMedium(Ref<Object> object, double density, Ref<Texture> albedo) : object(object), density(density), isotropic_material(New<Isotropic>(albedo)) {}

/*
* @brief 判断光线是否与当前对象碰撞
* @param r 光线
* @param t_min 光线的最小 t 值
* @param t_max 光线的最大 t 值
* @param info 碰撞点信息
* @return 是否碰撞
*/
virtual bool hit(const Ray& r, double t_min, double t_max, HitInfo& info) const override;

/*
* @brief 获取当前对象的包围盒
*/
virtual AABB GetBox() const override;
private:
Ref<Object> object;
double density;
Ref<Isotropic> isotropic_material;
};
bool ConstantMedium::hit(const Ray& r, double t_min, double t_max, HitInfo& info) const {
HitInfo info1, info2;
if (object->hit(r, -INFINITY, INFINITY, info1) && object->hit(r, info1.t + 0.0001, INFINITY, info2)) {
if (info1.t < t_min) info1.t = t_min;
if (info2.t > t_max) info2.t = t_max;
if (info1.t >= info2.t) return false;

if (info1.t < 0) info1.t = 0;
float distance_inside_boundary = (info2.t - info1.t) * r.Direction().length();
float hit_distance = -(1 / density) * log(Random::rand01());
if (hit_distance < distance_inside_boundary) {
info.t = info1.t + hit_distance / r.Direction().length();
info.position = r.At(info.t);
info.normal = Vec3(Random::rand01(), Random::rand01(), Random::rand01());
info.material = isotropic_material;
return true;
}
}
return false;
}

AABB ConstantMedium::GetBox() const {
return object->GetBox();
}

8.3 场景测试代码

void Cornell_smoke() {
Ref<Material> red = New<Lambertian>(New<TextureConstant>(Color(0.65, 0.05, 0.05)));
Ref<Material> blue = New<Lambertian>(New<TextureConstant>(Color(0.05, 0.05, 0.73)));
Ref<Material> green = New<Lambertian>(New<TextureConstant>(Color(0.12, 0.45, 0.15)));
Ref<Material> white = New<Lambertian>(New<TextureConstant>(Color(0.88, 0.88, 0.88)));
Ref<Material> light = New<Emit>(New<TextureConstant>(Color(1, 1, 1)));

world.Add(New<RectXZ>(213, 343, 227, 332, 554, light)); // 顶部光源
world.Add(New<RectYZ>(0, 555, 0, 555, 555, green)); // 左绿墙
world.Add(New<FlipNormal>(New<RectYZ>(0, 555, 0, 555, 0, red))); // 右红墙
world.Add(New<FlipNormal>(New<RectXZ>(0, 555, 0, 555, 555, white)));// 上白墙
world.Add(New<RectXZ>(0, 555, 0, 555, 0, white)); // 下白墙
world.Add(New<FlipNormal>(New<RectXY>(0, 555, 0, 555, 555, blue))); // 后蓝墙

Ref<Box> box1 = New<Box>(Vec3(0, 0, 0), Vec3(165, 165, 165), white);
Ref<Box> box2 = New<Box>(Vec3(0, 0, 0), Vec3(165, 330, 165), white);
Ref<Object> box_1 = New<Translate>(Vec3(130, 0, 65), New<RotateY>(-18, box1));
Ref<Object> box_2 = New<Translate>(Vec3(265, 0, 295), New<RotateY>(15, box2));
world.Add(New<ConstantMedium>(box_2, 0.006, New<TextureConstant>(Color(0.8, 0.58, 0)))); // 盒子1
world.Add(New<ConstantMedium>(box_1, 0.008, New<TextureConstant>(Color(0.9, 0.2, 0.72)))); // 盒子2
return;
}

Chapter 9:A Scene Testing All New Features

void Final_Scene() {
Ref<Material> red = New<Lambertian>(New<TextureConstant>(Color(0.65, 0.05, 0.05)));
Ref<Material> blue = New<Lambertian>(New<TextureConstant>(Color(0.05, 0.05, 0.73)));
Ref<Material> green = New<Lambertian>(New<TextureConstant>(Color(0.12, 0.45, 0.15)));
Ref<Material> white = New<Lambertian>(New<TextureConstant>(Color(0.88, 0.88, 0.88)));
Ref<Material> ground = New<Lambertian>(New<TextureConstant>(Color(0.48, 0.83, 0.53)));
Ref<Material> light = New<Emit>(New<TextureConstant>(Color(1, 1, 1)));

// 20 * 20 个盒子
Ref<ObjectWorld> boxes = New<ObjectWorld>();
int nb = 20;
for(int i = 0; i < nb; i++)
for (int j = 0; j < nb; j++) {
double w = 100;
double x0 = -1000 + i * w;
double z0 = -1000 + j * w;
double y0 = 0;
double x1 = x0 + w;
double y1 = 100 * (Random::rand01() + 0.01);
double z1 = z0 + w;
boxes->Add(New<Box>(Vec3(x0, y0, z0), Vec3(x1, y1, z1), ground));
}
boxes->Build();
world.Add(boxes);

// 顶部光源
world.Add(New<RectXZ>(123, 423, 147, 412, 550, light));

// 2个静止球 + 1个运动球
Vec3 center(400, 400, 200);
world.Add(New<SphereMoving>(center, center + Vec3(30, 0, 0), 0, 1, 50, New<Lambertian>(New<TextureConstant>(Color(0.7, 0.3, 0.1)))));
world.Add(New<Sphere>(Vec3(260, 150, 45), 50, New<Dielectric>(1.5)));
world.Add(New<Sphere>(Vec3(0, 150, 145), 50, New<Metal>(New<TextureConstant>(Color(0.8, 0.8, 0.9)), 10.0)));

// 体积雾
Ref<Object> boundary = New<Sphere>(Vec3(360, 150, 145), 70, New<Dielectric>(1.5));
world.Add(boundary);
world.Add(New<ConstantMedium>(boundary, 0.2, New<TextureConstant>(Color(0.2, 0.4, 0.9))));
boundary = New<Sphere>(Vec3(0, 0, 0), 5000, New<Dielectric>(1.5));
world.Add(New<ConstantMedium>(boundary, 0.0001, New<TextureConstant>(Color(1, 1, 1))));

// 贴图球
Ref<Material> earth = New<Lambertian>(New<TextureImage>("resource/earthmap.jpg"));
world.Add(New<Sphere>(Vec3(400, 200, 400), 100, earth));

// 噪声球
Ref<Material> noise = New<Lambertian>(New<TextureNoise>(0.1));
world.Add(New<Sphere>(Vec3(220, 280, 300), 80, noise));

// 1000 个球
int ns = 1000;
Ref<ObjectWorld> spheres = New<ObjectWorld>();
for(int j = 0; j < ns; j++)
spheres->Add(New<Sphere>(Vec3(165 * Random::rand01(), 165 * Random::rand01(), 165 * Random::rand01()), 10, white));
spheres->Build();
world.Add(New<Translate>(Vec3(-100, 270, 395), New<RotateY>(15, spheres)));
world.Build();
}