Ray tracking in One Weekend

目录

Chapter 0: Overview

Chapter 1: Output an image

Chapter 2: The vec3 class

Chapter 3: Rays, a simple camera, and background

Chapter 4: Adding a sphere

Chapter 5: Surface normals and multiple object

 


Chapter 0: Overview

主要讲了作者的教学经验以及Ray track的看法

作者推荐我们使用C++来完成这本书的编码实践

另外作者还附上了他的GitHub库 https://github.com/petershirley/raytracinginoneweekend


Chapter 1: Output an image

当你开始做渲染时,你得先知道你要如何查看图片(Image)。最直接的方式就是写一个文件来输出图片。这个章节就在告诉读者如何用代码生成一张图片,图片的格式(format)用到的是PPM文件格式。

About PPM file(关于PPM文件格式)

PPM(Portable Pixmap Format)图像格式是由Jef Poskanzer 在1991年所创造。创造之初就希望对于格式尽量的简单。

PPM and「PBM」and「PGM」三者之间的区别:

  • PBM 是位图(Bitmap),仅有黑与白,没有灰
  • PGM 是灰度图(Grayscale)
  • PPM 是通过RGB三种颜色显现的图像(Pixmaps)

每个图像文件的开头都通过2个字节「magic number」来表明文件格式的类型(PBM, PGM, PPM),以及编码方式(ASCII 或 Binary),这就意味着对于PBM和PGM和PPM都有两个版本,一种使用ACSII编写的版本,另一个使用binary格式编写的版本。[magic number]分别为P1、P2、P3、P4、P5、P6。

Magic Number Type Encoding
P1 Bitmap ASCII
P2 Grayscale ASCII
P3 Pixmap ASCII
P4 Bitmap Binary
P5 Grayscale Binary
P6 Pixmap Binary

编码方式:

  • ASCII编码格式适合人类阅读理解,可以用文本编辑器打开,读取对应图像的数据(比如PPM格式的RGB值)。
  • Binary格式适合机器阅读,按照二进制形式,顺序存储图像信息,不用空格分隔,所以图像处理起来更有效率,占用空间容量更少(由于缺少空格)。 

 

What is PPM format?(PPM格式详解)

A PPM header consists of the following entries, each separated by white space:

Magic Number Literally P3 for ASCII version, P6 for binary version
ImageWidth Width of image in pixels (ASCII decimal value)
ImageHeight Height of image in pixels (ASCII decimal value)
MaxGrey Maximum color value (ASCII decimal value)

PPM图像格式分为两部分,分别为头部分和图像数据部分。

头部分:由4部分组成,通过换行或空格对这4部分进行分割,在PPM标准中要求使用空格。头部分的意义在于提供了对于图像内容的整体概括描述。在头部可以用#来表示注释

  • 第1部分 version:P3或P6,指明PPM的编码(ASCII or binary)格式,
  • 第2部分 Width:图像的宽度
  • 第3部分 Heigh:高度,通过ASCII表示,
  • 第4部分 Maximum:最大像素值,0-255字节表示。

图像部分:图像部分紧挨着头部,用一系列的RGB值对图像进行描述。

  • 对于ASCII格式,就是按照RGB的顺序排列,以ASCII存储,并且,RGB中间用空格隔开,图片每一行用回车隔开。
  • 对于binary格式,就是每一个像素点的RGB值分别顺序存储并且按二进制写入文件(fwrite),没有任何分隔。

对于PPM格式,每个像素由3个十进制ACII码组成RGB定义色彩块。而ASCII码的取值范围是在(0,Maximum)从0到定义的颜色最大值。值得注意的是,每个RGB值都需要用空格来进行分隔。

Eg:

P3
3 2
255
255 000 000 000 255 000 000 000 255
255 255 000 255 255 255 000 000 000

-P3: PPM编码格式为ASCII
-3: 3列像素
-2: 2行像素
-255: 最大像素值

Ray tracking in One Weekend_第1张图片
 

How to view the PPM file?(如何查看PPM文件)

Xnview、PhotoShop等都可以支持查看PPM文件

 

参考:http://www.fileformat.info/format/pbm/egff.htm

https://www.cs.swarthmore.edu/~soni/cs35/f13/Labs/extras/01/ppm_info.html

http://netpbm.sourceforge.net/doc/ppm.html

Let’s make some C++ code to output such a thing:

