Peter Shirley Ray Tracing in One Weekend(上篇)

Peter Shirley-Ray Tracing in One Weekend (2016)

原著:Peter Shirleyios

本書是Peter Shirley ray tracing系列三部曲的第一本,也是學習ray tracing 入門比較容易的一本書,本身照着書上的內容,抄了一遍,Github有完整的代碼,和每一章學習過程的代碼,部分代碼加了註釋。c++

Github地址git

目錄:

  • 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渲染到圖片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

  • 像素從左往右打印
  • 從上向下打印
  • 這個例子中RGB計算出來在[0,1]之間,輸出以前映射到一個高範圍空間
  • 紅+綠=黃
  • 打印的內容保存成.ppm格式便可預覽

Chapter2:The vec3 class

用於幾何向量計算和顏色計算,包含顏色,向量,位置座標,偏移,主要包含重寫操做符,以及點乘、叉乘等操做。函數

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];
};

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的點的顏色。this

假設攝像機的位置就是眼睛位置,看到的內容爲ppm顯示的東西,簡歷座標系,z軸正方向,垂直平面向外,x向右,y向上,spa

計算公式:3d

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

Chapter4:Adding a sphere

球的公式:

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);
}

Chapter5:Surface normals and multiple objects

法線是垂直與物體表面的一個向量,對於上一節提到的球,他的法線方向是,從球心出發,射向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";
        }
    }
}

Chapter6:Antialiasing

真實世界中,照相機拍照時,一邊邊緣部分沒有鋸齒,由於每一個像素,前景和背景在邊緣的地方進行的混合。咱們能夠經過平均多個像素的值,達到同樣的效果。咱們的作法是,抽象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";
        }
    }

}

最後達到的效果以下

Chapter7:Diffuse Materials

以前已經實現了多個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]));

這樣就能夠看清楚交界處的陰影效果了,以下圖:

(上篇完)

下篇將從如下幾個方面繼續學習

  • Chapter8:Metal
  • Chapter9:Dielectrics
  • Chapter10:Positionable camera
  • Chapter11:Defocus
  • Chapter12:Where next?
相關文章
相關標籤/搜索