Peter Shirley-Ray Tracing in One Weekend (2016)

原著:Peter Shirley

本书是Peter Shirley ray tracing系列三部曲的第一本,也是学习ray tracing 入门比较容易的一本书,自己照着书上的内容,抄了一遍,Github有完整的代码,和每一章学习过程的代码,部分代码加了注释。

Github地址

目录:

  • Chapter1:Output an image
  • Chapter2:The vec3 class
  • Chapter3:Rays, a simple camera, and background
  • Chapter4:Adding a sphere
  • Chapter5:Surface normals and multiple objects
  • Chapter6:Antialiasing
  • Chapter7:Diffuse Materials

Chapter1:Output an image

使用ppm渲染到图片

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5. int nx =200;
  6. int ny=100;
  7. cout<<"P3\n"<<nx<<" "<<ny<<"\n255\n";
  8. for(int j=ny-1;j>=0;j--)
  9. {
  10. for(int i=0;i<nx;i++)
  11. {
  12. float r=float(i)/float(nx);
  13. float g=float(j)/float(ny);
  14. float b=0.2;
  15. int ir=int(255.99*r);
  16. int ig=int(255.99*g);
  17. int ib=int(255.99*b);
  18. cout<<ir<<" "ig<<" "<<ib<<"\n";
  19. }
  20. }
  21. }

说明:

  • 像素从左往右打印
  • 从上向下打印
  • 这个例子中RGB计算出来在[0,1]之间,输出之前映射到一个高范围空间
  • 红+绿=黄
  • 打印的内容保存成.ppm格式即可预览

Chapter2:The vec3 class

用于几何向量计算和颜色计算,包含颜色,向量,位置坐标,偏移,主要包含重写操作符,以及点乘、叉乘等操作。

  1. class vec3 {
  2. public:
  3. vec3() {}
  4. vec3(float e0, float e1, float e2) { e[0] = e0; e[1] = e1; e[2] = e2; }
  5. inline float x() const { return e[0]; }
  6. inline float y() const { return e[1]; }
  7. inline float z() const { return e[2]; }
  8. inline float r() const { return e[0]; }
  9. inline float g() const { return e[1]; }
  10. inline float b() const { return e[2]; }
  11. inline const vec3& operator+() const { return *this; }
  12. inline vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); }
  13. inline float operator[](int i) const { return e[i]; }
  14. inline float& operator[](int i) { return e[i]; };
  15. inline vec3& operator+=(const vec3 &v2);
  16. inline vec3& operator-=(const vec3 &v2);
  17. inline vec3& operator*=(const vec3 &v2);
  18. inline vec3& operator/=(const vec3 &v2);
  19. inline vec3& operator*=(const float t);
  20. inline vec3& operator/=(const float t);
  21. inline float length() const { return sqrt(e[0]*e[0] + e[1]*e[1] + e[2]*e[2]); }
  22. inline float squared_length() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; }
  23. inline void make_unit_vector();
  24. float e[3];
  25. };

Chapter3:Rays, a simple camera, and background

所有的ray tracers 都是以ray类为基础,计算颜色

p(t) = A + t*B

其中A是光源点,B是ray的方向,t是具体float值,空间中确定一条线,不同的t,可以到达不同地方。

p(t)称为点A关于t的函数。Ray tracing的本质是通过发射射线,计算像素点的颜色。在ray tracing之前需要有个摄像机,建立坐标系,显示背景色,以及ray hit的点的颜色。

假设摄像机的位置就是眼睛位置,看到的内容为ppm显示的东西,简历坐标系,z轴正方向,垂直平面向外,x向右,y向上,

计算公式:

  1. blended_value = (1-t)*start_value + t*end_value

Chapter4:Adding a sphere

球的公式:

  1. x*x + y*y +z*z = R*R

对于任意xyz,如果满足球面公式,(x,y,z)为球面的一个点。

如果球心位置为(cx,cy,cz),公式为

  1. (x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz) = R*R