对PPM文件有了一定的了解之后,我们开始用书中的例子开始用C++输出一段代码表示PPM文件来展示一张图片。

(书中的例子是在命令行中输出源代码结果,这样会导致我们不方便保存代码结果。我在这里用IO流将结果代码存到一个txt文件中,然后将txt文件的格式改为ppm。)

#include 
#include 

using namespace std;

int main() 
{
    ofstream outfile;
    outfile.open("firstImage.txt");

    int nx = 200;
    int ny = 100;
    outfile << "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.2f;

            int ir = int(255.99f*r);
            int ig = int(255.99f*g);
            int ib = int(255.99f*b);
            outfile << ir << " " << ig << " " << ib << "\n";
        }
    }
    outfile.close();
    return 0;
}

结果如下:

Ray tracking in One Weekend_第2张图片

 实现了图形学中的“Hello World”之后,我们来分析输出的PPM文件以及代码是如何得到这样的色彩变化的图片。

在代码中我们可以看到这样一段代码

float r = float(i) / float(nx);
float g = float(j) / float(ny);
float b = 0.2f;

在RGB三色通道中,从上到下,绿色通道green值减小;从左到右,红色通道值red增加;而蓝色通道值blue不变。所得图片即上图左上角就绿色,右下角红色的图片。

Let's do some change!(对源码做些改变,来输出另外一张不同颜色的图片)

将源码中设置的蓝色通道值b设定为固定值51,来增加图片中的蓝色。

float r = float(i) / float(nx);
float g = float(j) / float(ny);
float b = 51;

运行源码,查看新的PPM文件结果图片。

Ray tracking in One Weekend_第3张图片

Chapter 1 finished


Chapter 2: The vec3 class

大部分图形程序都有自己特定的类(class)来存储几何向量(geometric vectors)和颜色(colors)。其中大部分是四维的。

先介绍三维(X、Y、Z)

  • 几何向量(geometric vectors):X、Y、Z
  • 颜色(colors):r、g、b

四维:(X、Y、Z、W)

  • 几何向量(geometric vectors):X、Y、Z、齐次坐标(homogeneous coordinate)W
  • 颜色(colors):r、g、b、透明度(transparency) alpha

在《Ray tracking in One Weekend》这本书中用三维来表示位置、颜色、偏移、方向等。

向量类(Vec3.h)代码如下:

//#ifdef VEC3H
//#define VEC3H
//#endif
#include 
#include 
#include 

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

};

//输入流 输入格式
inline std::istream& operator>>(std::istream &is, Vec3 &t) {
    is >> t.e[0] >> t.e[1] >> t.e[2];
    return is;
}

//输出流 输出格式
inline std::ostream& operator<<(std::ostream &os, const Vec3 &t) {
    os << t.e[0] << " " << t.e[1] << " " << t.e[2];
    return os;
}

//生成单位向量 向量/向量的模
inline void Vec3::make_unit_vector() {
    float k = 1.0 / sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]);
    e[0] *= k; e[1] *= k; e[2] *= k;
}

//两向量相加
inline Vec3 operator+(const Vec3 &v1, const Vec3 &v2) {
    return Vec3(v1.e[0] + v2.e[0], v1.e[1] + v2.e[1], v1.e[2] + v2.e[2]);
}

//两向量相减
inline Vec3 operator-(const Vec3 &v1, const Vec3 &v2) {
    return Vec3(v1.e[0] - v2.e[0], v1.e[1] - v2.e[1], v1.e[2] - v2.e[2]);
}

//两向量相乘(for colors)
inline Vec3 operator*(const Vec3 &v1, const Vec3 &v2) {
    return Vec3(v1.e[0] * v2.e[0], v1.e[1] * v2.e[1], v1.e[2] * v2.e[2]);
}

//两向量相除(for colors)
inline Vec3 operator/(const Vec3 &v1, const Vec3 &v2) {
    return Vec3(v1.e[0] / v2.e[0], v1.e[1] / v2.e[1], v1.e[2] / v2.e[2]);
}

//一个标量乘一个向量
inline Vec3 operator*(float t, const Vec3 &v) {
    return Vec3(t*v.e[0], t*v.e[1], t*v.e[2]);
}

//一个向量除以一个标量
inline Vec3 operator/(Vec3 v, float t) {
    return Vec3(v.e[0] / t, v.e[1] / t, v.e[2] / t);
}

