Introduction: The Case for Utility Functions

In the previous lesson, you created a camera abstraction that encapsulated all camera-related logic into a clean, reusable class. This transformation made your code more maintainable by organizing related functionality into a focused component with a clear interface. The camera class eliminated scattered variables and inline calculations from your main function, replacing them with a simple, expressive API. This same principle of organization and reusability applies not just to classes, but to the fundamental building blocks of your code: constants and utility functions.

If you look carefully at your current ray tracer code, you'll notice several patterns that suggest opportunities for improvement. You have magic numbers scattered throughout your codebase. For example, when testing for ray-sphere intersections, you use 0.001 as a minimum distance threshold to avoid self-intersection artifacts. When computing the background gradient in ray_color(), you use 0.5 as a blending factor. These numbers work, but their meaning isn't immediately clear to someone reading the code. What does 0.001 represent? Why is it that specific value? Could it be changed safely?

You also have mathematical operations that appear in multiple places. Converting angles from degrees to radians is a common operation in computer graphics, and you'll need it frequently as you add camera controls and other features. Right now, if you needed to perform this conversion, you would write the formula inline: angle * 3.14159 / 180.0. But this approach has problems. First, the value of pi is hardcoded with limited precision. Second, the conversion logic is duplicated wherever you need it. Third, the intent isn't as clear as it could be. A function called degrees_to_radians() immediately communicates what's happening, while an inline calculation requires the reader to recognize the formula.

Looking ahead to features you'll implement in upcoming lessons, you'll need random number generation extensively. Materials that scatter light in random directions, antialiasing that samples multiple rays per pixel, and depth of field effects that randomize ray origins all depend on generating random values. Right now, you don't have any random number utilities in your codebase. When you need randomness, you'll have to write the same boilerplate code repeatedly: seeding the random number generator, calling , normalizing the result to the desired range, and handling edge cases. This repetition is error-prone and clutters your code with low-level details.

Creating a Constants Library

Mathematical constants are fundamental to ray tracing. You need pi for angle calculations, trigonometric functions, and geometric formulas. You need a representation of infinity for ray intersection tests, where you want to find the closest intersection within a potentially unbounded range. Rather than hardcoding these values with magic numbers scattered throughout your code, you'll create a dedicated header file that defines them once with appropriate precision and clarity.

Create a new file called src/constants.h and start with the header guards and necessary includes:

The <limits> header provides the std::numeric_limits template, which gives you access to properties of numeric types in a portable, standards-compliant way. This is the proper way to obtain special values like infinity rather than using platform-specific constants or magic numbers.

Now let's define infinity. In ray tracing, you frequently need to represent an unbounded range. For example, when searching for the closest ray-sphere intersection, you might initialize your search range from a small positive value to infinity. The std::numeric_limits<double>::infinity() function returns the IEEE 754 representation of positive infinity for double-precision floating-point numbers. This is a special value that compares as greater than any finite number and has well-defined behavior in arithmetic operations.

Let's break down this declaration. The inline keyword tells the compiler that this variable can be defined in a header file that might be included in multiple translation units without causing linker errors. Without inline, including this header in multiple source files would result in multiple definitions of the same variable, which violates the one definition rule. The inline keyword resolves this by allowing multiple definitions as long as they're identical.

Math Utility Functions for Cleaner Code

With your constants defined, you can now build utility functions that use them to perform common mathematical operations. One of the most frequent operations in computer graphics is converting angles between degrees and radians. Humans naturally think in degrees because they're more intuitive: a right angle is 90 degrees, a half circle is 180 degrees, a full circle is 360 degrees. However, most mathematical functions in programming languages, including C++'s trigonometric functions, work with radians. This means you constantly need to convert between the two representations.

Create a new file called src/math_utils.h and start with the header guards and includes:

Notice that we include constants.h because we'll need the pi constant for our angle conversion function. This demonstrates how utility headers can build on each other, creating layers of functionality.

Now let's implement the degrees-to-radians conversion function:

This function encapsulates the standard conversion formula. To convert degrees to radians, you multiply by pi and divide by 180. This formula comes from the relationship between degrees and radians: a full circle is 360 degrees or 2π radians, so one degree equals π/180 radians. The function is marked inline to suggest to the compiler that it should substitute the function body at the call site rather than generating a function call. For such a simple function, inlining eliminates the overhead of a function call while maintaining the benefits of a named, reusable operation.

