Note on "Ray tracing in one week" - Part 0

Note on "Ray tracing in one week" - Part 0

Ayano Kagurazaka Lv3

The code is compiled with cmake, please check out CMakeLists.txt

Note on “Ray tracing in one week” - Part 0

In part 1 we implemented a simple camera, associated viewport, and a simple ray tracing algorithm. In this part, we will wrap some code with OOP.

Wrapper

first take a look at our main function:

Associated code

main.cpp
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
#include "ImageUtil.h"
#include "MathUtil.h"
#include <vector>

ImageUtil::Color rayColor(const MathUtil::Ray& ray) {
auto unit = ray.dir().unit();
// spdlog::info(unit.y);
auto progx = std::min(0.5 * unit.x + 1, 1.0);
auto progy = std::min(0.5 * unit.y + 1, 1.0);
return {progx, progy, 1};
}


int main() {
// image0 dimension
int width = 1920;
auto aspect = 16.0 / 9.0;
int height = static_cast<int>(width / aspect);
height = height < 1 ? 1 : height;

// camera
auto focal_len = 1.0;

auto viewport_width = 2.0;
auto viewport_height = viewport_width / (static_cast<double>(width) / height);
auto camera_center = MathUtil::Vec3(0, 0, 0);

auto viewport_vec_h = MathUtil::Vec3(viewport_width, 0, 0);
auto viewport_vec_v = MathUtil::Vec3(0, -viewport_height, 0);

auto pix_delta_h = viewport_vec_h / width;
auto pix_delta_v = viewport_vec_v / height;

auto viewport_ul = camera_center - MathUtil::Vec3(0, 0, focal_len) - viewport_vec_h / 2 - viewport_vec_v / 2;


auto pixel_00 = viewport_ul + (pix_delta_h + pix_delta_v) * 0.5;

auto img = std::vector<std::vector<ImageUtil::Color>>();
for (int i = 0; i < height; ++i) {
auto v = std::vector<ImageUtil::Color>();
spdlog::info("line remaining: {}", (height - i + 1));
for (int j = 0; j < width; ++j) {
auto pix = pixel_00 + pix_delta_h * j + pix_delta_v * i;
auto ray_dir = pix - camera_center;
auto ray = MathUtil::Ray(camera_center, ray_dir);
auto color = rayColor(ray);
v.emplace_back(color);
}
img.emplace_back(v);
}
ImageUtil::makePPM(width, height, img, "./out", "test.ppm");
return 0;
}

Hmmmmm… it’s a little bit messy. Let’s wrap some code with OOP.

Camera

We know, a camera has associated ratio, view port, dimension, a center, and several associated vectors. So we can wrap them into a class.

Since Cameras are related to graphics, I will name the file GraphicObjects. We don’t have to initialize any vectors yet, we will do it later with a member function. Now we need to set aspect ratio and calculate height.

GraphicObjects.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Camera {
public:
Camera(int width, double aspect_ratio, double viewport_width, double focal_len, MathUtil::Point3 position);

private:
int width;
int height;
double aspect_ratio;
double viewport_width;
double viewport_height;
double focal_len;
MathUtil::Point3 position;
MathUtil::Vec3 hori_vec;
MathUtil::Vec3 vert_vec;
MathUtil::Vec3 pix_delta_x;
MathUtil::Vec3 pix_delta_y;
MathUtil::Point3 viewport_ul;
MathUtil::Point3 pixel_00;
};
GraphicObjects
1
2
3
4
5
6
7
#include "GraphicObjects.h"
#include <utility>

Camera::Camera(int width, double aspect_ratio, double viewport_width, double focal_len,
MathUtil::Point3 position) : width(width), aspect_ratio(aspect_ratio), viewport_width(viewport_width), focal_len(focal_len), position(std::move(position)), height(static_cast<int>(width / aspect_ratio)), viewport_height(viewport_width / (static_cast<double>(width) / static_cast<double>(height))) {
}

Now we write a updateVector() function to update all vectors since we will call that very often.

1
2
3
4
5
6
7
8
void Camera::updateVectors() {
hori_vec = MathUtil::Vec3(viewport_width, 0, 0);
vert_vec = MathUtil::Vec3(0, -viewport_height, 0);
pix_delta_x = hori_vec / width;
pix_delta_y = vert_vec / height;
viewport_ul = position - MathUtil::Vec3(0, 0, focal_len) - (vert_vec + hori_vec) / 2;
pixel_00 = viewport_ul + (pix_delta_y + pix_delta_x) * 0.5;
}

