Writing Your First Image
Introduction: From "Hello World" to "Hello Image"
In the previous lesson, you wrote your first C++ program that printed "Hello, Raytracer!" to the terminal. That simple program established the basic structure we'll use throughout this course and verified that your development environment works correctly. Now we're ready to take an exciting leap forward: instead of outputting text, we're going to generate actual visual images.
This transition from text to images is a pivotal moment in your ray tracing journey. Every ray tracer, no matter how sophisticated, ultimately does one thing: it produces image data. The stunning photorealistic renders you see in movies, the beautiful architectural visualizations, and the realistic reflections in modern games — they all start with the same fundamental task we're about to tackle: writing color values for each pixel in an image.
In this lesson, you'll learn how to represent images as grids of colored pixels and how to output them in a format called PPM (Portable Pixmap). We'll start with something deliberately simple: a solid red image. This might seem basic compared to the complex 3D scenes we'll eventually render, but it establishes the foundation for everything to come. Once you understand how to generate and output image data, adding ray tracing to calculate what colors those pixels should be becomes a natural next step.
By the end of this lesson, you'll have a program that generates a complete image file. You'll see your code produce visual output for the first time, and you'll understand the structure that every image-generating program follows. This is where your ray tracer truly begins.
Understanding Pixels and Color
Before we can generate images, we need to understand what images actually are at a fundamental level. When you look at a photograph on your screen or a frame from a movie, you're seeing a grid of tiny colored dots called pixels. The word "pixel" is short for "picture element," and it represents the smallest unit of an image that we can control individually.
Think of an image like a mosaic made of tiny colored tiles. Each tile is a single color, and when you step back and look at all the tiles together, they form a complete picture. Pixels work the same way. A typical image might be 1920 pixels wide and 1080 pixels tall, giving us over two million individual pixels. Each one contributes its small part to the overall image.
Every pixel has a color, and in computer graphics, we typically represent colors using the RGB color model. RGB stands for Red, Green, and Blue — the three primary colors of light. This might seem different from what you learned in art class, where the primary colors are red, yellow, and blue. That's because mixing paint (subtractive color) works differently from mixing light (additive color). When you combine red, green, and blue light in different proportions, you can create any color visible to the human eye.
Each color component in RGB has an intensity value that tells us how much of that color to include. In most image formats, including the one we'll use today, these intensity values range from 0 to 255. A value of 0 means none of that color, while 255 means the maximum amount. For example, pure red would be represented as red=255, green=0, blue=0. Pure white, which is all colors at maximum intensity, would be red=255, green=255, blue=255. Black, the absence of light, would be red=0, green=0, blue=0.
Let's look at some examples to make this concrete. If we want a bright yellow pixel, we'd combine maximum red (255) with maximum green (255) and no blue (0). Yellow is what you get when you mix red and green light. For a purple pixel, we might use red=128, green=0, blue=128 — equal amounts of red and blue at medium intensity. For a dark gray, we might use red=64, green=64, blue=64 — equal amounts of all three colors at low intensity.

