In the last part, we learned to calculate normal vector of a sphere. For the ray hitting the sphere at the front, the normal is pointing with the different direction with the array. For the ray hitting the sphere at the back (for example, the ray passed the front surface of the sphere and hit the back surface), the normal is pointing with the same direction with the array. We need to distinguish these two cases because this is significant for objects that has different inside and outside rendering rule, like a glass sphere.
icon-padding
In the figure I used a plane to represent a small portion of the sphere
If we want the normal is always pointing out, we can do this by checking the dot product of the normal and the ray direction. If the dot is positive, the ray is outside the sphere. We can use this code:
icon-padding
assume ray is a Ray object, center is a Vec3 object
If we want to make the normal always pointing against the array(for example, the inner surface of a glass sphere has normal pointing against the array), we still can check the dot product, but we need to reverse the normal if the dot product is positive, meaning the normal is pointing with the same direction with the array, and store whether we are hitting the outer surface or not.
icon-padding
assume ray is a Ray object, center, normal, outward_normal is a Vec3 object
The difference is that if you choose to implement the second method, you are determining whether it’s the front surface at the geometric calculation time, and if you choose to implement the first method, you are determining whether it’s the front surface at the shading time. Since there are much more shaders than geometric scenarios, it’s better to choose the second method.
Thus, we will modify our HitRecord to store this information and modify our hit function of the Sphere class.
Right now we only have one hittable objects, but we will have sooo much more. We need to have a way to store all the hittable objects and check whether the ray hits any of them. For this, we will create a class as the collection of all the hittable objects. Since we will have a lot of hittable objects, and they are going to be somehow big, so we will use std::shared_ptr to store them.
Notice that I implemented begin() and end() to make this class can be iterated with range-based for loop. I also used auto as the return type of the begin and end function. This is because I don’t want to make the definition too long, and I don’t want to type the long type name.
Also, it’s reasonable for us to make a header that includes many useful utility functions and constants, such as PI, infinity, and the conversion between degrees and radians. We will put that in a header called GlobUtil.hpp since it will be a single-header utility header.
GlobUtil.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13
#ifndef GLOBUTIL_HPP #define GLOBUTIL_HPP
#include<limits>
#define PI (3.1415926) #define INF (std::numeric_limits::double::infinity())
inlinedoubledeg2Rad(double deg){ return deg * PI / 180.0; }
#endif// GLOBUTIL_HPP
With our HittableList class, we can create a world with multiple objects. We can do it in our main function:
intmain(){ 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(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})));
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() - 10) 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, world); v.emplace_back(color); } img.emplace_back(v); } ImageUtil::makePPM(camera.getWidth(), camera.getHeight(), img, "out", "test.ppm"); return0; }
After rendering, we have this result:
Which is great.
Interval
To make our life easier, we can simplify our code a bit, by using some helper classes or functions. Let’s start with “a value within an interval”.
To do this, we will implement a Interval class that takes two values as the interval. It will check whether an input value is within the interval or surrounded by it. A code implementation is like this:
boolSphere::hit(const MathUtil::Ray &r, MathUtil::Interval interval, 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) returnfalse; auto discri_sqrt = std::sqrt(discriminant);
With world, we can now render a scene with multiple objects. However, our code is still a little bit messy, with drawing code and color mapping code scrambled in one place. We can wrap the draw code and render code to camera, since camera is the one actually doing rendering.
voidCamera::Render(const IHittable& world){ auto img = std::vector<std::vector<ImageUtil::Color>>();
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) { auto ray_dir = getPixRayDir(j, i); auto ray = MathUtil::Ray(getPosition(), ray_dir); auto color = rayColor(ray, world); v.emplace_back(color); } img.emplace_back(v); } ImageUtil::makePPM(getWidth(), getHeight(), img, "out", "test.ppm");
}
icon-padding
In this case we have to move the definition of IHittable and HitRecord to before rayColor, since we need to use them in the rayColor function.
voidCamera::Render(const IHittable& world){ auto img = std::vector<std::vector<ImageUtil::Color>>();
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) { auto ray_dir = getPixRayDir(j, i); auto ray = MathUtil::Ray(getPosition(), ray_dir); auto color = rayColor(ray, world); v.emplace_back(color); } img.emplace_back(v); } ImageUtil::makePPM(getWidth(), getHeight(), img, "out", "test.ppm");
boolSphere::hit(const MathUtil::Ray &r, MathUtil::Interval interval, 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) returnfalse; auto discri_sqrt = std::sqrt(discriminant);