用向量表示,球面点P,球心点C,半径可以表示为向量PC

  1. dot((p-C)(p-C)) = (x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz)

等价于

  1. dot((A + t*B - C),(A + t*B - C)) = R*R

展开之后

  1. t*t*dot(B,B) + 2*t*dot(A-C,A-C) + dot(C,C) - R*R = 0

ABC已知,这里是一个关于t的一元二次方程,对于t无解,有一个解,有两个解的情况,即为下图

通过打印颜色,利用红色的射线,ray hit 圆,hit到的地方显示红色

  1. bool hit_sphere(const vec3 & center, float radius,const ray& r)
  2. {
  3. vec3 oc = r.origin() -center;
  4. float a = dot(r.direction(), r.direction());
  5. float b = 2.0 * dot(oc,r.direction());
  6. float c = dot(oc,oc) -radius*radius;
  7. float discriminant = b*b - 4*a*c;
  8. return (discriminant>0);
  9. }
  10. vec3 color(const ray& r)
  11. {
  12. if(hit_sphere(vec3(0,0,-1),0.5,r))
  13. return vec3(1.0,0,0);
  14. vec3 unit_direction = unit_vector(r.direction());
  15. float t = 0.5 *(unit_direction.y() + 1.0);
  16. return (1.0-t)*vec3(1.0,1.0,1.0) + t*vec3(0.5,0.7,1.0);
  17. }

Chapter5:Surface normals and multiple objects

法线是垂直与物体表面的一个向量,对于上一节提到的球,他的法线方向是,从球心出发,射向hitpoint的。就像在地球上,地面的法向是从地心出发,射向你站立的点的。

假设N是长度在[-1,1]之间的单位向量,映射到去见[0,1]之间,再映射x/y/z到r/g/b,通常除了须要知道是否hit点,还要拿到hit point的数据。

  1. // 本章 hit_Sphere的返回值改为float了
  2. float hit_sphere(const vec3 & center, float radius,const ray& r)
  3. {
  4. vec3 oc = r.origin() -center;
  5. float a = dot(r.direction(), r.direction());
  6. float b = 2.0 * dot(oc,r.direction());
  7. float c = dot(oc,oc) -radius*radius;
  8. float discriminant = b*b - 4*a*c;
  9. if(discriminant<0)
  10. return -1.0;
  11. else
  12. return (-b-sqrt(discriminant))/(2.0*a);
  13. }
  14. vec3 color(const ray& r)
  15. {
  16. float t = hit_sphere(vec3(0,0,-1),0.5,r);
  17. if(t>0.0)
  18. {
  19. // 球心到hitpoint的单位法向量
  20. vec3 N = unit_vector(r.point_at_parameter(t)-vec3(0,0,-1));
  21. return 0.5*vec3(N.x() +1,N.y()+1,N.z()+1);
  22. }
  23. vec3 unit_direction = unit_vector(r.direction());
  24. t = 0.5 *(unit_direction.y() + 1.0);
  25. return (1.0-t)*vec3(1.0,1.0,1.0) + t*vec3(0.5,0.7,1.0);
  26. }

当场景中有多个可以被击中的物体的时候,需要一个Hitable的抽象类,包含抽象方法hit 是否击中,以及记录hit到的数据,包括hit的位置,hit点的法向,以及距离t

通过距离t

tmin< t < tmax

来控制hit到物体的距离远近,因为hit到之后将不再往后ray tracing。

  1. #include "ray.h"
  2. struct hit_record
  3. {
  4. float t;
  5. vec3 p;
  6. vec3 normal;
  7. };
  8. class hitable
  9. {
  10. public:
  11. virtual bool hit(const ray& r,float t_min,float t_max,hit_record & rec)const =0;
  12. };

