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即可。
.