Create My Ray Tracing Renderer

This is a study record of Ray Tracing in One Weekend.

Output My First Image

PPM Image format

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

int main() {

// Image

int image_width = 256;
int image_height = 256;

// Render

std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";

for (int j = 0; j < image_height; ++j) {
for (int i = 0; i < image_width; ++i) {
auto r = double(i) / (image_width-1);
auto g = double(j) / (image_height-1);
auto b = 0;

int ir = static_cast<int>(255.999 * r);
int ig = static_cast<int>(255.999 * g);
int ib = static_cast<int>(255.999 * b);

std::cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
}
  • static_cast<int> is used to convert double to int

The output is a text content, shows something like:

1
2
3
4
5
6
7
P3
256 256
255
0 0 0
1 0 0
2 0 0
3 0 0

The P3 means colours are in ASCII. The columns number is 256, and the rows number is 256. The max colour is 255.

Redirect to an image file

To view the image directly, we need to redirect the output file like this:
./helloworld > image.ppm .
./helloworld is the name of my output c++ file, and image.ppm is the image file.
The result of image.ppm looks like this:

Since the rows are written from top to bottom, and columns are written from left to right, it makes sense that the top left is dark and the bottom right is black.

Adding a Progress Indicator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (int j = 0; j < image_height; ++j) {
// Adding Progress Indicator
std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
auto r = double(i) / (image_width-1);
auto g = double(j) / (image_height-1);
auto b = 0;

int ir = static_cast<int>(255.999 * r);
int ig = static_cast<int>(255.999 * g);
int ib = static_cast<int>(255.999 * b);

std::cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
std::clog << "\rDone. \n";

Since the program outputs the image to the standard output stream std::cout, we use std::clog to record our progress. The output of std::clog is stored in a buffer until the buffer is flushed.

std::flush forces the buffered output in std::clog to be immediately written to its output (usually to the terminal).

Setting Vec3 Class

We create a vec3.h file to define the class vec3, and then include it in the main file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#ifndef VEC3_H
#define VEC3_H

#include <cmath>
#include <iostream>

using std::sqrt;

class vec3
{
public:
// defines an arry named 'e' of size 3, and each element's type is double
double e[3];
// initialise the elements of the array e with 0
vec3() : e{0, 0, 0} {}
vec3(double e0, double e1, double e2) : e{e0, e1, e2} {}

double x() const { return e[0]; }
double y() const { return e[1]; }
double z() const { return e[2]; }

// negations of the vector
vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); }
double operator[](int i) const { return e[i]; }
double &operator[](int i) { return e[i]; }

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

vec3 &operator*=(double t)
{
e[0] *= t;
e[1] *= t;
e[2] *= t;
return *this;
}

vec3 &operator/=(double t)
{
return *this *= 1 / t;
}

double length() const
{
return sqrt(length_squared());
}

double length_squared() const
{
return e[0] * e[0] + e[1] * e[1] + e[2] * e[2];
}
};

// alias for vec3, for geometric clarity
using point3 = vec3;

// Vector Utility Functions

inline std::ostream &operator<<(std::ostream &out, const vec3 &v)
{
return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2];
}

inline vec3 operator+(const vec3 &u, const vec3 &v)
{
return vec3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]);
}

inline vec3 operator-(const vec3 &u, const vec3 &v)
{
return vec3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]);
}

inline vec3 operator*(double t, const vec3 &v)
{
return vec3(t * v.e[0], t * v.e[1], t * v.e[2]);
}

inline vec3 operator*(const vec3 &v, double t)
{
return t * v;
}

inline vec3 operator/(vec3 v, double t)
{
return (1 / t) * v;
}

inline double dot(const vec3 &u, const vec3 &v)
{
return u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2];
}

inline vec3 cross(const vec3 &u, const vec3 &v)
{
return vec3(u.e[1] * v.e[2] - u.e[2] * v.e[1],
u.e[2] * v.e[0] - u.e[0] * v.e[2],
u.e[0] * v.e[1] - u.e[1] * v.e[0]);
}

inline vec3 unit_vector(vec3 v)
{
return v / v.length();
}

#endif

Color Utility Functions

We create a color.h file to define the color class, and then include it in the main file. It is worth to note that the color class is an alias for the vec3 class, so there is no main difference between the two classes but we can recognize the color class more easily.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef COLOR_H
#define COLOR_H

#include "vec3.h"

#include <iostream>

using color = vec3;

void write_color(std::ostream &out, color pixel_color)
{
// Write the translated [0, 255] of each color component
out << static_cast<int>(259.99 * pixel_color.x()) << ' '
<< static_cast<int>(259.99 * pixel_color.y()) << ' '
<< static_cast<int>(259.99 * pixel_color.z()) << '\n';
}
#endif

Rewrite our main file

After we have defined the vec3 class and the color class, we can rewrite our main file like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "vec3.h"
#include "color.h"

#include <iostream>

int main()
{
int image_width = 256;
int image_height = 256;

std::cout << "P3\n"
<< image_width << ' ' << image_height << "\n255\n";

for (int j = 0; j < image_height; ++j)
{
std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush;
for (int i = 0; i < image_width; ++i)
{
auto pixel_color = color(double(i) / (image_width - 1), double(j) / (image_height - 1), 0);
write_color(std::cout, pixel_color);
}
}
std::clog << "\rDone. \n";
}

Ray

Ray Class

We create a ray.h file to define the ray class. The ray class has two member variables: orig and dir, which are the origin and direction of the ray, respectively. The at method returns the point at the parameter t along the ray.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef RAY_H
#define RAY_H

#include "vec3.h"

class ray {
public:
ray() {}

// Initializes the member variables orig and dir of the ray object with the values passed to the constructor.
ray(const point3& origin, const vec3& direction): orig(origin), dir(direction){}

point3 origin() const {return orig;}
vec3 direction() const {return dir;}

point3 at(double t) const{
return orig + t * dir;
}

private:
point3 orig;
vec3 dir;
};

#endif

Sending Rays into the Scene

Viewport: a virtual rectangle in the 3D world that contains the grid of image pixel locations.

If pixels are spaced the same distance horizontally as they are vertically, the viewport that bounds them will have the same aspect ratio as the rendered image.

Pixel spacing: the distance between two adjacent pixels, and square pixels is the standard.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2020-2024 Yangyang Cui

请我喝杯咖啡吧~

支付宝
微信