The reason we use the range 0 to 255 is that it corresponds to 8 bits of data. A bit is the smallest unit of computer memory, holding either 0 or 1. Eight bits can represent 256 different values (2 to the power of 8), which we number from 0 to 255. This means each color component takes exactly one byte of memory, and a full RGB pixel takes three bytes. This is efficient for computers to work with and provides enough precision that the human eye can't distinguish individual color steps.
When we create an image, we need to specify its dimensions: how many pixels wide and how many pixels tall. An image that's 256 pixels wide and 256 pixels tall contains 65,536 total pixels (256 × 256). We'll need to specify a color for each one of these pixels. The way we organize this data is as a two-dimensional grid, where we can refer to any pixel by its position. We typically use coordinates where the first number represents the horizontal position (column) and the second represents the vertical position (row).
The PPM Format: Your First Image Format
Now that we understand what pixels are, we need a way to save them to a file that image viewers can display. There are many image formats you've probably heard of: JPEG, PNG, GIF, and others. These formats are sophisticated and use compression to make files smaller, but that complexity makes them challenging to work with when you're learning. Instead, we're going to use a format called PPM, which stands for Portable Pixmap.
PPM is perfect for learning because it's incredibly simple and human-readable. Unlike formats like PNG that store data in compressed binary form, PPM files are plain text. You can open a PPM file in any text editor and see exactly what's inside. This transparency makes it easy to understand what your program is doing and to debug problems when they arise.
A PPM file has a straightforward structure with four main parts. First comes the magic number, which is just a special code that identifies what type of file this is. For ASCII-based PPM files (the kind we'll create), the magic number is P3. This tells image viewers, "Hey, I'm a PPM file with text-based pixel data."
Next come the image dimensions: the width and height in pixels. These are just two numbers separated by a space. For example, 256 256 means the image is 256 pixels wide and 256 pixels tall.
The third part is the maximum color value. This tells the image viewer what range the color values use. We'll always use 255, which means our color components range from 0 to 255 as we discussed earlier.
Finally comes the actual pixel data: the RGB values for every pixel in the image. These are written as triplets of numbers, with each triplet representing one pixel's red, green, and blue components.
Let's look at a tiny example to make this concrete. Here's a complete PPM file for a 3×2 image (six pixels):
Let's break this down line by line. The first line, P3, is our magic number identifying this as an ASCII PPM file. The next line starting with # is a comment, which are allowed in PPM files and help explain the structure. The line 3 2 tells us this image is 3 pixels wide and 2 pixels tall. The next line, 255, sets our maximum color value. Then comes the pixel data.
The following six lines each contain an RGB triplet, representing one pixel's color:
255 0 0is pure red (maximum red, no green, no blue).0 255 0is pure green.0 0 255is pure blue.255 255 0is yellow (red and green at maximum, no blue).255 255 255is white (all colors at maximum).0 0 0is black (no color).
![]()
So this tiny image has red, green, and blue pixels in the first row, and yellow, white, and black pixels in the second row. The simplicity and readability of the PPM format make it ideal for learning and debugging.
Writing the PPM Header
Now let's start building our image generator. We'll begin with the PPM header, which contains the metadata about our image. Remember from the previous section that the header consists of three parts: the magic number, the dimensions, and the maximum color value.
Here's how we write the header in C++:
Let's examine this code carefully. At the top, we include <iostream> just like in our "Hello, Raytracer!" program. This gives us access to std::cout, which we'll use to output our image data.
Inside main(), we first define two constants: image_width and image_height. We're using the const keyword because these values won't change during our program's execution. We've chosen 256 for both dimensions, which will give us a square image with 65,536 pixels. The number 256 is convenient because it's a power of 2, which computers handle efficiently, and it's large enough to see detail but small enough to generate quickly.
The next line is where we output the PPM header. Let's break down this statement: std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";. We're using std::cout with the << operator multiple times in sequence. Each << sends something to the output stream.
First, we send "P3\n". This outputs the magic number P3 followed by a newline character (\n). The newline moves to the next line, which is important because each part of the PPM header should be on its own line.
Next, we send image_width, which outputs the value 256. Then we send ' ', which is a single space character. This space separates the width from the height. Then we send image_height, outputting another 256. Finally, we send "\n255\n", which outputs a newline, the maximum color value 255, and another newline.
When this program runs, the output looks like this:
This is a valid PPM header. It tells any image viewer that this is an ASCII PPM file, the image is 256×256 pixels, and color values range from 0 to 255. Of course, we haven't written any pixel data yet, so this isn't a complete image file. But it's the essential first step, and it demonstrates how we use std::cout to write structured text output.
Generating Pixels with Nested Loops
Now that we have our header, we need to generate the actual pixel data. Remember, our image is 256 pixels wide and 256 pixels tall, which means we need to output 65,536 RGB triplets. Writing these by hand would be impossible, so we'll use loops to generate them systematically.
The structure we need is a nested loop: an outer loop that iterates through each row of the image, and an inner loop that iterates through each pixel in that row. Here's the complete code with the pixel generation added:
Let's examine the loop structure carefully. The outer loop uses the variable j to represent the row number. Notice something interesting: we start at image_height - 1 (which is 255) and count down to 0. This might seem backward, but there's a good reason for it.
In many graphics systems, including the one we're building, we think of the coordinate system with the origin (0, 0) at the bottom-left corner of the image. The y-coordinate increases as we move up. However, when we write pixel data to a PPM file, we write it from top to bottom. By starting our loop at the highest row number and counting down, we're writing the top row of the image first, then working our way down. This ensures that when an image viewer displays our PPM file, it appears right-side up.
The loop condition j >= 0 means we continue as long as j is greater than or equal to zero. The update expression --j decrements j by one after each iteration. So our outer loop runs 256 times, once for each row.
Inside the outer loop, we have the inner loop that uses the variable i to represent the column number (the horizontal position within the current row). This loop starts at 0 and continues while i < image_width, incrementing i with ++i after each iteration. This inner loop also runs 256 times, once for each pixel in the row.
For each pixel, we define three integer variables: ired for red, igreen for green, and iblue for blue. Right now, we're setting red to 255 (maximum) and green and blue to 0. This gives us pure red for every pixel. Later in the course, we'll calculate these values based on ray tracing, but for now, we're keeping it simple with a solid color.
Finally, we output the RGB triplet for this pixel: std::cout << ired << ' ' << igreen << ' ' << iblue << '\n';. This writes the red value, a space, the green value, a space, the blue value, and a newline. The newline isn't strictly necessary in PPM format (whitespace is flexible), but it makes the file more readable if you open it in a text editor.
Let's trace through what happens. When j is 255 (the first row), the inner loop runs 256 times with i going from 0 to 255. Each iteration outputs 255 0 0 (red). After the inner loop completes, we've written the entire first row. Then j becomes 254, and we write the second row. This continues until j reaches 0 and we've written all 256 rows.
The result is 65,536 lines of pixel data, each containing 255 0 0, which creates a solid red image. When you combine this with the header we wrote earlier, you have a complete, valid PPM file.
Adding Progress Feedback
When you run the program we've built so far, it generates the image data very quickly. But as we add more complex ray tracing calculations in future lessons, rendering an image might take seconds, minutes, or even longer. It's helpful to have feedback about how the rendering is progressing so you know the program is working and haven't accidentally created an infinite loop.
We'll add progress feedback that displays which row we're currently rendering. Here's the updated code:
Notice we've added two new lines. At the beginning of the outer loop, we have: std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;. And after the loops complete, we have: std::cerr << "\nDone. \n";.
Let's understand what's happening here. First, notice we're using std::cerr instead of std::cout. Both are output streams, but they serve different purposes. std::cout is the standard output stream, which we use for the actual program output — in our case, the image data. std::cerr is the standard error stream, which is typically used for diagnostic messages, warnings, and errors.
Why does this distinction matter? When we run our program, we'll redirect the standard output to a file to save our image. If we used std::cout for our progress messages, those messages would end up in the image file and corrupt it. By using std::cerr, we ensure our progress messages go to the terminal where we can see them, while our image data goes to the file where it belongs.
The string we're outputting starts with \r, which is a carriage return character. Unlike \n (newline), which moves to the next line, a carriage return moves the cursor back to the beginning of the current line. This allows us to overwrite the previous progress message with the new one, creating the effect of a single line that updates in place rather than printing a new line for each row.
After the carriage return, we output the message "Scanlines remaining: " followed by the current value of j. The term "scanline" is traditional graphics terminology for a row of pixels. As j counts down from 255 to 0, we see the number of remaining scanlines decrease, giving us a sense of progress.
At the end of the line, we have std::flush. Normally, output streams use buffering for efficiency — they collect output in memory and write it in chunks rather than immediately. The std::flush manipulator forces the stream to write everything in its buffer immediately. This ensures we see the progress update right away rather than having it delayed until the buffer fills up.
After both loops complete, we output a final message: std::cerr << "\nDone. \n";. The \n at the beginning moves to a new line (since our progress messages were overwriting the same line). Then we print "Done." followed by several spaces. These spaces overwrite any remaining characters from the progress message, ensuring a clean final output. The final \n moves to a new line so any subsequent terminal output appears cleanly.
When you run this program, you'll see something like this in your terminal:
The number will count down rapidly from 255 to 0, updating in place. When it finishes, you'll see:
This progress feedback doesn't affect the image data at all — that's still going to std::cout exactly as before. But it makes the rendering process much more transparent and reassuring, especially as our ray tracer becomes more complex and takes longer to run.
Running Your Image Generator and Viewing Results
Now that we have a complete program, let's talk about how to run it and view the resulting image. The key concept here is output redirection, which is a feature of command-line shells that lets you send a program's output to a file instead of displaying it on the screen.
When you run a program normally, its output appears in your terminal. But we want to save our image data to a file so we can view it with an image viewer. On most systems, you would do this by running your program with output redirection like this:
The > symbol tells the shell to redirect the standard output (everything sent to std::cout) to a file named image.ppm. The progress messages we send to std::cerr still appear in the terminal because we're only redirecting standard output, not standard error.
On this platform, the environment handles this process for you automatically. When you run your code, the system captures the output, saves it as an image file, and displays it in the interface. You don't need to worry about redirection commands or file management — you can focus entirely on writing the code that generates the image.
When you run the complete program we've built in this lesson, you'll see a solid red square. Every pixel in the 256×256 image will be pure red (RGB: 255, 0, 0). This might not seem impressive compared to the complex scenes we'll eventually render, but it represents a major milestone. You've successfully generated image data, formatted it correctly as PPM, and produced a viewable image file.
If you were to open the PPM file in a text editor (which you can do on your own computer, though it's not necessary on CodeSignal), you'd see the header followed by 65,536 lines of 255 0 0. The file would be quite large — around 500 kilobytes — because we're storing everything as text. This is one reason why compressed image formats exist, but for our purposes, the simplicity of PPM is worth the extra file size.
The important thing to understand is the flow: your program generates RGB values for each pixel, outputs them in PPM format to standard output, that output gets saved to a file, and an image viewer interprets the PPM format to display the colored pixels. This same flow will apply to every image we generate throughout this course. The only thing that will change is how we calculate those RGB values — instead of hardcoding them to red, we'll use ray tracing to determine what color each pixel should be based on the 3D scene we're rendering.
Summary and What's Next
In this lesson, you learned how images are made up of pixels, each defined by RGB color values ranging from 0 to 255. You discovered the simple, human-readable PPM image format and wrote a C++ program that outputs a valid PPM file for a solid red image. You used nested loops to generate pixel data and added progress feedback using std::cerr so you can see rendering progress in the terminal.
This structure—writing a header, looping over pixels, and outputting RGB values—is the foundation of every ray tracer. In the next lesson, you'll build on this by generating color gradients based on pixel positions, making your images more interesting and taking the first step toward true ray tracing.