The function takes a double parameter and returns a double, maintaining the precision of your calculations. If you pass in 90 degrees, you get approximately 1.5708 radians (π/2). If you pass in 180 degrees, you get approximately 3.14159 radians (π). The conversion is exact within the limits of floating-point precision.

Random Number Generation Utilities

Random number generation is essential for many advanced ray tracing techniques. Realistic materials scatter light in random directions based on their surface properties. Antialiasing samples multiple rays per pixel with slight random variations to smooth out jagged edges. Depth of field effects randomize ray origins to simulate camera lens aperture. Motion blur samples rays at random times during a frame. All of these features depend on generating high-quality random numbers efficiently.

Create a new file called src/random_utils.h and start with the header guards and includes:

The <cstdlib> header provides the std::rand() function, which generates pseudo-random integers. While C++ offers more sophisticated random number generation facilities in the <random> header, std::rand() is sufficient for ray tracing purposes and has the advantage of simplicity. The random numbers don't need to be cryptographically secure; they just need to be well-distributed and fast to generate.

Let's implement the basic random number function that returns a value in the range from zero to just below one:

This function generates a random double in the range [0, 1), meaning it can return zero but will never quite reach one. Let's break down how it works. The std::rand() function returns a random integer between 0 and RAND_MAX, which is a constant defined by your C++ implementation (typically 32767 or 2147483647). To convert this integer to a double in [0, 1), we divide by RAND_MAX + 1.0.

The addition of 1.0 is crucial. If we divided by alone, the function could return exactly 1.0 when returns . By dividing by , the maximum possible return value is , which is slightly less than 1.0. This [0, 1) range is the standard convention for random number generators and prevents edge cases in code that assumes the value is strictly less than one.

Extending vec3 with Utility Integration

Now that you have constants and random number utilities, you can extend your vec3 class with new methods that leverage these foundations. These additions will make your vector class more powerful and prepare it for the advanced lighting and material features you'll implement in upcoming lessons. You'll add three new capabilities: checking if a vector is near zero, generating random vectors, and generating random unit vectors on a sphere.

Open your src/vec3.h file and add the random utilities include at the top, after the existing includes:

This gives your vec3 class access to the random number generation functions you just created. Now let's add the near_zero() method to the public section of the vec3 class, after the existing methods:

This method checks whether a vector is very close to zero in all three dimensions. The threshold value s is set to 1e-8, which is 0.00000001. This is small enough to catch vectors that are effectively zero for practical purposes, but large enough to avoid false positives from normal floating-point rounding errors. The method uses std::fabs() to get the absolute value of each component and checks if all three are below the threshold.

Why do you need this method? In ray tracing, you'll frequently compute vectors that should theoretically be zero but might have tiny non-zero values due to floating-point arithmetic imprecision. For example, when computing the reflection of a ray off a surface, certain geometric configurations might produce a direction vector that should be exactly zero but is actually something like (0.0000000001, -0.0000000002, 0.0000000001). Treating this as a valid direction vector could cause problems in your lighting calculations. The method lets you detect and handle these cases appropriately.

Summary: Building a Foundation for Advanced Features

In this lesson, you created a set of reusable utilities to make your ray tracer cleaner and more maintainable. You defined mathematical constants like pi and infinity in constants.h, eliminating magic numbers and making your code more readable. You added a degrees_to_radians() function in math_utils.h to clarify angle conversions and centralize this common operation. In random_utils.h, you encapsulated random number generation with random_double() functions, making it easy to generate random values in any range.

You also extended your vec3 class with a near_zero() method for numerical stability, a static random() method for generating random vectors, and a random_unit_vector() function for producing uniformly distributed directions on a sphere. These additions prepare your vector class for advanced features like realistic lighting and material scattering.

These utilities will be used throughout your ray tracer, supporting features like materials, antialiasing, and camera controls. By centralizing constants and common operations, your code becomes easier to read, modify, and extend. In the next practice exercises, you’ll use these utilities to deepen your understanding and prepare for more advanced ray tracing techniques.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal