画像のテクスチャマッピング

前節ではレイとオブジェクトの交点 \(\textbf{P}\) を使ってソリッドテクスチャ (大理石模様) を作成した。\(\textbf{P}\) から計算される二次元の座標 \((u, v)\) を使えば、ファイルから読み込んだ画像をオブジェクトの表面に表示させることができる。

計算した \((u, v)\) を使う直接的な方法は \(u\) と \(v\) を整数 \(i\) と \(j\) に丸めて画像の \((i, j)\) にあるピクセルを読むというものだが、こうすると画像の解像度を変えるたびにコードを変える必要があるので使いにくい。そこで画像のピクセル座標ではなくテクスチャ座標を使うというグラフィクスにおいて必ず使われる慣習の一つを採用する。テクスチャ座標は画像内のピクセルの位置を割合で表す。例えば \(N_{x} \times N_{y}\) の画像の \((i, j)\) ピクセルを表すテクスチャ座標 \((u, v)\) は \[ u = \frac{i}{N_{x} - 1}, \quad v = \frac{j}{N_{y} - 1} \] となる。

画像データの読み込み

まず画像を保持するテクスチャクラスが必要になる。私がよく使う画像ユーティリティライブラリ stb_image.h をここでも使うことにする。このライブラリは画像ファイルを読み込んでデータを unsigned char の大きな配列に格納し、配列には各ピクセルの RGB を表す [0..255] が並ぶ (0 のとき黒で 255 のとき白)。次の image_texture クラスはこの画像データを使っている:

#include "rtweekend.h"
#include "rtw_stb_image.h"

#include <iostream>

class image_texture : public texture {
public:
  const static int bytes_per_pixel = 3;

  image_texture()
    : data(nullptr), width(0), height(0), bytes_per_scanline(0) {}

  image_texture(const char* filename) {
    auto components_per_pixel = bytes_per_pixel;

    data = stbi_load(
      filename, &width, &height, &components_per_pixel, components_per_pixel);

    if (!data) {
      std::cerr << "ERROR: Could not load texture image file '"
                << filename
                << "'.\n";
      width = height = 0;
    }

    bytes_per_scanline = bytes_per_pixel * width;
  }

  ~image_texture() {
    delete data;
  }

  virtual color value(double u, double v, const vec3& p) const {
    // テクスチャのデータがない場合には、そのことが分かるようにシアン色を返す。
    if (data == nullptr)
      return color(0,1,1);

    // 入力されたテクスチャ座標を [0,1] で切り捨てる。
    u = clamp(u, 0.0, 1.0);
    v = 1.0 - clamp(v, 0.0, 1.0);  // v を反転させて画像の座標系に合わせる。

    auto i = static_cast<int>(u * width);
    auto j = static_cast<int>(v * height);

    // 整数座標をさらに切り捨てる (テクスチャ座標は 1.0 になってはいけない)。
    if (i >= width)  i = width-1;
    if (j >= height) j = height-1;

    const auto color_scale = 1.0 / 255.0;
    auto pixel = data + j*bytes_per_scanline + i*bytes_per_pixel;

    return color(color_scale*pixel[0],
                 color_scale*pixel[1],
                 color_scale*pixel[2]);
  }

private:
  unsigned char *data;
  int width, height;
  int bytes_per_scanline;
};
リスト 2.43 [texture.h] image_texture クラス

格納されるデータの順序に難しいところはない。嬉しいことに、stb_image は非常に簡単に使うことができる ── main.hrtw_stb_image.h をインクルードするだけだ:

#include "rtw_stb_image.h"
リスト 2.44 [main.cc] stb_image のインクルード

画像テクスチャを使う

適当にウェブから拾ってきた世界地図1を使ってみよう ──これでなくても、通常のメルカトル図法の地図なら何でもよい。

図 2.18: 世界地図

ファイルを読み込んで拡散マテリアルに割り当てるコードを示す:

hittable_list earth() {
  auto earth_texture = make_shared<image_texture>("earthmap.jpg");
  auto earth_surface = make_shared<lambertian>(earth_texture);
  auto globe = make_shared<sphere>(point3(0,0,0), 2, earth_surface);
  return hittable_list(globe);
}
リスト 2.45 [main.cc] image_texture の割り当て

全ての色をテクスチャとして扱う設計の利点が見え始めた ──lambertian マテリアルには任意のテクスチャを割り当てることができ、lambertian はテクスチャの種類を知る必要がない。

テクスチャを試すときは main.cc 内の ray_color() 関数を一時的に減衰だけを返すように変更すると分かりやすい。次の画像が得られる:

図 2.19: 世界地図をマップした球

  1. 出典: Wikipedia Commons (パブリックドメイン)[return]