We see that in our main function, we have to calculate all the pixel positions and the ray to that position, so we can write two function to do that.

1
2
3
4
5
6
7
MathUtil::Vec3 Camera::getPixelVec(int x, int y) const {
return pixel_00 + pix_delta_x * x + pix_delta_y * y;
}

MathUtil::Vec3 Camera::getPixRayDir(int x, int y) const {
return position - getPixelVec(x, y);
}

Now we apply these changes to our main function

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
#include "ImageUtil.h"
#include "MathUtil.h"
#include "GraphicObjects.h"
#include <vector>
#include <cmath>

ImageUtil::Color rayColor(const MathUtil::Ray& ray) {
auto unit = ray.dir().unit();
auto progx = std::min(0.5 * unit.x + 1, 1.0);
auto progy = std::min(0.5 * unit.y + 1, 1.0);
return {progx, progy, 1};
}


int main() {
auto camera = Camera(1920, 16.0 / 9.0, 2.0, 1, MathUtil::Point3(0, 0, 0));
auto img = std::vector<std::vector<ImageUtil::Color>>();
for (int i = 0; i < camera.getHeight(); ++i) {
auto v = std::vector<ImageUtil::Color>();
if (i % 10 == 0 || i > camera.getHeight() - 30) spdlog::info("line remaining: {}", (camera.getHeight() - i + 1));
for (int j = 0; j < camera.getWidth(); ++j) {
auto ray_dir = camera.getPixRayDir(j, i);
auto ray = MathUtil::Ray(camera.getPosition(), ray_dir);
auto color = rayColor(ray);
v.emplace_back(color);
}
img.emplace_back(v);
}
ImageUtil::makePPM(camera.getWidth(), camera.getHeight(), img, "./out", "test.ppm");
}

And we get the same result, with a large amount of code reduced.

result

Associated files

GraphicObjects.h
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

#ifndef ONEWEEKEND_GRAPHICOBJECTS_H
#define ONEWEEKEND_GRAPHICOBJECTS_H

#include "MathUtil.h"
#include <utility>

class Camera {
public:
Camera(int width, double aspect_ratio, double viewport_width, double focal_len, MathUtil::Point3 position);

int getWidth() const;

int getHeight() const;

double getAspectRatio() const;

double getViewportWidth() const;

double getViewportHeight() const;

double getFocalLen() const;

const MathUtil::Point3 &getPosition() const;

const MathUtil::Vec3 &getHoriVec() const;

const MathUtil::Vec3 &getVertVec() const;

const MathUtil::Vec3 &getPixDeltaX() const;

const MathUtil::Vec3 &getPixDeltaY() const;

const MathUtil::Point3 &getViewportUl() const;

const MathUtil::Point3 &getPixel00() const;

void setWidth(int width);

void setAspectRatio(double aspect_ratio);

void setFocalLen(double focal_len);

void setPosition(const MathUtil::Point3 &position);

[[nodiscard]] MathUtil::Vec3 getPixelVec(int x, int y) const;

[[nodiscard]] MathUtil::Vec3 getPixRayDir(int x, int y) const;

private:

void updateVectors();

int width;
int height;
double aspect_ratio;
double viewport_width;
double viewport_height;
double focal_len;
MathUtil::Point3 position;
MathUtil::Vec3 hori_vec;
MathUtil::Vec3 vert_vec;
MathUtil::Vec3 pix_delta_x;
MathUtil::Vec3 pix_delta_y;
MathUtil::Point3 viewport_ul;
MathUtil::Point3 pixel_00;
};

Associated files

GraphicObjects.cpp
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
Camera::Camera(int width, double aspect_ratio, double viewport_width, double focal_len, MathUtil::Point3 position) : width(width), aspect_ratio(aspect_ratio), viewport_width(viewport_width), focal_len(focal_len), position(std::move(position)), height(static_cast<int>(width / aspect_ratio)),viewport_height(viewport_width / (static_cast<double>(width) / static_cast<double>(height))) {
updateVectors();
}

void Camera::updateVectors() {
hori_vec = MathUtil::Vec3(viewport_width, 0, 0);
vert_vec = MathUtil::Vec3(0, -viewport_height, 0);
pix_delta_x = hori_vec / width;
pix_delta_y = vert_vec / height;
viewport_ul = position - MathUtil::Vec3(0, 0, focal_len) - (vert_vec + hori_vec) / 2;
pixel_00 = viewport_ul + (pix_delta_y + pix_delta_x) * 0.5;
}

int Camera::getWidth() const {
return width;
}

int Camera::getHeight() const {
return height;
}

