-
Notifications
You must be signed in to change notification settings - Fork 0
01 PPM Output
The output of raytracing is essentially a bunch of pixels, so to see it in the program we would need to setup an entire graphics pipeline and render loop, which we are not going to do. Instead, we will simply output the pixel data to an image which can then be viewed in an external application. If you don't have any pre-installed apps that can read PPM images, GIMP is a solid, reliable option that's open source and available on both Windows and Linux. (I'm sure there are leaner alternatives too, which you are encouraged to research for yourself.)
The code for this chapter is a bit different to the following ones, in that it strives to get some output ASAP, deferring structure and organization for later refactoring. The accumulated tech debt will be addressed soon afterwards.
While there are a multitude of ways to represent image data, the most common approach is to use a stream of 8 bits per channel (red, green, blue, maybe alpha), which yields a range of 0-255 (as unsigned). This is usually normalized to [0-1] (floating point) space before being manipulated, including in graphics shaders. The integral representation is usually colour corrected (also known as gamma correction) to provide more precision in the darker ranges. There will be no colour correction in this project. A contiguous array of std::uint8_t
models this data precisely, which is what we will use at the core.
struct Rgb {
std::array<std::uint8_t, 3> values{};
};
The PPM file format is extremely simple to deal with in code, and can be human-readable at the same time, which makes it the ideal format to debug tray
output.
P3 # "P3" means this is a RGB color image in ASCII
3 2 # "3 2" is the width and height of the image in pixels
255 # "255" is the maximum value for each color
# The part above is the header
# The part below is the image data: RGB triplets
255 0 0 # red
0 255 0 # green
0 0 255 # blue
255 255 0 # yellow
255 255 255 # white
0 0 0 # black
It is evident that two pieces of info are needed to produce valid PPM output:
- Image dimensions (width + height)
- Stream of RGB triplets (as unsigned
u8
s)
Using all the info above, this code gets us there:
struct ImageData {
std::span<Rgb const> data{};
std::array<std::uint32_t, 2> extent{};
};
std::ostream& operator<<(std::ostream& out, ImageData const& image) {
assert(image.data.size() == image.extent[0] * image.extent[1]);
// write header
out << "P3\n" << image.extent[0] << ' ' << image.extent[1] << "\n255\n";
// write each row
for (std::uint32_t row = 0; row < image.extent[1]; ++row) {
// write each column
for (std::uint32_t col = 0; col < image.extent[0]; ++col) {
// compute index
auto const index = row * image.extent[0] + col;
// obtain corresponding Rgb
auto const& rgb = image.data[index];
// write out each channel
for (auto const channel : rgb.values) { out << static_cast<int>(channel) << ' '; }
}
out << '\n';
}
return out;
}
Create a test image in int main
and stream it to std::cout
to check if everything works as expected:
int main() {
static constexpr auto extent = std::array{40U, 30U};
static constexpr auto white_v = Rgb{.values = {0xff, 0xff, 0xff}};
auto buffer = std::array<Rgb, extent[0] * extent[1]>{};
std::fill_n(buffer.data(), buffer.size(), white_v);
auto const image_data = ImageData{.data = buffer, .extent = extent};
std::cout << image_data;
}
Redirect to a file on the shell and check the image in a viewer, it should be a 40x30 white rectangle:
out/default/tray/Debug/tray > test.ppm