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:

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

The index of refraction in vacuum is $1.0$ and for some glass material it might 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°$ (angle of incidence) the corresponding $\beta$ angle (angle of refraction) will be approximately $26.23°$. The refractive index is defined as $n = \frac{c}{v}$ where $c$ is the speed of light in vacuum and $v$ is the speed of light in the corresponding medium.

Total internal reflection describes the behavior that at some angles no light is refracted (starting at the critical angle) but instead is only reflected. Total internal reflection is the reason for Fata Morganas, e.g. a wet-looking road on a hot day.

### Critical Angle

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

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

Total internal reflection only happens if $n_1 > n_2$ holds, which means it does not happen if we go from an optical “sparser” medium to an optical “denser” medium.

### Examples

The following table list some example for a 2D scenario where we have an incident ray form “above” and hitting the interface between two different medium types.

Case Normalized normal vector $\alpha$ Expected Incident vector Refraction index medium 1 Refraction index medium 2 Expected $\beta$ Expected normalized refracted vector
A (0, 1) $45°$ (-1, 1) 1 1.6 26.23° (0.441975594,-0.897027075)
B (0, 1) $0°$ (0,-1) 1 1.6 (0,-1)
C (0, 1) $45°$ (-1, 1) 1.6 1 Not available (total internal reflection) Not available (total internal refelction)

### 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 Twitter 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.”

Lets write a test for a refract function. First we have to think about a proper design. The refract function expects three input parameters: $\omega_i$, $n$ (a normal vector) and $\eta$. $\omega_i$ is the vector that points in the opposite direction of the incident vector. This is a common convention: $\omega_i$ points always towards the light source direction. So to say, $\omega_i$ is the negative direction of the incoming light. $n$ is the normal vector that points towards the direction from which the incoming ray is originating. $\eta$ is computed as the fraction of $n_2$ and $n_1$ where $n_1$ is refraction index of the medium form which the incoming ray is coming from. As a output parameter the refraction function is expected to compute the refraction vector, if there is no total internal reflection.

When looling at the source code of PBRT the refraction function as four parameters.

inline bool Refract(const Vector3f &wi, const Normal3f &n, Float eta,
Vector3f *wt)


PBRT uses the fourth parameter as an output parameter for the computed refracted vector. PBRT has the convention that for output parameters always pointers are used. Furthermore, the function returns true if there was no total internal reflection.

I decided to use a similar form for my refraction function. Instead of using a pointer for the computed refraction vector, I decided to use a reference value.

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);
Vector2f wi = -incident;
wi.normalize();

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

// Act
Vector2f refractedDirection;
bool validRefraction = refract(wi, 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));