double Camera::getAspectRatio() const {
return aspect_ratio;
}

double Camera::getViewportWidth() const {
return viewport_width;
}

double Camera::getViewportHeight() const {
return viewport_height;
}

double Camera::getFocalLen() const {
return focal_len;
}

const MathUtil::Point3 &Camera::getPosition() const {
return position;
}

const MathUtil::Vec3 &Camera::getHoriVec() const {
return hori_vec;
}

const MathUtil::Vec3 &Camera::getVertVec() const {
return vert_vec;
}

const MathUtil::Vec3 &Camera::getPixDeltaX() const {
return pix_delta_x;
}

const MathUtil::Vec3 &Camera::getPixDeltaY() const {
return pix_delta_y;
}

const MathUtil::Point3 &Camera::getViewportUl() const {
return viewport_ul;
}

const MathUtil::Point3 &Camera::getPixel00() const {
return pixel_00;
}

void Camera::setWidth(int width) {
Camera::width = width;
height = static_cast<int>(width / aspect_ratio);
updateVectors();
}

void Camera::setAspectRatio(double aspect_ratio) {
Camera::aspect_ratio = aspect_ratio;
height = static_cast<int>(width / aspect_ratio);
viewport_height = viewport_width / aspect_ratio;
updateVectors();
}

void Camera::setFocalLen(double focal_len) {
Camera::focal_len = focal_len;
updateVectors();
}

void Camera::setPosition(const MathUtil::Point3 &position) {
Camera::position = position;
updateVectors();
}

MathUtil::Vec3 Camera::getPixelVec(int x, int y) const {
return pixel_00 + pix_delta_x * x + pix_delta_y * y;
}

MathUtil::Vec3 Camera::getPixRayDir(int x, int y) const {
return getPixelVec(x, y) - position;
}

Spoiler alert: if you haven’t read part 2 yet, please don’t proceed since you might not know what is going on


Sphere, hittable object, and composite pattern

In part 2, we used a sphere, and we will use more spheres in the future. So it’s a good idea to wrap them into a class: Sphere. Because this is a Graphic Object, we will put it in the GraphicObjects.h file.

GraphicObjects.h : Sphere

1
2
3
4
5
6
7
class Sphere {
public:
Sphere(double radius, MathUtil::Vec3 position);
private:
double radius;
MathUtil::Vec3 position;
};

GraphicObjects.cpp : Sphere

1
2
3
Sphere::Sphere(double radius, MathUtil::Vec3 position) : radius(radius), position(std::move(position)) {

}

Now we need to implement whether a ray hits the sphere or not. We can certainly write a function in the Sphere class. But… maybe we can use polymorphism to make it more flexible? Let’s create a Hittable interface and do the composite pattern.

icon-padding

Note that we also need a HitRecord struct to record the hit information.

GraphicObjects.h : Hittable & HitRecord

1
2
3
4
5
6
7
8
9
10
11
12
13
struct HitRecord {
bool hit;
MathUtil::Point3 p;
double t;
MathUtil::Vec3 normal;
};

class IHittable {
public:
virtual ~IHittable() = default;

virtual bool hit(const MathUtil::Ray& r, double ray_tmin, double ray_tmax, HitRecord& record) const = 0;
};

Now we let Sphere compose IHittable and implement the hit function.

GraphicObjects.h : Sphere

1
2
3
4
5
6
7
8
9
10
class Sphere : public IHittable {
public:
Sphere(double radius, MathUtil::Vec3 position);

bool hit(const MathUtil::Ray &r, double ray_tmin, double ray_tmax, HitRecord& record) const override;

private:
double radius;
MathUtil::Vec3 position;
};

GraphicObjects.cpp : Sphere

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool Sphere::hit(const MathUtil::Ray &r, double ray_tmin, double ray_tmax, HitRecord& record) const {
MathUtil::Point3 oc = r.pos() - position;
auto a = r.dir().lengthSq();
auto h = oc.dot(r.dir());
auto c = oc.lengthSq() - radius * radius;

auto discriminant = h * h - a * c;
if (discriminant < 0) return false;
auto discri_sqrt = std::sqrt(discriminant);

auto root = (-h - discri_sqrt) / a;
if (root <= ray_tmin || ray_tmax <= root) {
root = (-h + discri_sqrt) / a;
if (root <= ray_tmin || ray_tmax <= root){
return false;
}
}
record.t = root;
record.p = r.at(root);
record.normal = (record.p - position) / radius;

return true;
}

Now let’s test that out in our main function:

