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

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

Ayano Kagurazaka Lv3

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

If we take a look on our result, we can see it’s good. But if we take a closer look, we will see there are little “stairs” between on the edge of the sphere. In 1920 width, it’s not really noticeable, but if we use 480 as the width, it will look like this:

stairs

This is because our image is drawn pixel by pixel. In real world, the resolution is sufficiently large, so we can’t see the “stairs”. But in computer graphics, we emulate real world images with pixels, so we can see the “stairs”. Solving this problem is called “anti-aliasing”.

Anti-aliasing

Imagine we are viewing a chess board close up, we see black and white grids-like stairs. But if we view it from a distance, we see gray instead of the grids-like “stairs”. This is because the black and white grids are mixed together and eliminated the stair effect, which can be used in our rendering.

With this in mind, we can implement our “anti-aliasing” algorithm. The idea is simple and straightforward: sample the light rays falling around the pixel, and then mix them together. The range for us to pick the sampling point will be a square around the pixel with a side length of a pixel. Picking method will simply depend on a random number generator.

icon-padding

dashed lines are random sampling points

In post-C++11, we can use the <random> library to generate random numbers. Here is the code:

MathUtil.h

1
2
3
4
5
inline double randomDouble() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}

icon-padding

We use inline to avoid multiple symbol lookup which impact performance.

After random sampling, we will mix the color together by taking the average of the color. To do this, we will modify the Camera class:

GraphicObjects.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Camera {
public:
// ...
int getSampleCount() const;
// ...
void setSampleCount(int sample_count);
// ...
private:
// ...
MathUtil::Vec3 randomDisplacement() const;
// ...
int sample_count = 20;
// ...
};

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

void Camera::Render(const IHittable& world) {
auto img = std::vector<std::vector<ImageUtil::Color>>();
auto now = std::chrono::system_clock::now();
for (int i = 0; i < getHeight(); ++i) {
auto v = std::vector<ImageUtil::Color>();
if (i % 10 == 0 || i > getHeight() - 10) spdlog::info("line remaining: {}", (getHeight() - i + 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);
}
pixel_color /= sample_count;
v.emplace_back(pixel_color);
}
img.emplace_back(v);
}
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, "out", "test.ppm");
}

// ...

void Camera::setSampleCount(int sample_count) {
Camera::sample_count = sample_count;
}

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

// ...

ImageUtil::Color Camera::rayColor(const MathUtil::Ray &ray, const IHittable &object) {
ImageUtil::Color color;
HitRecord record;
if (object.hit(ray, MathUtil::Interval(0, INF), record)) {
color = 0.5 * (record.normal + ImageUtil::Color(1, 1, 1));
}
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;
}
int Camera::getSampleCount() const {
return sample_count;
}
MathUtil::Vec3 Camera::randomDisplacement() const {
auto delta_x = pix_delta_x * (MathUtil::randomDouble() - 0.5);
auto delta_y = pix_delta_y * (MathUtil::randomDouble() - 0.5);
return delta_x + delta_y;
}

// ...

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//...

int main() {
auto camera = Camera(480, 16.0 / 9.0, 2.0, 1, MathUtil::Point3(0, 0, 0));
auto world = HittableList();
world.add(std::make_shared<Sphere>(Sphere(10, {0, -10, -5})));
world.add(std::make_shared<Sphere>(Sphere(2, {5, 0, -5})));
world.add(std::make_shared<Sphere>(Sphere(2, {-5, 0, -5})));
world.add(std::make_shared<Sphere>(Sphere(2, {0, 0, -15})));
camera.setSampleCount(100);
camera.Render(world);

return 0;
}

The effect is appearent:

aa

However, on my computer, it takes me 6.481s to render this image. This is because we are resampling the light rays 100 times for each pixel, which means 100 times slower than the original rendering. We can use multi-threading to speed up the rendering process. But this is not the focus of this note, so I will not cover it here.

Diffuse material

We now have shading on our sphere, the next step is to do texturing. We will start with diffuse material.