//一个向量乘一个标量
inline Vec3 operator*(const Vec3 &v, float t) {
    return Vec3(t*v.e[0], t*v.e[1], t*v.e[2]);
}

//两向量点乘(求内积,得一个常数)(for locations)
inline float dot(const Vec3 &v1, const Vec3 &v2) {
    return v1.e[0] * v2.e[0] + v1.e[1] * v2.e[1] + v1.e[2] * v2.e[2];
}

//两向量叉乘(求外积,得一个向量)(for locations)
inline Vec3 cross(const Vec3 &v1, const Vec3 &v2) {
    return Vec3((v1.e[1] * v2.e[2] - v1.e[2] * v2.e[1]),
        (-(v1.e[0] * v2.e[2] - v1.e[2] * v2.e[0])),
        (v1.e[0] * v2.e[1] - v1.e[1] * v2.e[0]));
}


inline Vec3& Vec3::operator+=(const Vec3 &v) {
    e[0] += v.e[0];
    e[1] += v.e[1];
    e[2] += v.e[2];
    return *this;
}

inline Vec3& Vec3::operator*=(const Vec3 &v) {
    e[0] *= v.e[0];
    e[1] *= v.e[1];
    e[2] *= v.e[2];
    return *this;
}

inline Vec3& Vec3::operator/=(const Vec3 &v) {
    e[0] /= v.e[0];
    e[1] /= v.e[1];
    e[2] /= v.e[2];
    return *this;
}

inline Vec3& Vec3::operator-=(const Vec3& v) {
    e[0] -= v.e[0];
    e[1] -= v.e[1];
    e[2] -= v.e[2];
    return *this;
}

inline Vec3& Vec3::operator*=(const float t) {
    e[0] *= t;
    e[1] *= t;
    e[2] *= t;
    return *this;
}

inline Vec3& Vec3::operator/=(const float t) {
    float k = 1.0 / t;

    e[0] *= k;
    e[1] *= k;
    e[2] *= k;
    return *this;
}

//归一化向量
inline Vec3 unit_vector(Vec3 v) {
    return v / v.length();
}

写好了Vec3.h之后,在Main.cpp中使用Vec3.h头文件也可以生成和Chapter1中一样的图片

#include 
#include 
#include "Vec3.h"
using namespace std;

int main()
{

    ofstream outfile;
    outfile.open("firstImage_With_Vec3.txt");

    int nx = 200;
    int ny = 100;
    outfile << "P3\n" << nx << " " << ny << "\n255\n";
    for (int j = ny - 1; j >= 0; j--)
    {
        for (int i = 0; i < nx; i++)
        {
            Vec3 col(float(i) / float(nx), float(j) / float(ny), 0.2);
            int ir = int(255.99*col[0]);
            int ig = int(255.99*col[1]);
            int ib = int(255.99*col[2]);
            outfile << ir << " " << ig << " " << ib << "\n";
        }
    }
    outfile.close();
    return 0;
}

Chapter 2 finished 


Chapter 3: Rays, a simple camera, and background

所有的光线追踪器(Ray Tracers)都有一个射线类(Ray Class),用来计算沿光线看到的颜色。

我们将光线定义为公式      p(t)=A+t*B

P是光线指向的目标位置(3D position along a line),A是光线的起点(ray origin),B是光线指向的方向(ray direction),t则是光线步长。

Ray tracking in One Weekend_第4张图片

射线类的定义文件如下:Ray.h

//#ifdef RAYH
//#define RAYH
//#endif
#include "Vec3.h"

class Ray
{
public:
    Ray(){}
    Ray(const Vec3& a, const Vec3& b) { A = a; B = b; }
    Vec3 origin() const { return A; }
    Vec3 direction() const { return B; }
    Vec3 point_at_parameter(float t) const { return A + t*B; }

    Vec3 A;
    Vec3 B;
};

光线追踪的核心:发送一条光线去穿过像素,进而计算出沿这条光线方向上看到了哪些颜色。它可以计算光线与物体表面交点以及交点处的颜色。上述内容会在后续的案例讲解过程中逐步理解的。

我们在此之后将光线统一称为视线

