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:

Refraction

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));

    float theta = degreeToRadian(26.23);
    Eigen::Rotation2Df rotationTransform(theta);
    Vector2f rotatedVector = rotationTransform.toRotationMatrix() * Vector2f(0.0, -1.0);

    EXPECT_THAT(refractedDirection.x(), ::testing::FloatNear(rotatedVector.x(), 0.0001f));
    EXPECT_THAT(refractedDirection.y(), ::testing::FloatNear(rotatedVector.y(), 0.0001f));
}

The unit test considers only the 2D case, but it can be extended easily to 3D. The fun part now is to implement this unit test.