对于sphere类基础hitable抽象类,实现自己的hit方法,去判断是否击中了球的对象


  1. #include "hitable.h"
  2. class sphere: public hitable {
  3. public:
  4. sphere() {}
  5. sphere(vec3 cen, float r) : center(cen), radius(r) {};
  6. virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
  7. vec3 center;
  8. float radius;
  9. };
  10. bool sphere::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
  11. vec3 oc = r.origin() - center;
  12. float a = dot(r.direction(), r.direction());
  13. float b = dot(oc, r.direction());
  14. float c = dot(oc, oc) - radius*radius;
  15. float discriminant = b*b - a*c;
  16. if (discriminant > 0) {
  17. float temp = (-b - sqrt(discriminant))/a;
  18. if (temp < t_max && temp > t_min) {
  19. rec.t = temp;
  20. rec.p = r.point_at_parameter(rec.t);
  21. rec.normal = (rec.p - center) / radius;
  22. return true;
  23. }
  24. temp = (-b + sqrt(discriminant)) / a;
  25. if (temp < t_max && temp > t_min) {
  26. rec.t = temp;
  27. rec.p = r.point_at_parameter(rec.t);
  28. rec.normal = (rec.p - center) / radius;
  29. return true;
  30. }
  31. }
  32. return false;
  33. }

还需要一个hitable list去记录击中所有的物体,也是继承hitable类,实现hit方法,去找出最近的物体。


  1. #include "hitable.h"
  2. class hitable_list: public hitable {
  3. public:
  4. hitable_list() {}
  5. hitable_list(hitable **l, int n) {list = l; list_size = n; }
  6. virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
  7. hitable **list;
  8. int list_size;
  9. };
  10. bool hitable_list::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
  11. hit_record temp_rec;
  12. bool hit_anything = false;
  13. double closest_so_far = t_max;
  14. for (int i = 0; i < list_size; i++) {
  15. if (list[i]->hit(r, t_min, closest_so_far, temp_rec)) {
  16. hit_anything = true;
  17. closest_so_far = temp_rec.t;
  18. rec = temp_rec;
  19. }
  20. }
  21. return hit_anything;
  22. }

本章新的main函数如下

  1. #include <iostream>
  2. #include "sphere.h"
  3. #include "hitable_list.h"
  4. #include "float.h"
  5. using namespace std;
  6. vec3 color(const ray& r,hitable *world)
  7. {
  8. hit_record rec;
  9. if(world->hit(r,0.0,MAXFLOAT,rec))
  10. return 0.5*vec3(rec.normal.x()+1,rec.normal.y()+1,rec.normal.z()+1);
  11. else
  12. {
  13. vec3 unit_direction = unit_vector(r.direction());
  14. float t = 0.5 *(unit_direction.y() + 1.0);
  15. return (1.0-t)*vec3(1.0,1.0,1.0) + t*vec3(0.5,0.7,1.0);
  16. }
  17. }
  18. int main()
  19. {
  20. int nx =200;
  21. int ny=100;
  22. cout<<"P3\n"<<nx<<" "<<ny<<"\n255\n";
  23. vec3 lower_left_corner(-2.0,-1.0,-1.0);
  24. vec3 horizontal(4.0,0.0,0.0);
  25. vec3 vertical(0.0,2.0,0.0);
  26. vec3 origin(0.0,0.0,0.0);
  27. hitable *list[2];
  28. // 球1
  29. list[0] = new sphere(vec3(0,0,-1),0.5);
  30. // 球2
  31. list[1] = new sphere(vec3(0,-100.5,-1),100);
  32. hitable *world = new hitable_list(list,2);
  33. for(int j=ny-1;j>=0;j--)
  34. {
  35. for(int i=0;i<nx;i++)
  36. {
  37. float u = float(i)/float(nx);
  38. float v = float(j)/float(ny);
  39. ray r(origin,lower_left_corner + u*horizontal +v * vertical);
  40. vec3 p = r.point_at_parameter(2.0);
  41. vec3 col = color(r,world);
  42. int ir=int(255.99* col[0]);
  43. int ig=int(255.99* col[1]);
  44. int ib=int(255.99* col[2]);;
  45. cout<<ir<<" "<<ig<<" "<<ib<<"\n";
  46. }
  47. }
  48. }