分析一下逻辑:我们看到物体,是外部光照射到物体表面,经过表面反射之后,那部分反射进入眼睛的光被我们捕捉,从而看到了光来源位置的物体,那么,我们假设从眼睛发射一束光,它代表我们的视线,当它沿着某个方向一直向前,视线会与物体表面相交,那么,我们就捕捉到了一个像素。光线追踪器就是计算视线的一种形式。

在开始光线追踪(ray tracking)之前,作者建议先用代码编写一个相机(Camera),同时也编写color方法来返回背景的颜色值。

相机框架(Camera Frame)的定义:将眼睛(eye)或者相机中心点(camera center)放在原点(0,0,0)处。然后遵循右手坐标系的设置要求,y轴正方向向上,x轴正方向向右,z轴正方向向相机外。我们的绘图区域为蓝色框代表的矩形平面,也就是你观察图像的那个屏幕。作者假设一束光线从左下角穿过屏幕,那么我们会用uv两个偏移向量来表示光线与屏幕焦点距左下角的距离。

Ray tracking in One Weekend_第5张图片

现在将原来的Main.cpp修改为下面的代码

#include 
#include 
#include "Ray.h"
using namespace std;


Vec3 Color(const Ray& r)
{
    Vec3 unit_direction = unit_vector(r.direction());
    float t = 0.5f*(unit_direction.y() + 1.0f);

    //(1-t)*白色+t*蓝色,结果是一个蓝白的渐变
    return (1.0f - t)*Vec3(1.0, 1.0, 1.0) + t*Vec3(0.5, 0.7, 1.0);
}

int main()
{

    ofstream outfile;
    outfile.open("firstImage_Ray.ppm");

    int nx = 200;
    int ny = 100;
    outfile << "P3\n" << nx << " " << ny << "\n255\n";

    Vec3 lower_left_corner(-2.0f, -1.0f, -1.0f);
    Vec3 horizontal(4.0f, 0.0f, 0.0f);
    Vec3 vertical(0.0f, 2.0f, 0.0f);
    Vec3 origin(0.0f, 0.0f, 0.0f);


    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 col = Color(r);
            int ir = int(255.99*col[0]);
            int ig = int(255.99*col[1]);
            int ib = int(255.99*col[2]);
            outfile << ir << " " << ig << " " << ib << "\n";
        }
    }
    outfile.close();
    return 0;
}