When a ray hits a surface, it will diffuse in all directions. This is because the surface is not smooth. In our case, we will just randomize the direction of the diffused ray to simulate this effect.

diffuse

icon-padding

dashed lines are random diffused rays

First we will make some helper functions to generate random vectors:

MathUtil.h

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

//...

inline double randomDouble(double min, double max) {
return min + (max - min) * randomDouble();
}

inline Vec3 randomVec3() {
return {randomDouble(), randomDouble(), randomDouble()};
}

inline Vec3 randomVec3(double min, double max) {
return Vec3(randomDouble(min, max), randomDouble(min, max), randomDouble(min, max));
}

//...

To make our choise of random ray direction more uniform, we will only choose the ray end up in the unit sphere. This is because if we pick a random vector in a cube, the vectors near the corner will be more likely to be picked. This is because the volume of the corner is smaller than the volume of the center. To do this, we will use the rejection method. Here is the code:

1
2
3
4
5
6
7
Vec3 randomVec3InUnitSphere() {
while (true) {
auto p = randomVec3(-1, 1);
if (p.lengthSq() >= 1) continue;
return p;
}
}

After we picked a desired vector, we will normalize it:

1
2
3
Vec3 randomUnitVec3() {
return randomVec3InUnitSphere().unit();
}

Then we will make sure that the random vector is in the correct direction by checking whether it’s on the same hemisphere as the normal:

1
2
3
4
5
Vec3 randomUnitVec3(const Vec3 &normal) {
auto vec = randomUnitVec3();
if (vec.dot(normal) > 0.0) return vec;
return -vec;
}

Now we can modify our rayColor function to simulate the diffuse material:

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(0, 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;
}

And we will modify our main function to make rendering take less time and demonstrate the effect:

main.cpp

1
2
3
4
5
6
7
8
9
10
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, {0, 0, -20})));
camera.setSampleCount(100);
camera.Render(world);

return 0;
}

The result looks good:

diffuse

Associated file

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
#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, {0, 0, -20})));
camera.setSampleCount(100);
camera.Render(world);

return 0;
}
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
class Camera {
public:
//...

int getSampleCount() const;

//...

void setSampleCount(int sample_count);

//...

void updateVectors();

MathUtil::Vec3 randomDisplacement() const;

int width;
int height;
double aspect_ratio;
double viewport_width;
double viewport_height;
double focal_len;
int sample_count = 20;
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.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ImageUtil::Color Camera::rayColor(const MathUtil::Ray &ray, const IHittable &object) {
ImageUtil::Color color;
HitRecord record;
if (object.hit(ray, MathUtil::Interval(0, 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;
}
int Camera::getSampleCount() const {
return sample_count;
}
MathUtil::Vec3 Camera::randomDisplacement() const {
auto delta_x = pix_delta_x * (MathUtil::randomDouble() - 0.5);
auto delta_y = pix_delta_y * (MathUtil::randomDouble() - 0.5);
return delta_x + delta_y;
}
MathUtil.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
    class Vec3 {
public:

//...

static Vec3 random();

static Vec3 random(double min, double max);

static Vec3 randomVec3InUnitSphere();

static Vec3 randomUnitVec3InHemiSphere(const Vec3 &normal);

static Vec3 randomUnitVec3();

};

//...

inline double randomDouble() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}

inline double randomDouble(double min, double max) {
return min + (max - min) * randomDouble();
}
//...

directory layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── CMakeLists.txt
├── README.md
├── include
│   └── OneWeekend
│   ├── GlobUtil.hpp
│   ├── GraphicObjects.h
│   ├── ImageUtil.h
│   └── MathUtil.h
├── src
│   ├── GraphicObjects.cpp
│   ├── ImageUtil.cpp
│   ├── MathUtil.cpp
│   └── main.cpp
└── thirdparties
├── include
│   └── spdlog # spdlog header files
└── lib
└── Darwin
└── libspdlog.a
Comments
On this page
Note on "Ray tracing in one week" - Part 4