Chapter6:Antialiasing

真实世界中,照相机拍照时,一边边缘部分没有锯齿,因为每个像素,前景和背景在边缘的地方进行的混合。我们可以通过平均多个像素的值,达到一样的效果。我们的做法是,抽象camera类,后面再写颜色的部分。

还需要写个随机数的生成器,用来控制采样点的位置,范围是在[0,1]之间。这里我定义了一个宏

  1. #define random(a,b) (rand()%(b-a+1)+a)

使用rand()程序运行时每次生成的随机数和上一次相同,便于调试。

对于给的一个像素,我们有好几个采样点在像素内,对每个采样点进行ray tracer,再平均每个采样点的color。

camera类

  1. class camera
  2. {
  3. vec3 origin;
  4. vec3 horizontal;
  5. vec3 vertical;
  6. vec3 lower_left_corner;
  7. public :
  8. camera()
  9. {
  10. lower_left_corner = vec3 (-2.0,-1.0,-1.0);
  11. horizontal = vec3(4.0,0.0,0.0);
  12. vertical = vec3(0.0,2.0,0.0);
  13. origin = vec3(0.0,0.0,0.0);
  14. }
  15. ray get_ray(float u,float v)
  16. {
  17. return ray(origin,lower_left_corner+u*horizontal + v*vertical - origin);
  18. }
  19. };

main函数

  1. #include <iostream>
  2. #include "sphere.h"
  3. #include "hitable_list.h"
  4. #include "float.h"
  5. #include "camera.h"
  6. #include "random"
  7. #define random(a,b) (rand()%(b-a+1)+a)
  8. using namespace std;
  9. vec3 color(const ray& r,hitable *world)
  10. {
  11. hit_record rec;
  12. if(world->hit(r,0.0,MAXFLOAT,rec))
  13. return 0.5*vec3(rec.normal.x()+1,rec.normal.y()+1,rec.normal.z()+1);
  14. else
  15. {
  16. vec3 unit_direction = unit_vector(r.direction());
  17. float t = 0.5 *(unit_direction.y() + 1.0);
  18. return (1.0-t)*vec3(1.0,1.0,1.0) + t*vec3(0.5,0.7,1.0);
  19. }
  20. }
  21. int main()
  22. {
  23. int nx =200;
  24. int ny=100;
  25. // 采样数量ns
  26. int ns = 100;
  27. cout<<"P3\n"<<nx<<" "<<ny<<"\n255\n";
  28. camera cam;
  29. hitable *list[2];
  30. // 球1
  31. list[0] = new sphere(vec3(0,0,-1),0.5);
  32. // 球2
  33. list[1] = new sphere(vec3(0,-100.5,-1),100);
  34. hitable *world = new hitable_list(list,2);
  35. random_device rd;
  36. for(int j=ny-1;j>=0;j--)
  37. {
  38. for(int i=0;i<nx;i++)
  39. {
  40. vec3 col(0,0,0);
  41. for(int s = 0; s<ns; s++)
  42. {
  43. float u = (float(i)+float(random(0,100))/100.0f)/float(nx);
  44. float v = (float(j)+float(random(0,100))/100.0f)/float(ny);
  45. ray r = cam.get_ray(u,v);
  46. vec3 p = r.point_at_parameter(2.0);
  47. col += color(r,world);
  48. }
  49. // color 取均值
  50. col /= float(ns);
  51. int ir=int(255.99* col[0]);
  52. int ig=int(255.99* col[1]);
  53. int ib=int(255.99* col[2]);;
  54. cout<<ir<<" "<<ig<<" "<<ib<<"\n";
  55. }
  56. }
  57. }

最后达到的效果如下

Chapter7:Diffuse Materials