color函数根据y坐标的上下线性关系来混合白色和蓝色。 我首先将y定义为一个单位向量,所以-1.0 t来完成缩放功能 ,t的取值范围设定为大于0小于1(0.0。 当t=1.0时我能够得到蓝色。 当t=0.0时我能够得到白色。 如果t的值介于[0,1]两者之间,我想要白色和蓝色的混合色。 这形成“线性混合”或“线性插值”或简称“lerp”。

lerp总是以下形式:blended_value =(1-t)* start_value + t * end_value,t的范围为[0,1]

运行Main.cpp得到的结果如下:

Ray tracking in One Weekend_第6张图片

线性插值Lerp()函数

差值公式  blended_value =(1-t)* start_value + t * end_value

t 是一个系数,根据情况确定,将开始颜色和终止颜色进行比例混合,然后得到开始色和终止色组成的混合色。

 

Q:如果我们要做一个从白色到蓝色根据坐标位置确定混合比例进行插值的矩形彩图,我们该如何做呢?

第一步、我们需要确定分辨率,假定为400*200,就用coord1.1的坐标体系去做。

第二步、我们需要确定开始位置和终止位置,假定从左下角混合到右上角,混合颜色为白色和蓝色。

第三步、我们需要确定视线,即确定从眼睛出发到屏幕的向量

1. 确定在平面中的位置,从左下角开始每一个平面位置均由水平和垂直两个分向量叠加而成:

Ray tracking in One Weekend_第7张图片 diagram 3-1

由坐标系统得知,lower-left的坐标为(-2,-1,-1)

若y =(0,0.5,0),x =(1,0,0),则pos =(-1,-0.5,-1)

2.当我们确定了pos之后,其实,视线已经确定好了,因为眼睛的坐标为(0,0,0)pos即为视线向量

第四步、我们需要从分辨率到屏幕做一个映射。

看图还记得我们的分辨率是400*200吗,而屏幕的范围是4*2的矩形。所以,我们需要做一个映射,和上次一样,我们可以将分辨率下的位置通过除法转换到标准坐标,然后再通过标准坐标转换到屏幕坐标。例如一个分辨率下的x的步长为388,首先通过388/400变为一个0~1的实数,然后乘以4,即可变为屏幕步长。然后通过diagram 3-1,确定屏幕中的位置

第五步、我们需要确定比例系数t,我们可以选择x或y方向的其中一个做映射。

因为屏幕坐标系不确定,我们采用的是4*2的,但不是铭文规定的,所以我们需要将视线向量(等同于pos坐标位置)进行单位化,这样的话就可以把每个坐标分量的长度控制在[-1, 1],如果我们把它+1再除以2,那么就完全转化到[0, 1]了,此时,每个坐标方向的分向量范数均为[0, 1],它们是由三个坐标基共同作用而成的独一无二的,所以,你可以采用x的单位化值作为t,也可以把y作为t。

第六步、经过上述一顿操作,我们终于得到了屏幕某个点对应的blend_value,颜色混合值(或称为插值)

至此,我们基于位置进行的颜色插值就讲解完了

#define LOWPRECISION

#include 
#include "ray.h"
using namespace lvgm;

#define stds std::

ray::vec_type lerp(const ray& r)
{
    ray::vec_type unit_dir = r.direction().ret_unitization();    //单位化
    ray::value_type t = 0.5*(unit_dir.y() + 1.0);                //将y分量映射到[0, 1]
    //插值公式 白色&蓝色
    return (1.0 - t)*ray::vec_type(1.0, 1.0, 1.0) + t*ray::vec_type(0.0, 0.0, 1.0);    
}

void build_3_1()
{
    int X = 400, Y = 200;                //分辨率 400*200
    stds ofstream file("graph3-1.ppm");
    if (file.is_open())
    {
        file << "P3\n" << X << " " << Y << "\n255\n";
        ray::vec_type left_bottom{ -2.0,-1.0,-1.0 };    //左下角作为开始位置
        ray::vec_type horizontal{ 4.0,0,0 };            //屏幕水平宽度
        ray::vec_type vertical{ 0,2.0,0 };                //屏幕垂直高度
        ray::vec_type eye{ 0,0,0 };                        //眼睛位置
        for (int j = Y - 1; j >= 0; --j)
            for (int i = 0; i < X; ++i)
            {
                vec2 para(ray::value_type(i) / X, ray::value_type(j) / Y);
                ray r(eye, left_bottom + para.u() * horizontal + para.v() * vertical);
                ray::vec_type color = lerp(r);            //得到插值颜色(rgb)
                int ir = int(255.99*color.r());
                int ig = int(255.99*color.g());
                int ib = int(255.99*color.b());
                file << ir << " " << ig << " " << ib << stds endl;
            }
        file.close();
    }
    else
        stds cerr << "load file failed!" << stds endl;
    stds cout << "complished" << stds endl;
}

int main()
{
    build_3_1();
}

 

Chapter 3 finished 


Chapter 4: Adding a sphere

Before start it:

上一节我们主要将的是插值函数,现在在三维立体空间下进行三维插值。

在背景图上添加一个球体,在平面中显示是一个圆。

Q:如何描述一个球体?

A:球体是非常简单的形体,因为只需要一个球心和一个半径,就能确认球体的位置。

Q:如何判断球体与ray是否相交?若我们在场景中放置一个球体,那么对球体进行采样的时候就会涉及到如何判断射线与球体相交的问题。

A:利用Ray上的一点到圆心的距离()和圆的半径之间的关系来判断Ray和圆的位置关系。分别有相离,相切,相交三种。

计算射线与球体的距离,相交则计算公式有2个根(2 root)即有两个交点,相切则计算公式有一个根(1 root)即唯一交点为切点,相离则计算公式没有根(0 root)即没有交点。

Ray tracking in One Weekend_第8张图片 标题

Q:如何计算距离?

A:坐标和向量表征来计算距离。
回想一下我以前学过的两点之间的距离公式以及球心为原点的球体公式:x*x+y*y+z*z=R*R

对于球心在C(cx.cy.cz)半径为R的球体,点P(x,y,z)和点C之间的距离计算应当满足下面这个等式:

(x-cx)*(x-cx)+(y-cy)*(y-cy)+(z-cz)*(z-cz)=R*R  即点P到点C之间的距离等于圆的半径

dot((p - C),(p - C)) = R*R=(x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz)

计算放到代码中 利用我们在上一节中自己的定义的Vec3中写的dot函数完成向量之间的数量积

//两向量点乘(求内积,得一个常数)(for locations)
inline float dot(const Vec3 &v1, const Vec3 &v2) {
    return v1.e[0] * v2.e[0] + v1.e[1] * v2.e[1] + v1.e[2] * v2.e[2];
}

(即我们熟悉的\underset{A}{\rightarrow}\cdot \underset{B}{\rightarrow}= (a_{1}*a_{2}+b_{1}*b_{2}+c_{1}*c_{2}),其中各向量分别为\underset{A}{\rightarrow}= (a_{1},b_{1},c_{1}) \underset{B}{\rightarrow}= (a_{2},b_{2},c_{2})

好了,到了这里我们就掌握了如何通过向量计算距离值。

Q:如何判断Ray与圆的距离?

A:记得我们上一节对Ray的处理

我们将光线定义为公式      p(t)=A+t*B

P是光线指向的目标位置(3D position along a line),A是光线的起点(ray origin),B是光线指向的方向(ray direction),t则是光线步长。

把我们刚刚讲到的点P变成P(t),则相切时的距离 dot((p(t)−C),(p(t)−C))=R∗R 也等同于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 这就是我们熟悉的一元二次方程,根据求根公式来判断方程有无根,进而判断Ray与圆的位置关系。OK 想法说完了,下面再代码里实现叭...

修改Main.cpp中的部分代码修改如下:

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.0f*dot(oc, r.direction());
    float c = dot(oc, oc) - radius*radius;
    float discrimiant = b*b - 4.0f*a*c;
    return (discrimiant > 0.0f);
}

Vec3 Color(const Ray& r)
{
    if (hit_sphere(Vec3(0.0f, 0.0f, -1.0f), 0.5f, r))
        return Vec3(1.0f, 0.0f, 0.0f);

    Vec3 unit_direction = unit_vector(r.direction());
    float t = 0.5f*(unit_direction.y() + 1.0f);

    //(1-t)*白色+t*蓝色,结果是一个蓝白的渐变
    return (1.0f - t)*Vec3(1.0f, 1.0f, 1.0f) + t*Vec3(0.5f, 0.7f, 1.0f);
}

最终得到的结果如下:

Ray tracking in One Weekend_第9张图片 Output_sphere

Chapter 4 finished


Chapter 5: Surface normals and multiple objects.

平面法向量以及多个物体

首先,我们需要找到一个平面法向量。众所周知,法向量是和平面垂直的。对于球体而言,法向量的方向表示是撞击点P(即Ray和球体接触的hitpoint) 减去球体中心C,如下:(For a sphere, the normal is in the direction of the hitpoint minus the center:)

Ray tracking in One Weekend_第10张图片 Surface normals

修改代码来实现找到球体上的不同面产生不同的颜色。

我们不想让球体仅仅是红色了,我们也想让它可以渐变,那么就可以用到它的法向量,让法向量的XYZ值映射到RGB值即可!

再Main.cpp中修改代码如下:

//注意与Chapter4不同,此时该函数的返回值以从bool变为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.0f*dot(oc, r.direction());
    float c = dot(oc, oc) - radius*radius;
    float discrimiant = b*b - 4.0f*a*c;
    if (discrimiant < 0.0f)
    {
        return -1.0f;
    }
    else
    {
        return (-b - sqrt(discrimiant)) / (2.0f*a);
    }
}

Vec3 Color(const Ray& r)
{
    float t = hit_sphere(Vec3(0.0f, 0.0f, -1.0f), 0.5, r);
    if (t > 0.0f)
    {
        //法向量
        Vec3 N = unit_vector(r.point_at_parameter(t) - Vec3(0.0f, 0.0f, -1.0f));
        return 0.5f*Vec3(N.x() + 1.0f, N.y() + 1.0f, N.z() + 1.0f);
    }
    //绘制背景
    Vec3 unit_direction = unit_vector(r.direction());
    t = 0.5f*(unit_direction.y() + 1.0f);
    //(1-t)*白色+t*蓝色,结果是一个蓝白的渐变
    return (1.0f - t)*Vec3(1.0f, 1.0f, 1.0f) + t*Vec3(0.5f, 0.7f, 1.0f);
}

得到的结果如下:

Ray tracking in One Weekend_第11张图片 Output_sphere_with_surface_normal

 

你可能感兴趣的:(图形学)