The code is compiled with cmake, please check out CMakeLists.txt
Ok… A new topic… Yet to be finished…
Preface
Ayano why you opened a new topic while you are having other topics unfinished
Well, I’m interested in computer graphics. I’m doing OpenGL. So it’s pretty reasonable for me to be interested in ray tracing anyway.
This series of articles are based on Ray Tracing in One Week , but I’m not probably going to follow the division of part defined by the book(Maybe just write till I think that’s enough for one article).
Ok… Let’s get into topic…
Image output and math
We are talking about ray tracing, which is a field of computer graphics. To actually see graphics, we need to be able to produce images. Luckily, there is a type of image that can be easily produced: ppm .
ppm is easy to generate and edit because it’s plain text format, and we can easily manipulate with std::ofstream. Now we can create a generatePPM function to do so.
But wait, an image has three channels in ppm, how will we store that? Well, we will define a class to store that. It has 3 elements: r, g, b, and we will let it return the constructed color string. Let’s name it Vec3 since vectors are used in many places of computer graphics and using Vec3 as color is reasonable since they all have 3 elements.
This file I will call it MathUtil because we will probably use more math functions and classes.
With this class, we can use std::ofstream to construct our ppm image. Let’s define a makePPM method that will take in image width and height, an image 2d vector, save directory, and file name. We will define it in a file called ImageUtil.
icon-padding
For the sake of readability, I will define Color as an alias of Vec3.
Now we can display a ppm image. Recall that we will use Vec3 more often in computer graphics? Now it’s time to make it more suit our needs.
For a Vector in R3, it should be closed under Vec3 addition and scalar multiplication, define dot product and cross product, and shortcut for increment and decrement (+= and -=). We also need to be able to quickly determine its norm and the unit vector on that direction. For the sake of debugging, we will implement std::string operator and << operator
With these enhancements in mind, let’s enhance out Vec3 class:
Now we can do basic arithmetic for vectors on Vec3.
Ray tracing, starting with Ray
We all know from basic linear algebra, a line in R3, ℓ, is defined by ℓ(t)=A+Bt where A,B∈R3. Here A is the origin of the line, and B is the direction of the line. With this schematic, we can also define a ray in R3, since a ray is just a line that can only go in one direction (t∈[0,∞)). Also, we need to ‘trace’ the ray, so we need to know where the ray is at.
With these in mind, now we can define a Ray class:
icon-padding
Still, to make code more readable, I defined Point3 as an alias of Vec3 and wrapped all inside a namespace.
Point3 Ray::at(double t)const{ return position + direction * t; }
Vec3 Ray::dir()const{ return direction; }
Vec3 Ray::pos()const{ return position; } }
Ray Tracing, then Tracing
Now we have a ray, let’s build a ray tracer.
Ray tracer is basically a thing that cast rays out and compute the color seen by the ray. It involves three steps:
Cast a ray from its “eye”
Determine the closest object the ray intersection.
Get the color from the point of intersection.
For better image presentation and makes our life easier, we will use an aspect ratio of 16:9
Aspect ratio, viewport, and camera
You probably know what aspect ratio is: it’s the ratio between screen width and screen height. In 16:9, the ratio is:
width/height=16/9=1.7778
To use minimal magic number and to change the aspect ratio easily, we will calculate it from the width and height of the image.
1 2 3 4
int width = 256; auto aspect = 16.0 / 9.0; int height = static_cast<int>(width / aspect); height = height < 1 ? 1 : height;
icon-padding
The height could be less than 1, so we need to check that.
Viewport, as its name suggests, is the port that we view the scene. It has the same aspect ratio with the image because we don’t want to shrink the image.
Camera is the ‘eye’ we used to view the scene, and the object that casts arrays. It has a focal length, which is the distance between the camera and the associated viewport. Dimensions of the viewport can be found this way:
1 2 3 4 5
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);
Note that we are calculating aspect ratio from the image, not the defined aspect ratio. This is because when we are finding the image dimensions, we rounded down some value, making the actual aspect ratio different from the defined one. We will use the actual aspect ratio to calculate viewport height.
Ray casting
Now we have a camera and a viewport, we can cast rays from the camera to the viewport. The way we are casting ray, however, requires a little of explaination.
In the first step of how ray tracer works, we need to cast a ray. In order to obtain a color on each pixel of our image, we need to cast a ray through each point on our viewport corresponding to an actual pixel.
To achieve this, we will use some linear algebra.
icon-padding
All the coordinates below are in right hand coordinate system.
Assume the width of the viewport is w and the height of the viewport is h, we make two vectors, name them A and B, such that
Let’s denote the camera position as O, focal length to be f, focal length vector F such that
F=⎣⎢⎡00−f⎦⎥⎤
We can find the upper left corner of the viewport, P, which is:
P=O−2A+B−F
Now we can find the position of each pixel on the viewport. To find this, we first have to find the position of the first pixel on the viewport, P00. And we need to find the horizontal and vertical distance between pixels, Δx and Δy.
Since the viewport is a projection of the image, and they share same aspect ratio, they should have the same pixel density. Let’s denote the screen width and height in pixel as W and H we have:
\begin{align*}
\Delta x & = w / W \\
\Delta y & = h / H
\end{align*}
Now we can cast rays from the camera to the viewport. Let’s denote the ray as ℓ, and the ray from camera to Pij as ℓij, we have:
ℓij=Pij−O
With this direction, and position from the camera, we can construct a ray.
Currently, we don’t have any objects to interact with, so we will just skip to step 3. In this case, we will make a gradient image. We will use the x, y magnitude of the normalized ray as color of the pixel.
code:
1 2 3 4 5 6 7
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 ImageUtil::Color(progx, progy, 1); }
icon-padding
Because y and x must be within the range [−1,1], we defined prog to record the progress as y changes value