原著:Peter Shirleyios
本书是Peter Shirley ray tracing系列三部曲的第一本,也是学习ray tracing 入门比较容易的一本书,本身照着书上的内容,抄了一遍,Github有完整的代码,和每一章学习过程的代码,部分代码加了注释。c++
Github地址git
使用ppm渲染到图片github
#include <iostream> using namespace std; int main() { int nx =200; int ny=100; cout<<"P3\n"<<nx<<" "<<ny<<"\n255\n"; for(int j=ny-1;j>=0;j--) { for(int i=0;i<nx;i++) { float r=float(i)/float(nx); float g=float(j)/float(ny); float b=0.2; int ir=int(255.99*r); int ig=int(255.99*g); int ib=int(255.99*b); cout<<ir<<" "ig<<" "<<ib<<"\n"; } } }
说明:dom
用于几何向量计算和颜色计算,包含颜色,向量,位置坐标,偏移,主要包含重写操做符,以及点乘、叉乘等操做。函数
class vec3 { public: vec3() {} vec3(float e0, float e1, float e2) { e[0] = e0; e[1] = e1; e[2] = e2; } inline float x() const { return e[0]; } inline float y() const { return e[1]; } inline float z() const { return e[2]; } inline float r() const { return e[0]; } inline float g() const { return e[1]; } inline float b() const { return e[2]; } inline const vec3& operator+() const { return *this; } inline vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); } inline float operator[](int i) const { return e[i]; } inline float& operator[](int i) { return e[i]; }; inline vec3& operator+=(const vec3 &v2); inline vec3& operator-=(const vec3 &v2); inline vec3& operator*=(const vec3 &v2); inline vec3& operator/=(const vec3 &v2); inline vec3& operator*=(const float t); inline vec3& operator/=(const float t); inline float length() const { return sqrt(e[0]*e[0] + e[1]*e[1] + e[2]*e[2]); } inline float squared_length() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; } inline void make_unit_vector(); float e[3]; };
全部的ray tracers 都是以ray类为基础,计算颜色
p(t) = A + t*B
其中A是光源点,B是ray的方向,t是具体float值,空间中肯定一条线,不一样的t,能够到达不一样地方。学习
p(t)称为点A关于t的函数。Ray tracing的本质是经过发射射线,计算像素点的颜色。在ray tracing以前须要有个摄像机,创建坐标系,显示背景色,以及ray hit的点的颜色。this
假设摄像机的位置就是眼睛位置,看到的内容为ppm显示的东西,简历坐标系,z轴正方向,垂直平面向外,x向右,y向上,spa
计算公式:3d
blended_value = (1-t)*start_value + t*end_value
球的公式:
x*x + y*y +z*z = R*R
对于任意xyz,若是知足球面公式,(x,y,z)为球面的一个点。
若是球心位置为(cx,cy,cz),公式为
(x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz) = R*R
用向量表示,球面点P,球心点C,半径能够表示为向量PC
dot((p-C)(p-C)) = (x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz)
等价于
dot((A + t*B - C),(A + t*B - C)) = R*R
展开以后
t*t*dot(B,B) + 2*t*dot(A-C,A-C) + dot(C,C) - R*R = 0
ABC已知,这里是一个关于t的一元二次方程,对于t无解,有一个解,有两个解的状况,即为下图
经过打印颜色,利用红色的射线,ray hit 圆,hit到的地方显示红色
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(oc,r.direction()); float c = dot(oc,oc) -radius*radius; float discriminant = b*b - 4*a*c; return (discriminant>0); } vec3 color(const ray& r) { if(hit_sphere(vec3(0,0,-1),0.5,r)) return vec3(1.0,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); }
法线是垂直与物体表面的一个向量,对于上一节提到的球,他的法线方向是,从球心出发,射向hitpoint的。就像在地球上,地面的法向是从地心出发,射向你站立的点的。
假设N是长度在[-1,1]之间的单位向量,映射到去见[0,1]之间,再映射x/y/z到r/g/b,一般除了需要知道是否hit点,还要拿到hit point的数据。
// 本章 hit_Sphere的返回值改成float了 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(oc,r.direction()); float c = dot(oc,oc) -radius*radius; float discriminant = b*b - 4*a*c; if(discriminant<0) return -1.0; else return (-b-sqrt(discriminant))/(2.0*a); } vec3 color(const ray& r) { float t = hit_sphere(vec3(0,0,-1),0.5,r); if(t>0.0) { // 球心到hitpoint的单位法向量 vec3 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()); 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); }
当场景中有多个能够被击中的物体的时候,须要一个Hitable的抽象类,包含抽象方法hit 是否击中,以及记录hit到的数据,包括hit的位置,hit点的法向,以及距离t
经过距离t
tmin< t < tmax
来控制hit到物体的距离远近,由于hit到以后将再也不日后ray tracing。
#include "ray.h" 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; };
对于sphere类基础hitable抽象类,实现本身的hit方法,去判断是否击中了球的对象
#include "hitable.h" class sphere: public hitable { public: sphere() {} sphere(vec3 cen, float r) : center(cen), radius(r) {}; virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const; 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(oc, r.direction()); float c = dot(oc, oc) - radius*radius; float discriminant = b*b - a*c; if (discriminant > 0) { float temp = (-b - sqrt(discriminant))/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(discriminant)) / 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去记录击中全部的物体,也是继承hitable类,实现hit方法,去找出最近的物体。
#include "hitable.h" 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; 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 hit_anything = false; double closest_so_far = t_max; for (int i = 0; i < list_size; i++) { if (list[i]->hit(r, t_min, closest_so_far, temp_rec)) { hit_anything = true; closest_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; }
本章新的main函数以下
#include <iostream> #include "sphere.h" #include "hitable_list.h" #include "float.h" using namespace std; vec3 color(const ray& r,hitable *world) { hit_record rec; if(world->hit(r,0.0,MAXFLOAT,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; cout<<"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]; // 球1 list[0] = new sphere(vec3(0,0,-1),0.5); // 球2 list[1] = new sphere(vec3(0,-100.5,-1),100); hitable *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"; } } }
真实世界中,照相机拍照时,一边边缘部分没有锯齿,由于每一个像素,前景和背景在边缘的地方进行的混合。咱们能够经过平均多个像素的值,达到同样的效果。咱们的作法是,抽象camera类,后面再写颜色的部分。
还须要写个随机数的生成器,用来控制采样点的位置,范围是在[0,1]之间。这里我定义了一个宏
#define random(a,b) (rand()%(b-a+1)+a)
使用rand()程序运行时每次生成的随机数和上一次相同,便于调试。
对于给的一个像素,咱们有好几个采样点在像素内,对每一个采样点进行ray tracer,再平均每一个采样点的color。
camera类
class camera { vec3 origin; vec3 horizontal; vec3 vertical; vec3 lower_left_corner; public : camera() { lower_left_corner = vec3 (-2.0,-1.0,-1.0); horizontal = vec3(4.0,0.0,0.0); vertical = vec3(0.0,2.0,0.0); origin = vec3(0.0,0.0,0.0); } ray get_ray(float u,float v) { return ray(origin,lower_left_corner+u*horizontal + v*vertical - origin); } };
main函数
#include <iostream> #include "sphere.h" #include "hitable_list.h" #include "float.h" #include "camera.h" #include "random" #define random(a,b) (rand()%(b-a+1)+a) using namespace std; vec3 color(const ray& r,hitable *world) { hit_record rec; if(world->hit(r,0.0,MAXFLOAT,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; // 采样数量ns int ns = 100; cout<<"P3\n"<<nx<<" "<<ny<<"\n255\n"; camera cam; hitable *list[2]; // 球1 list[0] = new sphere(vec3(0,0,-1),0.5); // 球2 list[1] = new sphere(vec3(0,-100.5,-1),100); hitable *world = new hitable_list(list,2); random_device rd; for(int j=ny-1;j>=0;j--) { for(int i=0;i<nx;i++) { vec3 col(0,0,0); for(int s = 0; s<ns; s++) { float u = (float(i)+float(random(0,100))/100.0f)/float(nx); float v = (float(j)+float(random(0,100))/100.0f)/float(ny); ray r = cam.get_ray(u,v); vec3 p = r.point_at_parameter(2.0); col += color(r,world); } // color 取均值 col /= float(ns); 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"; } } }
最后达到的效果以下
以前已经实现了多个object 和每一个像素多个采样,本章将实现漫反射材质。首先须要明确的一点是,物体和材质的关系,咱们假设球体有一个本身的材质,一般在渲染中,每一个物体都有本身的材质。
不发光的物体,漫反射是吸取周围的颜色,显示出来,物体表面反射周围的光线的方向是随机的,以下图,在2个不一样的物体的漫反射表面间,发射了3条光线,三条光线的漫反射以后的路径各不相同:
漫反射物体的表面,也可能会吸取部分光线,表面越暗,吸取的光线越多,吸取以后看起来就像一个哑光的表面。
选择一个随机的点切一个单位半径的球,这个点就是hitpoint,在球上选个随机点s,从p到s作一条线,做为漫反射的方向,这个球的球心是(p + N),N是hitpoitn的法向。
关于球面上s点如何区,这里的作法是,在单位cube中,选一个点,x、y、z都在[-1,1]之间,若是这个点不在球内,继续选点,直到知足在球内的这个条件。
// 单位cube随机取点,返回一个在球内的点 vec3 random_in_unit_sphere() { vec3 p; do{ p = 2.0*vec3(random1,random1,random1) - vec3(1,1,1); }while (dot(p,p) >= 1.0); return p; } vec3 color(const ray& r,hitable *world) { hit_record rec; if(world->hit(r,0.0,MAXFLOAT,rec)) { vec3 target = rec.p + rec.normal + random_in_unit_sphere(); return 0.5* color(ray(rec.p, target - rec.p), world); } 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); } }
获得的图像以下:
球和地板的交界处的颜色可能不明显,是由于吸取的光太多了,能够通多将颜色开放的方法,来提升物体表面的亮度,减小吸取的光
col = vec3(sqrt(col[0]),sqrt(col[1]),sqrt(col[2]));
这样就能够看清楚交界处的阴影效果了,以下图:
(上篇完)
下篇将从如下几个方面继续学习