之前已经实现了多个object 和每个像素多个采样,本章将实现漫反射材质。首先需要明确的一点是,物体和材质的关系,我们假设球体有一个自己的材质,通常在渲染中,每个物体都有自己的材质。

不发光的物体,漫反射是吸收周围的颜色,显示出来,物体表面反射周围的光线的方向是随机的,如下图,在2个不同的物体的漫反射表面间,发射了3条光线,三条光线的漫反射之后的路径各不相同:

漫反射物体的表面,也可能会吸收部分光线,表面越暗,吸收的光线越多,吸收之后看起来就像一个哑光的表面。

选择一个随机的点切一个单位半径的球,这个点就是hitpoint,在球上选个随机点s,从p到s做一条线,作为漫反射的方向,这个球的球心是(p + N),N是hitpoitn的法向。

关于球面上s点如何区,这里的做法是,在单位cube中,选一个点,x、y、z都在[-1,1]之间,如果这个点不在球内,继续选点,直到满足在球内的这个条件。

  1. // 单位cube随机取点,返回一个在球内的点
  2. vec3 random_in_unit_sphere()
  3. {
  4. vec3 p;
  5. do{
  6. p = 2.0*vec3(random1,random1,random1) - vec3(1,1,1);
  7. }while (dot(p,p) >= 1.0);
  8. return p;
  9. }
  10. vec3 color(const ray& r,hitable *world)
  11. {
  12. hit_record rec;
  13. if(world->hit(r,0.0,MAXFLOAT,rec))
  14. {
  15. vec3 target = rec.p + rec.normal + random_in_unit_sphere();
  16. return 0.5* color(ray(rec.p, target - rec.p), world);
  17. }
  18. else
  19. {
  20. vec3 unit_direction = unit_vector(r.direction());
  21. float t = 0.5 *(unit_direction.y() + 1.0);
  22. return (1.0-t)*vec3(1.0,1.0,1.0) + t*vec3(0.5,0.7,1.0);
  23. }
  24. }

得到的图像如下:

球和地板的交界处的颜色可能不明显,是因为吸收的光太多了,可以通多将颜色开放的方法,来提高物体表面的亮度,减少吸收的光

  1. col = vec3(sqrt(col[0]),sqrt(col[1]),sqrt(col[2]));

这样就可以看清楚交界处的阴影效果了,如下图:

(上篇完)

下篇将从以下几个方面继续学习

  • Chapter8:Metal
  • Chapter9:Dielectrics
  • Chapter10:Positionable camera
  • Chapter11:Defocus
  • Chapter12:Where next?

