画像の出力

PPM フォーマット

レンダラを書き始めると、すぐに画像を確認する手段が必要になる。一番簡単なのはファイルに書き出してしまうことだ。ただ画像フォーマットがたくさんあってどれも複雑なのが問題となる。私はいつもプレーンテキストの PPM ファイルから始めることにしている。Wikipedia にこのフォーマットの分かりやすい説明がある:

PPM フォーマットの説明
図 1.1: PPM フォーマットの説明

PPM フォーマットを出力する簡単な C++ コードを書こう:

#include <iostream>

int main() {
  const int image_width = 256;
  const int image_height = 256;

  std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";

  for (int j = image_height-1; j >= 0; --j) {
    for (int i = 0; i < image_width; ++i) {
      auto r = double(i) / (image_width-1);
      auto g = double(j) / (image_height-1);
      auto b = 0.25;

      int ir = static_cast<int>(255.999 * r);
      int ig = static_cast<int>(255.999 * g);
      int ib = static_cast<int>(255.999 * b);

      std::cout << ir << ' ' << ig << ' ' << ib << '\n';
    }
  }
}
リスト 1.1 [main.cc] 簡単な PPM ファイルを出力するプログラム

このコードに関して注意すべき点がある:

  1. ピクセルは行ごとに、左から右へ出力される。
  2. 列は上から下に出力される。
  3. グラフィクスプログラミングの慣習として、赤 (red) 緑 (green) 青 (blue) の各要素は 0.0 から 1.0 の値を取る。後でハイダイナミックレンジを考えるときにはこの範囲外の値をレンダラの内部で扱うが、その場合でも出力時にはトーンマップで 0.0 から 1.0 の値に変換する。そのためこの部分のコードはこれから変化しない。

画像ファイルの作成

このプログラムは PPM ファイルの内容を標準出力に書き出すので、ファイルに保存するには標準出力をリダイレクトする必要がある。コマンドラインでリダイレクト演算子 < を使えば行える:

build\Release\inOneWeekend.exe > image.ppm

このコマンドは Winodws の例であり、Mac や Linux では次のようにする:

build/inOneWeekend > image.ppm

出力ファイルを画像ビューアで開けば内容を確認できる。私は Mac で ToyViewer を使っているが、どんなものでも構わない。ビューアによっては PPM ファイルをサポートしていない可能性もあるので、その場合には "PPM ビューア" と Google で検索してほしい。

PPM ファイルの例
図 1.2: 出力される PPM ファイル

やった! これがグラフィクスにおける "hello world" だ。もし画像がこのようになっていないなら、出力された PPM ファイルをテキストエディタで開いて内容を確認しよう。正しいファイルでは最初の部分が次のようになっている:

P3
256 256
255
0 255 63
1 255 63
2 255 63
3 255 63
4 255 63
5 255 63
6 255 63
7 255 63
8 255 63
9 255 63
...
リスト 1.2 [image.ppm] 出力される PPM ファイルの先頭部分

画像が表示されないなら、ファイルに余分な改行や文字があるのだろう。すると画像ビューアはデータを読み取れない。

PPM 以外のフォーマットで画像を生成するときには、私はヘッダーオンリーの画像ライブラリ stb_image.h をよく使う。GitHub のページ から入手できる。

進捗インジケータの追加

次に進む前に、出力に進捗インジケータを追加しておこう。これがあれば長いレンダリングがどこまで進んだかを確認できるし、無限ループなどの問題によって実行が進まなくなったのを検出することもできる。

現在のプログラムは画像を標準出力ストリーム (std::cout) に出力するので、インジケータはエラー出力ストリーム (std::cerr) に書くことにする。

  ...
  for (int j = image_height-1; j >= 0; --j) {
    std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
    for (int i = 0; i < image_width; ++i) {
      auto r = double(i) / (image_width-1);
      auto g = double(j) / (image_height-1);
      auto b = 0.25;

      int ir = static_cast<int>(255.999 * r);
      int ig = static_cast<int>(255.999 * g);
      int ib = static_cast<int>(255.999 * b);

      std::cout << ir << ' ' << ig << ' ' << ib << '\n';
    }
  }
  std::cerr << "\nDone.\n";
  ...
リスト 1.3 [main.cc] 進捗インジケータを付けたレンダリングのメインループ