Introduction: From Solid Colors to Gradients

In the previous lesson, you learned about the vec3 class and discovered how it can represent both geometric quantities like positions and directions, as well as colors through the color type alias. You saw how working with normalized color values in the range [0.0, 1.0] provides flexibility and consistency in your code. You also learned how to convert these normalized values to the integer range [0, 255] required by the PPM image format.

Up to this point, every image you've created has been a solid color. In Lesson 2, you generated a completely red square in which every single pixel had the same RGB values. While this was an excellent way to learn the basics of image generation and the PPM format, real images are rarely uniform. Photographs, rendered scenes, and even simple graphics typically contain colors that vary across the image. The sky might transition from deep blue at the top to lighter blue near the horizon. A sunset might blend from orange to pink to purple. These smooth transitions between colors are called gradients.

In this lesson, you'll take the next step in your ray tracing journey by learning to create images in which the color changes based on each pixel's position. Instead of assigning the same color to every pixel, you'll calculate each pixel's color based on its coordinates. This is a fundamental concept that extends far beyond simple gradients. When you eventually trace rays through a scene, you'll calculate each pixel's color based on where the ray goes, what it hits, and how light interacts with surfaces. The coordinate-to-color mapping you'll learn today is the foundation for all of that.

By the end of this lesson, you'll have created your first gradient image. The image will smoothly transition from one color combination in one corner to a different combination in another corner. You'll understand how to map pixel coordinates to color values, how to normalize those coordinates to the [0.0, 1.0] range, and how to use utility functions to keep your code clean and organized. This might seem like a small step, but it represents a crucial shift in thinking: you're moving from static, uniform images to dynamic, calculated images in which every pixel can be unique.

Creating the Color Utility Function

As your ray tracer grows more sophisticated, you'll find yourself repeatedly performing certain operations. One such operation is converting a color object (with normalized values in the range [0.0, 1.0]) to the integer format required by the PPM file format (values in the range [0, 255]). Rather than writing this conversion code every time you need it, you'll create a utility function that handles this task cleanly and consistently.

To keep your code organized, you'll create a new header file called color.h that contains color-related functionality. This follows a common practice in C++ development: grouping related functions and types into dedicated header files. Your vec3.h file handles vector mathematics, and now color.h will handle color-specific operations. Let's look at the complete color.h file:

The file begins with include guards (#ifndef COLOR_H, #define COLOR_H, and #endif) to prevent multiple inclusion, just like in your vec3.h file. You include because the function writes to an output stream, for the function, and because you're working with the type, which is an alias for .

Mapping Pixel Coordinates to Colors

Now you're ready to learn the core concept of this lesson: using a pixel's position to determine its color. This is where the magic happens. Instead of assigning the same color to every pixel, you'll calculate each pixel's color based on where it sits in the image.

Think about the structure of your image. You have a grid of pixels, each identified by two coordinates: i for the horizontal position (column) and j for the vertical position (row). The pixel at the top-left corner has coordinates (0, image_height - 1), and the pixel at the bottom-right corner has coordinates (image_width - 1, 0). Remember that you iterate through rows from top to bottom, which is why j starts at image_height - 1 and decreases.

To create a gradient, you need to map these integer pixel coordinates to normalized color values in the range [0.0, 1.0]. The key formula for this mapping is:

Let's break down what this formula does. The variable i represents the horizontal pixel coordinate, ranging from 0 on the left edge to image_width - 1 on the right edge. By dividing i by image_width - 1, you normalize this coordinate to the range [0.0, 1.0]. When is , the result is . When is , the result is . For pixels in between, you get proportional values.

Building Your First Gradient Image

Now let's put everything together and write the complete program that generates a gradient image. You'll create a new main.cc file that uses both the vec3 class and your new write_color utility function. Here's the complete code:

Let's walk through this code step by step. At the top, you include the necessary headers. You need <fstream> for file output, <iostream> for error messages, and "color.h", which brings in the write_color function and, through its own includes, the vec3 class and the color type alias.

The program begins by defining the image dimensions. You're creating a 256×256 pixel image, which is a nice square size that's large enough to see the gradient clearly but small enough to generate quickly. These are declared as const int because they won't change during the program's execution.

Next, you create an output file stream using . This opens a file named for writing. The following line checks if the file opened successfully. If the file couldn't be opened (perhaps due to permission issues or disk space problems), the condition evaluates to true, and the program prints an error message to and returns with an error code of . This is good practice: always check that file operations succeed before proceeding.

Summary: Ready to Create Colorful Images

In this lesson, you moved beyond solid color images to create your first gradient, where colors vary smoothly across the image based on pixel coordinates. You learned how to use a dedicated color.h header file with a write_color utility function to convert normalized color values to the integer range required by the PPM format, using std::clamp to keep values valid.

You discovered how to map pixel coordinates to color values by normalizing them with formulas like double(i) / (image_width - 1), ensuring the gradient spans the full range from 0.0 to 1.0. By mapping each color component independently, you can create a variety of gradient patterns.

You built a complete program that generates a gradient image, with red increasing from left to right, green from bottom to top, and blue constant. This approach gives you mathematical control over every pixel’s color—a foundational concept for procedural image generation and ray tracing.

In the upcoming practice exercises, you’ll experiment with different gradient formulas and see how changing the math changes the image, building intuition for how code translates to visual results.

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