main.cpp : rayColor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ImageUtil::Color rayColor(const MathUtil::Ray& ray) {
auto sphere = Sphere(2, {0, 0, -5});
ImageUtil::Color color;
HitRecord record;
if (sphere.hit(ray, 0, 2147483647, record)) {
color = {record.normal.x + 1, record.normal.y + 1, record.normal.z + 1};
color *= 0.5;
}
else {
auto unit = ray.dir().unit();
color = ImageUtil::Color(std::min(0.5 * unit.x + 1, 1.0), std::min(0.5 * unit.y + 1, 1.0), 1);
}
return color;
}

result2

Now we get a sphere!

Associated files

GraphicObjects.h
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

// no change...

struct HitRecord {
bool hit;
MathUtil::Point3 p;
double t;
MathUtil::Vec3 normal;
};

class IHittable {
public:
virtual ~IHittable() = default;

virtual bool hit(const MathUtil::Ray& r, double ray_tmin, double ray_tmax, HitRecord& record) const = 0;
};

class Sphere : public IHittable {
public:
Sphere(double radius, MathUtil::Vec3 position);

bool hit(const MathUtil::Ray &r, double ray_tmin, double ray_tmax, HitRecord& record) const override;

private:
double radius;
MathUtil::Vec3 position;
};

// no change...
GraphicObjects.cpp
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

// no change...

Sphere::Sphere(double radius, MathUtil::Vec3 position) : radius(radius), position(std::move(position)) {

}

bool Sphere::hit(const MathUtil::Ray &r, double ray_tmin, double ray_tmax, HitRecord& record) const {
MathUtil::Point3 oc = r.pos() - position;
auto a = r.dir().lengthSq();
auto h = oc.dot(r.dir());
auto c = oc.lengthSq() - radius * radius;

auto discriminant = h * h - a * c;
if (discriminant < 0) return false;
auto discri_sqrt = std::sqrt(discriminant);

auto root = (-h - discri_sqrt) / a;
if (root <= ray_tmin || ray_tmax <= root) {
root = (-h + discri_sqrt) / a;
if (root <= ray_tmin || ray_tmax <= root){
return false;
}
}
record.t = root;
record.p = r.at(root);
record.normal = (record.p - position) / radius;

return true;
}

// no change...


Patch and tweak

Patch

In the code we wrote in part 4, there is a problem related to the floating point precision: when the ray hit the sphere, it will slightly up or down from the point it supposed to be hitting, and it will intersect with the sphere again with a very small t if it is slightly down from the sphere surface. We can fix this by restricting the smallest value for t.

icon-padding

GlobUtil.hpp

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

#include <limits>

const double PI = 3.1415926;
const double INF = std::numeric_limits<double>::infinity();
const double EPS = 1e-5;


inline double deg2Rad(double deg) {
return deg * PI / 180.0;
}

#endif // GLOBUTIL_HPP

icon-padding

GraphicObjects.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
ImageUtil::Color Camera::rayColor(const MathUtil::Ray &ray, const IHittable &object) {
ImageUtil::Color color;
HitRecord record;
if (object.hit(ray, MathUtil::Interval(EPS, INF), record)) {
auto refl = MathUtil::Vec3::randomUnitVec3InHemiSphere(record.normal);
color = 0.5 * rayColor(MathUtil::Ray(record.p, refl), object);
}
else {
auto unit = ray.dir().unit();
color = ImageUtil::Color(std::min(0.5 * unit.x + 1, 1.0), std::min(0.5 * unit.y + 1, 1.0), 1);
}
return color;
}

Now let’s run our main function again… seg fault?

Use debugger to check call stack, we can see that the rayColor function is called recursively every time it hits something and on my situation it hits 100+ times or so. We can fix this by allocating memory on heap rather than stack and delete one temp variable. We can also change the stop condition to the point when the ray reflected some times.

icon-padding

GraphicObjects.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

//...

int getRenderDepth() const;

void setRenderDepth(int renderDepth);

int getRenderThreadCount() const;

void setRenderThreadCount(int renderThreadCount);
//...
private:

ImageUtil::Color rayColor(const MathUtil::Ray &ray, const IHittable &object, int depth);

//...

int render_depth = 50;
int render_thread_count = 0;

icon-padding