Peter Shirley Ray Tracing in One Weekend(上篇)的更多相关文章

  1. Peter Shirley Ray Tracing in One Weekend(下篇)

    Peter Shirley-Ray Tracing in One Weekend (2016) 原著:Peter Shirley 下篇主要对本书的后5章节进行学习,包括材质球的Metal,和Diele ...

  2. Fundamentals of Computer Graphics 中文版(第二版) (Peter Shirley 著)

    1 引言 2 数学知识 3 光栅算法 4 信号处理 5 线性代数 6 矩阵变换 7 观察 8 隐藏面消除 9 表面明暗处理 10 光线追踪 11 纹理映射 12 完整的图形流水线 13 图形学的数据结 ...

  3. OpenCascade Ray Tracing Rendering

    OpenCascade Ray Tracing Rendering eryar@163.com 摘要Abstract:OpenCascade6.7.0中引入了光线跟踪算法的实现.使用光线跟踪算法可实现 ...

  4. 开始研究Ray tracing

    几个月前面试时Boss问过我一个问题--"除了scanline渲染方法,你还知道什么其他渲染方式?",我没答出来,至今记忆犹新. 前段时间摆弄Intel VTune时看了它的示例代 ...

  5. Ray Tracing

    Ray Tracing 题目链接:http://codeforces.com/problemset/problem/724/C 拓展欧几里得 //为什么这次C题这么难啊=.= 可以观察到,光线在矩形中 ...

  6. 《Ray Tracing in One Weekend》、《Ray Tracing from the Ground Up》读后感以及光线追踪学习推荐

    <Ray Tracing in One Weekend> 优点: 相对简单易懂 渲染效果相当好 代码简短,只看书上的代码就可以写出完整的程序,而且Github上的代码是将基类与之类写在一起 ...

  7. 【Ray Tracing The Next Week 超详解】 光线追踪2-7 任意长方体 && 场景案例

    上一篇比较简单,很久才发是因为做了一些好玩的场景,后来发现这一章是专门写场景例子的,所以就安排到了这一篇 Preface 这一篇要介绍的内容有: 1. 自己做的光照例子 2. Cornell box画 ...

  8. 【RAY TRACING THE REST OF YOUR LIFE 超详解】 光线追踪 3-7 混合概率密度

     Preface 注:鉴于很多网站随意爬取数据,可能导致内容残缺以及引用失效等问题,影响阅读,请认准原创网址: https://www.cnblogs.com/lv-anchoret/category ...

  9. 【RAY TRACING THE REST OF YOUR LIFE 超详解】 光线追踪 3-5 random direction & ONB

     Preface 往后看了几章,对这本书有了新的理解 上一篇,我们第一次尝试把MC积分运用到了Lambertian材质中,当然,第一次尝试是失败的,作者发现它的渲染效果和现实有些出入,所以结尾处声明要 ...

随机推荐

  1. 小菜鸟之liunx

    目录 第一章:Linux简介 1 Linux特点 1 CentOS 1 第二章:Linux安装 2 Linux目录结构 2 第三章:Linux常用命令 2 Linux命令的分类 3 操作文件或目录常用 ...

  2. Ajax方式上传文件报错"Uncaught TypeError: Illegal invocation"

    今天使用ajax上传文件时,出现了错误.数据传输的方式是通过定义formData完成的,提交的文件对象也设置为dom对象,但是还是不能发送请求.F12看到后台报了个错误:Uncaught TypeEr ...

  3. DP+线段树维护矩阵(2019牛客暑期多校训练营(第二场))--MAZE

    题意:https://ac.nowcoder.com/acm/contest/882/E 给你01矩阵,有两种操作:1是把一个位置0变1.1变0,2是问你从第一行i开始,到最后一行j有几种走法.你只能 ...

  4. LeetCode 2——两数相加(JAVA)

    给出两个 非空 的链表用来表示两个非负的整数.其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字. 如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和 ...

  5. Django的URLconf

    URL 概要 我们要在Django项目中为应用程序设计URL,我们可以创建一个名为URLconf(通常为urls.py)的Python模块.这个模块是纯Python代码,是一个简单的正则表达式到Pyt ...

  6. win10下搭建vue开发环境

    特别说明:下面任何命令都是在windows的命令行工具下进行输入,打开命令行工具的快捷方式如下图:     详细的安装步骤如下: 一.安装node.js 说明:安装node.js的windows版本后 ...

  7. IOC+EF+Core搭建项目框架(三)

    /// <summary> /// 表示类别映射配置 /// </summary> public partial class sys_UserMap : NopEntityTy ...

  8. 在realm中动态查询用户的权限&角色

    @Controller @Scope("prototype") @Namespace("/") @ParentPackage("struts-defa ...

  9. js中逻辑运算符||和&& 的返回值并不只有true和false

    以前我一直认为逻辑运算符的返回值一直是true或者false,其实根本就没考虑过它又返回值,一直是在if判断语句中作为一个条件使用,只是为了能让条件正确与否进入if语句.根本就没用到逻辑运算符的返回值 ...

  10. Matlab函数kmeans

    Matlab函数kmeans K-means聚类算法采用的是将N*P的矩阵X划分为K个类,使得类内对象之间的距离最大,而类之间的距离最小. 使用方法:Idx=Kmeans(X,K)[Idx,C]=Km ...