Note on "Ray Tracing The Next Week" - Part 4

Note on "Ray Tracing The Next Week" - Part 4

Ayano Kagurazaka Lv3

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

icon-padding

I didn’t follow the article in this part.

Solid textures and Perlin noises

One common way to generate solid texture is to use Perlin noises, which is a noise generation algorithm that generates noises that looks like blurred white noise. The image below is a sample Perlin noise texture:

Alt text

Image from Wikipedia

To generate Perlin noise in R2\mathbb{R}^2, we divide the surface into grids, assign a gradient vector for each grid vertex, and compute the dot product of the vector for the grid vertex and the distance vector of the point relative to the vertex to determine the value on that point. Then, we do an interpolation between the dot product result to make the noise smoother.

In R3\mathbb{R}^3, the process is similar, except we need to take the dot product 8 times.

icon-padding

Cold knowledge: Perlin noise is defined for any finite dimension, with time complexity of O(2n)O(2^n)

Our class definition will be this:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

class Perlin {
public:
Perlin() = default;

~Perlin() = default;

double noise(const Point3 &p) const;

private:
class Perlin {
public:
Perlin() = default;

~Perlin() = default;

double noise(const Point3 &p) const;

private:
static constexpr int perm[] = {
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30,
69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62,
94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136,
171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161,
1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126,
255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253,
19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193,
238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31,
181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180};

double fade(double x) const;

double lerp(int begin, int end, int weight) const;

double gradientDotProd(int hash, const AppleMath::Vector3 &pt) const;
};

double fade(double x) const;

double lerp(int begin, int end, int weight) const;

double gradientDotProd(int hash, const AppleMath::Vector3 &pt) const;
};

icon-padding

The permutation table is the one Perlin used in his original paper, repeated twice to prevent integer overflow

First we do the hashing with the given perm lookup table:

MathUtil.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
double x = p[0], y = p[1], z = p[2];
int xi = static_cast<int>(floor(x)) & 255;
int yi = static_cast<int>(floor(y)) & 255;
int zi = static_cast<int>(floor(z)) & 255;
x -= floor(x);
y -= floor(y);
z -= floor(z);

int llb, lrb, ulb, urb, llf, lrf, ulf, urf;
llb = perm[perm[perm[xi ] + yi ] + zi ];
lrb = perm[perm[perm[xi + 1 ] + yi ] + zi ];
ulb = perm[perm[perm[xi ] + yi + 1 ] + zi ];
urb = perm[perm[perm[xi + 1 ] + yi + 1 ] + zi ];
llf = perm[perm[perm[xi ] + yi ] + zi + 1 ];
lrf = perm[perm[perm[xi + 1 ] + yi ] + zi + 1 ];
ulf = perm[perm[perm[xi ] + yi + 1 ] + zi + 1 ];
urf = perm[perm[perm[xi + 1 ] + yi + 1 ] + zi + 1 ];

We first extract the integer part of the coordinate, take bitwise AND so it’s always less than 255. Then we extract the decimal part of the original value to find the relative position inside the block. Then we take hash with the integer coordinate. Here we are doing coordinate plus 1 because we need to find 8 perms that belongs to each vertex of the block(one element in 3d grid), and we assume the grid size is 1, so we plus 1 to find another vertex.

Now we generate the gradient vector in each vertex and find the dot product of each. One thing that is a little different from the Wikipedia implementation is that in the paper Improving noise by Perlin himself, he used an optimized approach. Instead of using a randomly generated random vector, he just used the vector that defined by the direction from the center to the edges:

(1,1,0),(1,1,0),(1,1,0),(1,1,0),(1,0,1),(1,0,1),(1,0,1),(1,0,1),(0,1,1),(0,1,1),(0,1,1),(0,1,1)(1,1,0),(-1,1,0),(1,-1,0),(-1,-1,0),\\ (1,0,1),(-1,0,1),(1,0,-1),(-1,0,-1),\\ (0,1,1),(0,-1,1),(0,1,-1),(0,-1,-1)