GraphicObjects.cpp

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
void Camera::Render(const IHittable& world, const std::string& path, const std::string& name) {
auto now = std::chrono::system_clock::now();
spdlog::info("rendering started!");
int worker_cnt;
if(render_thread_count == 0) {
worker_cnt = std::thread::hardware_concurrency() == 0 ? 12 : std::thread::hardware_concurrency();
}
else {
worker_cnt = render_thread_count;
}
spdlog::info("using {} threads", worker_cnt);
auto displacement = getHeight() / worker_cnt;
auto workers = std::vector<std::future<std::vector<std::vector<ImageUtil::Color>>>>();
for (int i = 0; i < worker_cnt - 1; ++i) {
auto f = std::async(std::launch::async, &Camera::RenderWorker, this, std::ref(world), i * displacement, (i + 1) * displacement);
workers.push_back(std::move(f));
}
auto f = std::async(std::launch::async, &Camera::RenderWorker, this, std::ref(world), (worker_cnt - 1) * displacement, getHeight());
workers.push_back(std::move(f));
auto img = std::vector<std::vector<ImageUtil::Color>>();
for (auto& i : workers) {
auto res = i.get();
img.insert(img.end(), res.begin(), res.end());
}
auto end = std::chrono::system_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - now);
spdlog::info("finished! rendering time: {}s", static_cast<double>(elapsed.count()) / 1000.0);
ImageUtil::makePPM(getWidth(), getHeight(), img, path, name);
}

//...

ImageUtil::Color Camera::rayColor(const MathUtil::Ray &ray, const IHittable &object, int depth) {
HitRecord record;
if(depth <= 0) return {0, 0, 0};
if (object.hit(ray, MathUtil::Interval(EPS, INF), record)) {
auto refl = std::make_shared<MathUtil::Vec3>(MathUtil::Vec3::randomUnitVec3InHemiSphere(record.normal));
return 0.5 * rayColor(MathUtil::Ray(record.p, *refl), object, depth - 1);
}
else {
auto unit = ray.dir().unit();
return ImageUtil::Color(std::min(0.5 * unit.x + 1, 1.0), std::min(0.5 * unit.y + 1, 1.0), 1);
}
}

//...

std::vector<std::vector<ImageUtil::Color>> Camera::RenderWorker(const IHittable &world, int start, int end) {
auto img = std::vector<std::vector<ImageUtil::Color>>();
std::stringstream ss;
ss << std::this_thread::get_id();
spdlog::info("thread {} (from {} to {}) started!", ss.str(), start, end - 1);
auto range = end - start;
auto begin = std::chrono::system_clock::now();
for (int i = start; i < end; ++i) {
auto v = std::vector<ImageUtil::Color>();
if (i % (range / 3) == 0) {
spdlog::info("line remaining for thread {}: {}", ss.str(), (range - (i - start) + 1));
}
for (int j = 0; j < getWidth(); ++j) {
ImageUtil::Color pixel_color = {0, 0, 0};
for (int k = 0; k < sample_count; ++k) {
auto ray_dir = getPixRayDir(j, i);
auto ray = MathUtil::Ray(getPosition(), ray_dir);
pixel_color += rayColor(ray, world, render_depth);
}
pixel_color /= sample_count;
v.emplace_back(pixel_color);
}
img.emplace_back(v);
}
auto finish = std::chrono::system_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(finish - begin);
spdlog::info("thread {} (from {} to {}) finished! time: {}s", ss.str(), start, end - 1, static_cast<double>(elapsed.count()) / 1000.0);
return img;
}

//...

int Camera::getRenderDepth() const {
return render_depth;
}
void Camera::setRenderDepth(int renderDepth) {
render_depth = renderDepth;
}
int Camera::getRenderThreadCount() const {
return render_thread_count;
}
void Camera::setRenderThreadCount(int renderThreadCount) {
render_thread_count = renderThreadCount;
}

We modify our main function a little bit, and we get the result we want.

icon-padding

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "ImageUtil.h"
#include "GraphicObjects.h"

int main() {
auto camera = Camera(1920, 16.0 / 9.0, 2.0, 1, MathUtil::Point3(0, 0, 0));
auto world = HittableList();
world.add(std::make_shared<Sphere>(Sphere(80, {0, -85, -20})));
world.add(std::make_shared<Sphere>(Sphere(5, {-5, 0, -20})));
world.add(std::make_shared<Sphere>(Sphere(5, {5, 0, -20})));
camera.setSampleCount(100);
camera.setRenderDepth(50);
camera.setRenderThreadCount(12);
camera.Render(world, "out", "test.ppm");
return 0;
}

The result:

multithread

looks the same as single threaded rendering, and with 12 threads, it only takes 130s to render the image.

With one thread, it takes 385s to render the image, and the result is shown below:

singlethread

Which is the same as the multithreaded one. So multthreading works!

Comments