Skip to content

01 PPM Output

Karn Kaul edited this page Oct 1, 2022 · 1 revision

Introduction

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.

Images

Data Representation

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{};
};

PPM

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

Code

It is evident that two pieces of info are needed to produce valid PPM output:

  1. Image dimensions (width + height)
  2. Stream of RGB triplets (as unsigned u8s)

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

ppm_output

Clone this wiki locally