We can implement that with an intuitive way:

MathUtil.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
double Perlin::gradientDotProd(int hash, const AppleMath::Vector3 &pt) const {
auto x = pt[0];
auto y = pt[1];
auto z = pt[2];
switch (hash & 0xF) {
case 0x0:
return x + y;
case 0x1:
return -x + y;
case 0x2:
return x - y;
case 0x3:
return -x - y;
case 0x4:
return x + z;
case 0x5:
return -x + z;
case 0x6:
return x - z;
case 0x7:
return -x - z;
case 0x8:
return y + z;
case 0x9:
return -y + z;
case 0xA:
return y - z;
case 0xB:
return -y - z;
case 0xC:
return y + x;
case 0xD:
return -y + z;
case 0xE:
return y - x;
case 0xF:
return -y - z;
default:
return 0;
}
}

Then we do the dot product between the gradient and the distance vector, with the direction determined by the hashed value:

MathUtil.cpp
1
2
3
4
5
6
7
8
9
10
11
12
double Perlin::noise(const Point3& p) const {
//...

double dotllb = gradientDotProd(llb, {x , y , z });
double dotlrb = gradientDotProd(lrb, {x - 1 , y , z });
double dotulb = gradientDotProd(ulb, {x , y - 1 , z });
double doturb = gradientDotProd(urb, {x - 1 , y - 1 , z });
double dotllf = gradientDotProd(llf, {x , y , z - 1 });
double dotlrf = gradientDotProd(lrf, {x - 1 , y , z - 1 });
double dotulf = gradientDotProd(ulf, {x , y - 1 , z - 1 });
double doturf = gradientDotProd(urf, {x - 1 , y - 1 , z - 1 });
}

Here we are doing grid - 1 to find the distance between dot and vertex. It cannot be 1 - grid because with 1 - grid, the result vector will be pointing from the input point to the corner, which is the opposite vector.

Before interpolation, we need to process our lerp weight value from linear to a curve. We do that in our fade function, with Perlin’s original implementation, which is this formula:

f(x)=x3(x(x615)+10)f(x) = x^3 * (x * (x * 6 - 15) + 10)

And this code:

MathUtil.cpp
1
2
3
double Perlin::fade(double x) const {
return x * x * x * (x * (x * 6 - 15) + 10);
}

Now we can implement the lerp function to smoothen the value:

MathUtil.cpp
1
2
3
double Perlin::lerp(double begin, double end, double weight) const {
return begin + weight * (end - begin);
}

Now all functions have been implemented, we can put all pieces together, and get the final part done:

MathUtil.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
double Perlin::noise(const Point3 &p) const {
//...
x = fade(x);
y = fade(y);
z = fade(z);


double x0 = lerp(dotllb, dotlrb, x);
double x1 = lerp(dotulb, doturb, x);
double x2 = lerp(dotllf, dotlrf, x);
double x3 = lerp(dotulf, doturf, x);

double y0 = lerp(x0, x1, y);
double y1 = lerp(x2, x3, y);

double out = lerp(y0, y1, z);


return (out + 1) / 2;
//...
}

Here we are doing a trilinear interpolation to get the interpolated value in R3\mathbb{R}^3. We first interpolate four values on the four axes that lies on xy-plane, then we interpolate the pairs that on the same vertical plane, finally we interpolate these two values to find the final value.

Note that the range of this interpolation [1,1][-1, 1] since we have both positive and negative value. We can map that range to [0,1][0, 1] by + 1 then / 2.

Here is the final code:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Perlin {
public:
Perlin() = default;

~Perlin() = default;

double noise(const Point3 &p) const;

private:
static constexpr int perm[] = {
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252,
219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68,
175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230,
220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209,
76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186,
3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59,
227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70,
221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178,
185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51,
145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50,
45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66,
215, 61, 156, 180, 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140,
36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26,
197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125,
136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1,
216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100,
109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85,
212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2,
44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79,
113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179,
162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204,
176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141,
128, 195, 78, 66, 215, 61, 156, 180
};

double fade(double x) const;

double lerp(double begin, double end, double weight) const;

double gradientDotProd(int hash, const AppleMath::Vector3 &pt) const;
};
MathUtil.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
double Perlin::gradientDotProd(int hash, const AppleMath::Vector3 &pt) const {
auto x = pt[0];
auto y = pt[1];
auto z = pt[2];
switch (hash & 0xF) {
case 0x0:
return x + y;
case 0x1:
return -x + y;
case 0x2:
return x - y;
case 0x3:
return -x - y;
case 0x4:
return x + z;
case 0x5:
return -x + z;
case 0x6:
return x - z;
case 0x7:
return -x - z;
case 0x8:
return y + z;
case 0x9:
return -y + z;
case 0xA:
return y - z;
case 0xB:
return -y - z;
case 0xC:
return y + x;
case 0xD:
return -y + z;
case 0xE:
return y - x;
case 0xF:
return -y - z;
default:
return 0;
}
}

double Perlin::fade(double t) const { return t * t * t * (t * (t * 6 - 15) + 10); }

double Perlin::lerp(double begin, double end, double weight) const { return begin + weight * (end - begin); }

double Perlin::noise(const Point3 &p) const {
double x = p[0], y = p[1], z = p[2];
int xi = static_cast<int>(floor(x)) & 255;
int yi = static_cast<int>(floor(y)) & 255;
int zi = static_cast<int>(floor(z)) & 255;
x -= floor(x);
y -= floor(y);
z -= floor(z);

int llb, lrb, ulb, urb, llf, lrf, ulf, urf;
llb = perm[perm[perm[xi ] + yi ] + zi ];
lrb = perm[perm[perm[xi + 1 ] + yi ] + zi ];
ulb = perm[perm[perm[xi ] + yi + 1 ] + zi ];
urb = perm[perm[perm[xi + 1 ] + yi + 1 ] + zi ];
llf = perm[perm[perm[xi ] + yi ] + zi + 1 ];
lrf = perm[perm[perm[xi + 1 ] + yi ] + zi + 1 ];
ulf = perm[perm[perm[xi ] + yi + 1 ] + zi + 1 ];
urf = perm[perm[perm[xi + 1 ] + yi + 1 ] + zi + 1 ];
double dotllb = gradientDotProd(llb, {x , y , z });
double dotlrb = gradientDotProd(lrb, {x - 1 , y , z });
double dotulb = gradientDotProd(ulb, {x , y - 1 , z });
double doturb = gradientDotProd(urb, {x - 1 , y - 1 , z });
double dotllf = gradientDotProd(llf, {x , y , z - 1 });
double dotlrf = gradientDotProd(lrf, {x - 1 , y , z - 1 });
double dotulf = gradientDotProd(ulf, {x , y - 1 , z - 1 });
double doturf = gradientDotProd(urf, {x - 1 , y - 1 , z - 1 });

x = fade(x);
y = fade(y);
z = fade(z);


double x0 = lerp(dotllb, dotlrb, x);
double x1 = lerp(dotulb, doturb, x);
double x2 = lerp(dotllf, dotlrf, x);
double x3 = lerp(dotulf, doturf, x);

double y0 = lerp(x0, x1, y);
double y1 = lerp(x2, x3, y);

double out = lerp(y0, y1, z);


return (out + 1) / 2;
}

Now let’s create a texture with it, make a two ball scene real quick, and apply that in the main function:

Texture.h
1
2
3
4
5
6
7
8
9
class NoiseTexture : public ITexture {
public:
explicit NoiseTexture() = default;

Color value(double u, double v, const Point3& p) const override;
private:
Perlin noise;
double frequency;
};
Texture.cpp
1
2
3
4
Color NoiseTexture::value(double u, double v, const Point3& p) const 
{
return Color{1, 1, 1} * noise.noise(p * frequency);
}
scenes.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void perlinSpheres()
{
HittableList world;
Camera camera(1920, 16.0 / 9.0, 20, Point3{0, 0, 0}, Point3{13, 2, 3}, 0);
camera.setSampleCount(100);
camera.setShutterSpeed(1.0/24.0);
camera.setRenderDepth(50);
camera.setRenderThreadCount(12);
camera.setChunkDimension(64);
auto tex = std::make_shared<NoiseTexture>();
world.add(std::make_shared<Sphere>(1000, Point3{0, -1000, 0}, std::make_shared<Lambertian>(tex)));
world.add(std::make_shared<Sphere>(2, Point3{0, 2, 0}, std::make_shared<Lambertian>(tex)));


render(world, camera);
}
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
#include "scenes.h"

int main() {

int option = 3;
switch(option) {
case 0:
randomSpheres();
break;
case 1:
twoSpheres();
break;
case 2:
huajiSphere();
break;
case 3:
perlinSpheres();
break;
default:
break;
}
return 0;
}

and we get this image:

perlinScene

Tweaking the noise

We can use some parameters to tweak the outcome of the noise function. Four common ways are frequency, octave, amplitude, and persistence. Frequency is how fast the light and dark alternate, in an intuitive way. Octave is a method that generates multiple Perlin noise layers and add them together with a given weight. Amplitude is how large the texture fluctuates, which will initially be a value in (0,1](0, 1] and decrease over the course of octave. And persistence is the decreasing factor of amplitude when using octave to get noise pattern.

First we need to modify our rawNoise function, since right now it returns value between [0,1][0, 1], which will only increase the value if we add them up, but we want the raw value to be between [1,1][-1, 1], so we can properly do octave. We do this by removing the modification of the value at the end of the rawNoise function:

MathUtil.cpp
1
2
3
4
5
6
7
double Perlin::rawNoise(const Point3 &p) const {
//...

double out = lerp(y0, y1, z);

return out;
}

We will add octaveNoise to the Perlin class to implement the parameterized control over the generated Perlin noise:

MathUtil.h
1
2
3
4
5
6
7
8
9
10
11
12
13
class Perlin {
public:
Perlin() = default;

~Perlin() = default;

double rawNoise(const Point3 &p) const;

double octaveNoise(const Point3& p, double frequency, int octave_count, double presistence) const;

private:
//...
}
MathUtil.cpp
1
2
3
4
5
6
7
8
9
10
11
12
double Perlin::octaveNoise(const Point3& p, double frequency, int octave_count, double persistence) const {
double sum = 0;
double max_value = 0;
double amplitude = 1;
for (int i = 0; i < octave_count; ++i) {
sum += rawNoise(p * frequency) * amplitude;
max_value += amplitude;
amplitude *= persistence;
frequency *= 2;
}
return sum / max_value;
}
Texture.h
1
2
3
4
5
6
7
8
9
10
11
class NoiseTexture : public ITexture {
public:
NoiseTexture(double frequency, int octave_count, double persistence);

Color value(double u, double v, const Point3& p) const override;
private:
Perlin noise;
double frequency;
int octave_count;
double persistence;
};

And apply the octave noise to the value:

Texture.cpp
1
2
3
Color NoiseTexture::value(double u, double v, const Point3 &p) const {
return Color{1, 1, 1} * 0.5 * (noise.octaveNoise(p, frequency, octave_count, persistence) + 1);
}

Finally, modify the value in the scene file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void perlinSpheres()
{
HittableList world;
Camera camera(400, 16.0 / 9.0, 20, Point3{0, 0, 0}, Point3{13, 2, 3}, 0);
camera.setSampleCount(100);
camera.setShutterSpeed(1.0/24.0);
camera.setRenderDepth(50);
camera.setRenderThreadCount(12);
camera.setChunkDimension(64);
auto tex = std::make_shared<NoiseTexture>(0.5, 10, 0.8);
world.add(std::make_shared<Sphere>(1000, Point3{0, -1000, 0}, std::make_shared<Lambertian>(tex)));
world.add(std::make_shared<Sphere>(2, Point3{0, 2, 0}, std::make_shared<Lambertian>(tex)));

render(world, camera);
}

