I think a good way to get started with ray tracing is to begin with refraction. This is a basic operation that happens in almost every ray tracer. Refraction happens when a light ray hits another medium as shown in the following image:

Of course, this is just a simplified model of how light travels through space and how light is modeled, but this is fine enough to render some pleasant pictures.

According to Snell’s law, the following equation holds:

$$\frac{sin(\alpha)}{sin(\beta)} = \frac{n_2}{n_1}$$

The refraction index of vacuum is $1.0$ and for some glass material it can be for instance $1.6$. Assuming that $n_1 = 1.0$ and $n_2 = 1.6$ we can compute for an given incident ray the direction of the refracted ray. E.g. if $\alpha = 45°$ the corresponding $\beta$ angle will be approximately $26.23°$.

Critical Angle

The critcal anlge at which total internal reflection happens can be computed using this formular:

$$\theta_c = sin^{-1}(\frac{n_1}{n_2})$$

For the given example of vaccum and some air material $\theta_c$ equals to approximately $38.68°$

A test driven development approach to implement ray refraction

According to test driven development we write first a test before starting with the implementation.

Thomas Willberger Founder, CEO and realtime rendering developer at Enscape3D once wrote in a tweet: “Software wisdom: Start with the best-debuggable, most simple implementation. Iron out early mistakes and add tests BEFORE adding bells and whistles. It‘s invaluable to know that the core part works and you do not have to suspect the whole codebase when hunting a bug.”

This is the test I came up with after several iterations:

TEST(RefractionVacuumToGlass, When_IncidentVectorIs45Degrees_Then_RefratedVectorIsAbout26Degrees) {
// Arrange
const Normal2f normal(0.0f, 1.0f);
Vector2f incident(-1.0f, 1.0f);
incident.normalize();

const auto refractionIndexVacuum = 1.0f;
const auto refractionIndexGlass = 1.6f;

// Act
Vector2f refractedDirection;
bool validRefraction = refract(incident, normal, refractionIndexVacuum / refractionIndexGlass, refractedDirection);

// Assert
EXPECT_TRUE(validRefraction);
EXPECT_TRUE(refractedDirection.x() > 0.0f);
EXPECT_TRUE(refractedDirection.y() < 0.0f);

EXPECT_THAT(refractedDirection.norm(), ::testing::FloatEq(1.0f));