Bare-Bones Ray Tracing
这一章内容是写一个最简单的光线追踪器,我们trace plane 和 sphere 就可以了。
本篇文章主要是整理代码结构,具体图形学知识一定要看书,书上讲的很清楚。
本章的光线为平行光,起点都在viewplane,方向为z轴负方向。我们把要trace的物体放在viewplane后面。显示被光线击中并且离viewplane最近的物体的颜色。
本书作者给的代码实现中包含了很多类,所以理解起来有点困难。并且因为作者给的代码不能直接运行(编译会报错),我重新按照作者的结构实现了一下,不过去掉了Point3D这个类,所有三维的点都用Vector3D表示。输出的图像为PPM格式,代码运行环境为Archlinux,没在windows环境下测试过。在此整理一下我的代码结构:
首先看下有main的主代码:
#include "World.h"
#include <fstream>
#include <iostream>
using namespace std;
ofstream out;
int main()
{
out.open("fileppm.ppm", ios::out);
out << "P3\n"
<< 400 << " " << 400 << "\n255\n";
World w;
w.build();
w.render_scene();
out.close();
return 0;
}
World就是所有类的一个主体,我们在World的成员函数里实现build函数(初始化所有东西),然后调用render_scene来绘制图像。
class World {
public:
ViewPlane vp;
RGBColor background_color;
Sphere sphere;
Tracer *tracer_ptr;
std::vector<GeometricObject *> objects;
World();
void build();
void add_object(GeometricObject *object_ptr);
ShadeRec hit_bare_bones_objects(const Ray &ray) const;
void render_scene() const;
void display_pixel(const RGBColor &pixel_color) const;
~World();
};
接下来看看build都初始化了一些什么:
void World::build()
{
vp.set_hres(400);
vp.set_vres(400);
vp.set_pixel_size(1);
vp.set_gamma(1.0);
background_color = blue;
//指针指向的对象是一个球体,trace_ray函数即为singlesphere中的函数
//tracer_ptr = new SingleSphere(this);
//sphere.set_center(0.0);
//sphere.set_radius(200);
tracer_ptr = new MultipleObjects(this);
Sphere *sphere_ptr = new Sphere;
sphere_ptr->set_center(-10, -40, 0);
sphere_ptr->set_radius(100.0);
sphere_ptr->set_color(1.0, 0.0, 0.0);
add_object(sphere_ptr);
sphere_ptr = new Sphere(Vector3D(0, 60, 0), 80.0);
sphere_ptr->set_color(1.0, 1.0, 0.0);
add_object(sphere_ptr);
Plane *plane_ptr = new Plane;
plane_ptr->a = Vector3D(0.0);
plane_ptr->nor = Vector3D(0.6, 0.3, 0.7);
plane_ptr->set_color(0.0, 0.30, 0.0);
add_object(plane_ptr);
}
注意被注释掉的一部分是只画一个Sphere的代码,这里我们实现多个物体渲染(MultipleObjects)。可以大致明白先初始化了Viewplane,然后通过tracer_ptr新建一个MultipleObjects对象,然后通过add_object加入三个物体的指针,初始化完成。
一、添加Objects
这里非常让人迷惑,Viewplane还是比较好理解的,我们透过这个plane去看这个世界,因此不多讲。
再看tracer_ptr,它的类型为Tracer*,可以把它看作我们追踪的所有物体的一个父类。我们的MultipleObjects就是Trace的子类。
那么Tracer到底干了些什么?看看目前的Tracer类:
class World;
class Tracer {
public:
Tracer(void);
Tracer(World *world_ptr);
virtual ~Tracer(void);
virtual RGBColor trace_ray(const Ray &ray) const;
public:
World *world_ptr;
};
注意:
public:
World *world_ptr;
这代表这我们初始化一个Tracer,需要一个World型的指针。这也就代表了,一个Tracer,与一个World关联起来了。我们新建了一个World以后,把它的指针传递给Tracer对象,我们就可以利用tracer_ptr对这个World里的物体进行渲染。
最开始的是一个前置声明,因为Tracer类是比World类先定义的,但是我们需要用到World,所以:
class World;
Tracer里的虚函数得在继承它的类里重新实现,这里的tracer_ray就是核心功能实现的函数。
我们看MultipleObjects:
class MultipleObjects : public Tracer {
public:
MultipleObjects(void);
MultipleObjects(World *_worldPtr);
virtual ~MultipleObjects(void);
virtual RGBColor trace_ray(const Ray &ray) const;
};
与此对应的有SingleSphere:
class SingleSphere : public Tracer {
public:
SingleSphere(void);
SingleSphere(World *_worldPtr);
virtual ~SingleSphere(void);
virtual RGBColor trace_ray(const Ray &ray) const;
};
接下来我们先回到build函数里写的
tracer_ptr = new MultipleObjects(this);
也就是说我们现在用的是MultipleObjects的trace方法。
然后我们看看如何实现多个物体的追踪。首先想到的肯定是vector容器,每条光线都要按顺序遍历一下这个vector,如果有物体被hit,那么选离viewplane最近的那个物体,显示它的颜色。
于是我们有了:
std::vector<GeometricObject *> objects;
GeometricObject就是所有被追踪物体的父类,Plane,Sphere都继承自它。
class GeometricObject {
public:
RGBColor color;
GeometricObject();
GeometricObject(const GeometricObject &obj);
GeometricObject &operator=(const GeometricObject &rhs);
virtual bool hit(const Ray &ray, double &t, ShadeRec &sr) const = 0;
virtual ~GeometricObject();
void set_color(const RGBColor &c);
void set_color(const float r, const float g, const float b);
RGBColor get_color(void);
};
我们调用vector的push_back函数把GeometricOject的对象指针加进去。这里我们加入了一个plane和两个Sphere,代码比较好理解。
二、光线追踪
现在我们已经添加好了三个物体,接下来就是设置ray然后检测是否hit。看看render_scene函数
void World::render_scene() const
{
RGBColor pixel_color;
Ray ray;
double zw = 200.0;
double x, y;
ray.d = Vector3D(0, 0, -1);
for (int r = 0; r < vp.vres; r++) {
for (int c = 0; c < vp.hres; c++) {
pixel_color = background_color;
x = vp.s * (c - 0.5 * (vp.hres - 1.0));
y = vp.s * (r - 0.5 * (vp.vres - 1.0));
ray.o = Vector3D(x, y, zw);
pixel_color = tracer_ptr->trace_ray(ray);
display_pixel(pixel_color);
}
}
}
两层循环里的内容需要靠一张图来理解:
如果你对这个图没什么感觉,先看
https://en.wikipedia.org/wiki/Pixel
x = vp.s * (c - 0.5 * (vp.hres - 1.0));
y = vp.s * (r - 0.5 * (vp.vres - 1.0));
每个pixel对应一条光线,光线中心为pixel的中心。
pixel_color = tracer_ptr->trace_ray(ray);
这句话就是trace的核心。此时tracer_ptr是一个指向MultipleObjects的指针,因此我们得看MultipleObjects的trace_ray函数
RGBColor MultipleObjects::trace_ray(const Ray &ray) const
{
ShadeRec sr(world_ptr->hit_bare_bones_objects(ray)); // sr is copy constructed
if (sr.hit_an_object)
return (sr.color);
else
return (world_ptr->background_color);
}
这里又牵扯到一个新的类ShadeRec,意为Shade Record。即着色记录,记录了是否击中,击中的物体颜色,击中点对应光线的参数t等等。hit_bare_bones_objects是World的成员函数,实现如下:
ShadeRec World::hit_bare_bones_objects(const Ray &ray) const
{
ShadeRec sr(*this);
double t, tmin = kHugeValue;
for (size_t j = 0; j < objects.size(); j++) {
if (objects[j]->hit(ray, t, sr)) {
if (t < tmin) {
sr.hit_an_object = true;
tmin = t;
sr.color = objects[j]->get_color();
}
}
}
return sr;
}
这里就非常明了了,即遍历该World里的objects容器里的所有物体,判断.......最后返回该着色记录。
到此只需要在屏幕上显示该点颜色即可。
结果如下(因为我设置的plane并非垂直z轴,是一个倾斜的,因此它会对两个球体的显示有影响,这是一个透视问题,如果你把plane的参数改成与z轴垂直,那么你不会看到紫色的background,只有绿色的plane,以及两个部分重叠的标准圆形,我们还没有开始shading,因此看起来是平面图):
三、源代码及资源
Ray Trace From Ground up pdf
https://github.com/vladotrocol/Graphics/blob/master/Ray%20Tracing%20From%20The%20Ground%20Up.pdf
关于我修改过的代码在此下载(本来是不想要积分的,,为啥自动给我加了):
https://download.csdn.net/download/moon_cy/10483443
输出的图像为PPM格式,代码运行环境为Archlinux,没在windows环境下测试过。鉴于本菜鸡还不会写Makefile,如果需要看结果的,请在命令行输入:
g++ -g Build_World.cpp Vector3D.cpp RGBColor.cpp Ray.cpp GeometryObject.cpp Plane.cpp ViewPlane.cpp ShadeRec.cpp Sphere.cpp Tracer.cpp One_Sphere_to_trace.cpp World.cpp Multiple_Objects_to_trace.cpp -o q
./q
查看file.ppm即可。
.