C++ Raytracer

This ray tracer was the first 3D graphics programming project for me. I started off with an example that had functions available for intersecting with infinite planes and spheres. Using those functions I started off by rendering the distance from the camera (first image), the closer something is, the lighter the pixel appears in the screen. With this the scene is immediately visible. From there on I created a point light and added diffuse shading, then shadows, anti-aliasing (picking a random target within the pixel’s space), reflections, refractions, Fresnel equations, Beer’s law, and finally normal maps.

Picking a random position within a pixel’s space instead of always the top-left position, and blending the pixels together, I found that you get very realistic looking images. Without it, there’s always a noise visible, and edges look sharp, because everything is aliased. I also used randomness to pick which pixels to render. By this the screen will be more visible more soon as you’re not rendering just one point at a time, but a bit of the entire screen spread out. This makes moving the camera around a lot faster.

Code Snippets

Reflections

When a material is reflective, this part will recursively call the trace function with the reflection ray. To prevent clogging or getting stuck in an infinite loop, the ray depth is increased by one, and checked at the top of the Trace function.

float reflscalar = _Ray.intersection.material->reflection;
if (reflscalar > 0.0f) {
	vec3 N = _Ray.intersection.N;
	if (glm::dot(N, _Ray.D) > 0) {
		N = -N;
		cp = contactpoint + N * EPSILON;
	}

	// Trace reflection ray
	Ray reflectionray;
	reflectionray.O = cp;
	reflectionray.D = _Ray.D + 2 * glm::dot(_Ray.D, -N) * N;
	reflectionray.t = INFINITY;
	raytype = RAY_REFLECTIVE;
	reflect_color = Trace(reflectionray, depth + 1, refrIndex) * _Ray.intersection.material->color;
	reflect_color *= reflscalar;
}

Refractions

Refractions are very similar to reflections, only they are going through the object. An object made out of glass distorts the view of whatever is behind it, because the ray’s direction is changed. It works very similar to reflections, only the calculation of the ray’s direction is different.

After that there’s a part that calculates the weight between how much is reflected, and how much is refracted. When you look at a refractive surface, the angel at which you look changes what you see. When looking straight at a window, you can see right through, but when you look at it from an angel, it will reflect much more. This piece of code uses Schlick’s Approximation, which approximates Fresnel’s equations. It calculates the weight between the reflection and refraction.

// Schlick
float v = 0;
if (ddn > 0)
	v = std::pow(nt - 1, 2) / std::pow(nt + 1, 2);
else
	v = std::pow(1 - nt, 2) / std::pow(nt + 1, 2);

float wt = v + (1 - v) * std::pow(1 + glm::dot(N, _Ray.D), 5);
reflect_color *= wt;
refract_color *= 1 - wt;

Absorption

Light gets absorbed by anything it travels through, even air. It hits particles in there. The reason for the sky being blue is because the sky-blue colour is the combination of the waves hitting the most particles. For the ray-tracer I only applied absorption to objects with a higher density, as you won’t see it in the air easily. A large sphere made out of glass would appear darker than a small one, even with the same density. To calculate how much light is absorbed, I used Beer’s Law.

// Schlick
float v = 0;
if (ddn > 0)
	v = std::pow(nt - 1, 2) / std::pow(nt + 1, 2);
else
	v = std::pow(1 - nt, 2) / std::pow(nt + 1, 2);

float wt = v + (1 - v) * std::pow(1 + glm::dot(N, _Ray.D), 5);
reflect_color *= wt;
refract_color *= 1 - wt;