and we get a dark marble like texture:

PerlinStone

And if we change the parameter, making the frequency bigger, persistence smaller, we will get a denser image with brighter area:

HighFreq

frequency 0.5, octave count 10, persistence 0.5

Normally, we use Perlin noise with modification by using the output of the noise function as input of another function, and use that function as the color function. One way we can manipulate the Perlin noise function is to use sine function to generate periodic texture. We can do it in the value function:

Texture.cpp
1
2
3
4

Color NoiseTexture::value(double u, double v, const Point3 &p) const {
return Color{1, 1, 1} * (sin(10 * noise.octaveNoise(p, frequency, octave_count, persistence) + 5));
}

The result is a texture that looks like white marble:

PerlinWhiteMarble

More application

Perlin noise has applications way beyond generating solid textures. For example, The terrain generation of many game uses Perlin noise. We can simulate that by doing a colorization of a sphere with certain return value of octave function corresponding to a color.

Let’s make a TerrainTexture. The definition is almost the same with NoiseTexture, but we are doing different colorization:

Texture.h
1
2
3
4
5
6
7
8
9
10
11
class TerrainTexture : public ITexture {
public:
TerrainTexture(double frequency, int octave_count, double persistence);

Color value(double u, double v, const Point3& p) const override;
private:
Perlin noise;
double frequency;
int octave_count;
double persistence;
};
Texture.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Color TerrainTexture::value(double u, double v, const Point3 &p) const {
double height = noise.octaveNoise(p, frequency, octave_count, persistence);
if (Interval(-1, 0).within(height)) {
return Color{0, 0, 1} + Color{0, 0, 0xee / 255.0} * height;
}
else if (Interval(0, 0.05).within(height)) {
return Color{0xe9 / 255.0, 0xd5 / 255.0, 0x5A / 255.0};
}
else if (Interval(0.05, 1).within(height)) {
return Color{0, 0xDD / 255.0, 0} - Color{0, (0xDD - 0x33) / 255.0, 0} * height / 0.95;
}
else {
spdlog::warn("unexpected value: {}", height);
return Color{0, 0, 0};
}
}

Here, the return value of the noise function becomes the height. When the height is below 0, we treat that as ocean and make it blue. If a small portion of land is around the sea, it’s beach. The other are land and plant, colored in green. With the height, all colorization (except beach) are made with gradient.

Then we can make a scene called terrain:

scenes.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void terrain()
{
HittableList world;
Camera camera(1920, 16.0 / 9.0, 20, Point3{0, 0, 0}, Point3{0, 0, -50}, 0);
camera.setSampleCount(100);
camera.setShutterSpeed(1.0/24.0);
camera.setRenderDepth(50);
camera.setRenderThreadCount(12);
camera.setChunkDimension(64);
auto tex = std::make_shared<TerrainTexture>(0.2, 10, 0.5);
world.add(std::make_shared<Sphere>(10, Point3{0, 0, 0}, std::make_shared<Lambertian>(tex)));

render(world, camera);
}

and modify the main function:

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
#include "scenes.h"

int main() {

int option = 4;
switch(option) {
case 0:
randomSpheres();
break;
case 1:
twoSpheres();
break;
case 2:
huajiSphere();
break;
case 3:
perlinSpheres();
break;
case 4:
terrain();
default:
break;
}
return 0;
}

And the result looks good:

terrain

we can tweak our parameter a little bit, and we will get different terrains:

terrainHighFreq

changed frequency from 0.2 to 0.5

associated code

code can be found by this link

Comments
On this page
Note on "Ray Tracing The Next Week" - Part 4