レイトレーシング
CG

レイトレーシング入門2「テクスチャとコーネルボックス」

More than 1 year has passed since last update.

テクスチャ

テクスチャは物体表面の模様だったり,反射率などのパラメータなどが格納されている画像データです.物体にテクスチャをマッピングすれば,画像を使って様々な制御が行えるようになります.マッピング情報からテクスチャ上のカラーを参照することをサンプリングとかルックアップなどといいます.なので,テクスチャのことをルックアップテーブルなんて呼ばれることもあります.

テクスチャの用途は実に様々ですが,ここでは反射率(アルベド)を格納した画像データとして扱います.テクスチャは主に手続き型テクスチャと画像テクスチャに分かれます.それぞれプロシージャルテクスチャ,イメージテクスチャとも呼びます.今回は両方のテクスチャを作成します.まずは基本となるテクスチャの抽象クラスを定義します.

// rayt.h
class Texture {
public:
    virtual vec3 value(float u, float v, const vec3& p) const = 0;
};

u, v はテクスチャ座標です.p は対象ピクセルの位置情報です.物体に当たった位置のテクスチャ座標が必要なので,HitRec に追加します.

class HitRec {
public:
    float t;
    float u;
    float v;
    vec3 p;
    vec3 n;
    MaterialPtr mat;
};

カラーテクスチャ

最もシンプルな手続き型テクスチャで単色のテクスチャです.カラー(反射率)を持っています.

// rayt.h
class ColorTexture : public Texture {
public:
    ColorTexture(const vec3& c)
        : m_color(c) {
    }

    virtual vec3 value(float u, float v, const vec3& p) const override {
        return m_color;
    }
private:
    vec3 m_color;
};

value 関数では単にカラー(反射率)を返しています.このポインタを typedef で定義しておきます.

// rayt.h
class Texture;
typedef std::shared_ptr<Texture> TexturePtr;

次に材質にテクスチャを設定できるようにし,反射率に反映させるようにします.Lambertian, Metal のアルベドをテクスチャに変更します.

class Lambertian : public Material {
public:
    Lambertian(const TexturePtr& a)
        : m_albedo(a) {
    }

    virtual bool scatter(const Ray& r, const HitRec& hrec, ScatterRec& srec) const override {
        vec3 target = hrec.p + hrec.n + random_in_unit_sphere();
        srec.ray = Ray(hrec.p, target - hrec.p);
        srec.albedo = m_albedo->value(hrec.u, hrec.v, hrec.p);
        return true;
    };

private:
    TexturePtr m_albedo;
};

class Metal : public Material {
public:
    Metal(const TexturePtr& a, float fuzz)
        : m_albedo(a)
        , m_fuzz(fuzz) {
    }

    virtual bool scatter(const Ray& r, const HitRec& hrec, ScatterRec& srec) const override {
        vec3 reflected = reflect(normalize(r.direction()), hrec.n);
        reflected += m_fuzz*random_in_unit_sphere();
        srec.ray = Ray(hrec.p, reflected);
        srec.albedo = m_albedo->value(hrec.u, hrec.v, hrec.p);
        return dot(srec.ray.direction(), hrec.n) > 0;
    }

private:
    TexturePtr m_albedo;
    float m_fuzz;
};

球体の生成のところを書き換えます.

// Camera

vec3 w(-2.0f, -1.0f, -1.0f);
vec3 u(4.0f, 0.0f, 0.0f);
vec3 v(0.0f, 2.0f, 0.0f);
m_camera = std::make_unique<Camera>(u, v, w);

// Shapes

ShapeList* world = new ShapeList();
world->add(std::make_shared<Sphere>(
    vec3(0.6, 0, -1), 0.5f,
    std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.1f, 0.2f, 0.5f)))));
world->add(std::make_shared<Sphere>(
    vec3(-0.6, 0, -1), 0.5f,
    std::make_shared<Metal>(
        std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.8f)), 0.4f)));
world->add(std::make_shared<Sphere>(
    vec3(0, -100.5, -1), 100,
    std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.0f)))));
m_world.reset(world);

これを実行すると次のようになります.(rayt201.cpp, rayt201.h)

tuto-raytracing-color-texture.png

格子模様テクスチャ

これも手続き型テクスチャです.格子状の模様を生成します.

// rayt.h
class CheckerTexture : public Texture {
public:
    CheckerTexture(const TexturePtr& t0, const TexturePtr& t1, float freq)
        : m_odd(t0)
        , m_even(t1)
        , m_freq(freq) {
    }

    virtual vec3 value(float u, float v, const vec3& p) const override {
        float sines = sinf(m_freq*p.getX()) * sinf(m_freq*p.getY()) * sinf(m_freq*p.getZ());
        if (sines < 0) {
            return m_odd->value(u, v, p);
        }
        else {
            return m_even->value(u, v, p);
        }
    }

private:
    TexturePtr m_odd;
    TexturePtr m_even;
    float m_freq;
};

ColorTexture を2つ設定し,$\sin$ 関数を使って交互に描きます.m_freq は周波数で,縞模様の間隔を調整できます.このテクスチャを床に設定してみます.

ShapeList* world = new ShapeList();
world->add(std::make_shared<Sphere>(
    vec3(0.6, 0, -1), 0.5f,
    std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.1f, 0.2f, 0.5f)))));
world->add(std::make_shared<Sphere>(
    vec3(-0.6, 0, -1), 0.5f,
    std::make_shared<Metal>(
        std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.8f)), 0.4f)));
world->add(std::make_shared<Sphere>(
    vec3(0, -100.5, -1), 100,
    std::make_shared<Lambertian>(
        std::make_shared<CheckerTexture>(
            std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.0f)),
            std::make_shared<ColorTexture>(vec3(0.8f, 0.2f, 0.0f)), 10.f))));
m_world.reset(world);

次のようになります.(rayt202.cpp)

tuto-raytracing-checker-texture-output.png

画像テクスチャ

画像テクスチャはビットマップファイルなどの画像ファイルから読み込んで,それをテクスチャとして扱います.画像テクスチャを使用するには,当たった位置のテクスチャ座標が必要です.球体のテクスチャ座標は球状マッピングで生成します.

u = \frac{\phi}{2\pi}, \quad v = \frac{\theta}{\pi}.

方位角 $\phi$,極角 $\theta$ を計算するには位置から三角関数で求まります.

\begin{align}
x &= \cos(\phi)\cos(\theta) \\[2ex]
y &= \cos(\phi)\sin(\theta) \\[2ex]
z &= \sin(\theta) \\[2ex]
\end{align}

そうすると

\phi = \tan^{-1}(y,x), \quad \theta = \sin^{-1}(z).

atan2 は $[-\pi,\pi]$ の範囲を返し,sin は $[-\pi/2,\pi/2]$ の範囲を返します.それを [0,1] にマッピングします.

u = 1 - \frac{(\phi+\pi)}{2\pi}, \quad v = \frac{\theta + \pi/2}{\pi}.

位置から球状マッピングしたテクスチャ座標を取得するコードは次のようになります.

// rayt.h
inline void get_sphere_uv(const vec3& p, float& u, float& v) {
    float phi = atan2(p.getZ(), p.getX());
    float theta = asin(p.getY());
    u = 1.f - (phi + PI) / (2.f * PI);
    v = (theta + PI / 2.f) / PI;
}

球体に当たった時にこの関数を使ってテクスチャ座標を設定します.

virtual bool hit(const Ray& r, float t0, float t1, HitRec& hrec) const override {
    vec3 oc = r.origin() - m_center;
    float a = dot(r.direction(), r.direction());
    float b = 2.0f*dot(oc, r.direction());
    float c = dot(oc, oc) - pow2(m_radius);
    float D = b*b - 4*a*c;
    if (D > 0) {
        float root = sqrtf(D);
        float temp = (-b - root) / (2.0f*a);
        if (temp < t1 && temp > t0) {
            hrec.t = temp;
            hrec.p = r.at(hrec.t);
            hrec.n = (hrec.p - m_center) / m_radius;
            hrec.mat = m_material;
            get_sphere_uv(hrec.p, hrec.u, hrec.v);
            return true;
        }
        temp = (-b + root) / (2.0f*a);
        if (temp < t1 && temp > t0) {
            hrec.t = temp;
            hrec.p = r.at(hrec.t);
            hrec.n = (hrec.p - m_center) / m_radius;
            hrec.mat = m_material;
            get_sphere_uv(hrec.p, hrec.u, hrec.v);
            return true;
        }
    }

    return false;
}

次は画像テクスチャを実装します.画像ファイルの読み込みは stb_image.h を使います.

// rayt.h
class ImageTexture : public Texture {
    public:
        ImageTexture(const char* name) {
            int nn;
            m_texels = stbi_load(name, &m_width, &m_height, &nn, 0);
        }

        virtual ~ImageTexture() {
            stbi_image_free(m_texels);
        }

        virtual vec3 value(float u, float v, const vec3& p) const override {
            int i = (u) * m_width;
            int j = (1 - v) * m_height - 0.001;
            return sample(i,j);
        }

        vec3 sample(int u, int v) const
        {
            u = u<0 ? 0 : u >= m_width ? m_width - 1 : u;
            v = v<0 ? 0 : v >= m_height ? m_height - 1 : v;
            return vec3(
                int(m_texels[3 * u + 3 * m_width * v]) / 255.0,
                int(m_texels[3 * u + 3 * m_width * v + 1]) / 255.0,
                int(m_texels[3 * u + 3 * m_width * v + 2]) / 255.0);
        }

    private:
        int m_width;
        int m_height;
        unsigned char* m_texels;
    };

stbi_load で画像データをファイルから読み込みます.stbi_image_free で画像データを読み込んだデータのメモリを解放します.テクスチャ座標からカラーをサンプリングするのは sample 関数です.画像ファイルを用意して,画像テクスチャを使用してみます.

ShapeList* world = new ShapeList();
world->add(std::make_shared<Sphere>(
    vec3(0.6, 0, -1), 0.5f,
    std::make_shared<Lambertian>(
        std::make_shared<ImageTexture>("./assets/brick_diffuse.jpg"))));
world->add(std::make_shared<Sphere>(
    vec3(-0.6, 0, -1), 0.5f,
    std::make_shared<Metal>(
        std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.8f)), 0.4f)));
world->add(std::make_shared<Sphere>(
    vec3(0, -100.5, -1), 100,
    std::make_shared<Lambertian>(
        std::make_shared<CheckerTexture>(
            std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.0f)),
            std::make_shared<ColorTexture>(vec3(0.8f, 0.2f, 0.0f)), 10.f))));
m_world.reset(world);

レンダリングすると次のような画像になります.(rayt203.cpp, rayt203.h)

tuto-raytracing-image-texture-output.png

発光

物体から光を放出するような材質を作ります.この材質の物体は照明と考えることができます.材質クラスに発光色を返す仮想関数を追加して,発光する材質はオーバーライドします.

class Material {
public:
    virtual bool scatter(const Ray& r, const HitRec& hrec, ScatterRec& srec) const = 0;
    virtual vec3 emitted(const Ray& r, const HitRec& hrec) const { return vec3(0); }
};

既存の材質クラスには変更を加えないように純粋仮想関数ではなくて黒を返すようにします.発光材質を DiffuseLight クラスとします.発光色はテクスチャで設定します.

class DiffuseLight : public Material {
public:
    DiffuseLight(const TexturePtr& emit)
        : m_emit(emit) {
    }

    virtual bool scatter(const Ray& r, const HitRec& hrec, ScatterRec& srec) const override {
        return false;
    }

    virtual vec3 emitted(const Ray& r, const HitRec& hrec) const override {
        return m_emit->value(hrec.u, hrec.v, hrec.p);
    }

private:
    TexturePtr m_emit;
};

散乱はしないので,scatter 関数は何もせずに false を返します.
あとは反射した光に発光を加えます.なので,color を書き換えます.

vec3 color(const rayt::Ray& r, const Shape* world, int depth) {
    HitRec hrec;
    if (world->hit(r, 0.001f, FLT_MAX, hrec)) {
        vec3 emitted = hrec.mat->emitted(r, hrec);
        ScatterRec srec;
        if (depth < MAX_DEPTH && hrec.mat->scatter(r, hrec, srec)) {
            return emitted + mulPerElem(srec.albedo, color(srec.ray, world, depth + 1));
        }
        else {
            return emitted;
        }
    }
    return background(r.direction());
}

物体を発光させてみましょう.次のようなコードになります.

ShapeList* world = new ShapeList();
world->add(std::make_shared<Sphere>(
    vec3(0, 0, -1), 0.5f,
    std::make_shared<DiffuseLight>(
        std::make_shared<ColorTexture>(vec3(1)))));
world->add(std::make_shared<Sphere>(
    vec3(0, -100.5, -1), 100,
    std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.8f)))));
m_world.reset(world);

実行結果は次のようになります.(rayt204.cpp)

tuto-raytracing-diffuse-light-output.png

四角形

新しい物体「四角形」を追加します.これは軸に平行な四角形とします.なので,XY軸,XZ軸,YZ軸に平行な四角形です.例えば,XY軸の平面上にある四角形は図のようなものです.

tuto-raytracing-rect.png

光線を四角形に飛ばしたときに,当たるかどうかの判定を考えます.それにはまず光線と平面との交点を求めます.光線と平面が平行でない限り,光線の直線上のどこかで平面と当たります.光線の方程式は以下のとおりです.

\vec{p}(t) = \vec{o} + t\vec{d}.

各ベクトルのZ要素で表すと

\vec{p}_{z}(t) = \vec{o}_{z} + t\vec{d}_{z}.

$z = k$ なので

t = \frac{k-\vec{o}_{z}}{\vec{d}_{z}}.

上の式で $t$ が求まるので,光線の方程式から平面上の $x$ と $y$ を求められます.

x = \vec{o}_x + t\vec{d}_x, \quad y = \vec{o}_y + t\vec{d}_y.

もし $x_0 < x < x_1$ かつ $y_0 < y < y_1$ なら当たっていることになります.
これはXY軸の四角形ですが,XZ軸,YZ軸も同様に考えることができます.これをもとに Rect クラスを実装します.

class Rect : public Shape {
public:
    enum AxisType {
        kXY = 0,
        kXZ,
        kYZ
    };
    Rect() {}
    Rect(float x0, float x1, float y0, float y1, float k, AxisType axis, const MaterialPtr& m)
        : m_x0(x0)
        , m_x1(x1)
        , m_y0(y0)
        , m_y1(y1)
        , m_k(k)
        , m_axis(axis)
        , m_material(m) {
    }

    virtual bool hit(const Ray& r, float t0, float t1, HitRec& hrec) const override {

        int xi, yi, zi;
        vec3 axis;
        switch (m_axis) {
        case kXY: xi = 0; yi = 1; zi = 2; axis = vec3::zAxis(); break;
        case kXZ: xi = 0; yi = 2; zi = 1; axis = vec3::yAxis(); break;
        case kYZ: xi = 1; yi = 2; zi = 0; axis = vec3::xAxis(); break;
        }

        float t = (m_k - r.origin()[zi]) / r.direction()[zi];
        if (t < t0 || t > t1) {
            return false;
        }

        float x = r.origin()[xi] + t*r.direction()[xi];
        float y = r.origin()[yi] + t*r.direction()[yi];
        if (x < m_x0 || x > m_x1 || y < m_y0 || y > m_y1) {
            return false;
        }

        hrec.u = (x - m_x0) / (m_x1 - m_x0);
        hrec.v = (y - m_y0) / (m_y1 - m_y0);
        hrec.t = t;
        hrec.mat = m_material;
        hrec.p = r.at(t);
        hrec.n = axis;
        return true;
    }

private:
    float m_x0, m_x1, m_y0, m_y1, m_k;
    AxisType m_axis;
    MaterialPtr m_material;
};

XY軸, XZ軸,YZ軸は指定できるようにしました.hit関数では平行な軸によって参照するベクトルの要素を切り替えています.テクスチャ座標は次のような計算式で求めています.

u = \frac{x-x_0}{x_1-x_0}, \quad v = \frac{y-y_0}{y_1-y_0}.

それでは四角形をレンダリングしてみます.

// Camera

vec3 lookfrom(13,2,3);
vec3 lookat(0, 1, 0);
vec3 vup(0, 1, 0);
float aspect = float(m_image->width()) / float(m_image->height());
m_camera = std::make_unique<Camera>(lookfrom, lookat, vup, 30, aspect);

// Shapes

ShapeList* world = new ShapeList();
world->add(std::make_shared<Sphere>(
    vec3(0, 2, 0), 2,
    std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.5f, 0.5f, 0.5f)))));
world->add(std::make_shared<Sphere>(
    vec3(0, -1000, 0), 1000,
    std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.8f, 0.8f, 0.8f)))));
world->add(std::make_shared<Rect>(
    3,5,1,3,-2,Rect::kXY,
    std::make_shared<DiffuseLight>(
        std::make_shared<ColorTexture>(vec3(4)))));
m_world.reset(world);

あと,サンプリング数も 500 に設定しています.これは次のようになります.(rayt205.cpp)

tuto-raytracing-rect-output.png

四角形のまわりに白いつぶつぶのようなものがあります.また,四角形自身も若干暗く見えませんか? 発光の強さを 1 以上にしたのでオーバーフローを起こしているようです.内部では float(32bit) で値を計算していますが,unsigned char(8bit, 範囲は[0,255]) に変換しているときに起きているようです.コンピュータグラフィックスでは,このように [0,255] = [0.0,1.0] の範囲のことを LDR (Low Dynamic Range)といい,それ以上の範囲のことを HDR (High Dynamic Range)といいます.内部では HDR で計算していることになります.HDR から LDR に変換することを「トーンマッピング」と呼ぶことがあります.今回は,LDR に変換するときに単純にクランプ処理([0,1]に切り詰める)をします.ImageFilter クラスを継承して,TonemapFilter として実装します.

// rayt.h
class TonemapFilter : public ImageFilter {
public:
    TonemapFilter() {}
    virtual vec3 filter(const vec3& c) const override {
        return minPerElem(maxPerElem(c, Vector3(0.f)), Vector3(1.f));;
    }
};

Image クラスで TonemapFilter をフィルターリストに追加します.

Image(int w, int h) {
    m_width = w;
    m_height = h;
    m_pixels.reset(new rgb[m_width*m_height]);
    m_filters.push_back(std::make_unique<GammaFilter>(GAMMA_FACTOR));
    m_filters.push_back(std::make_unique<TonemapFilter>());
}

もう一度レンダリングすると次のようになります.(rayt206.h)

tuto-raytracing-rect-tonemap-output.png

つぶつぶが無くなっていて,明るくなっているのがわかると思います.

コーネルボックス

発光材質と四角形を追加したので,有名なコーネルボックスを作ってみます.

void build() {

    m_backColor = vec3(0);

    // Camera

    vec3 lookfrom(278,278,-800);
    vec3 lookat(278, 278, 0);
    vec3 vup(0, 1, 0);
    float aspect = float(m_image->width()) / float(m_image->height());
    m_camera = std::make_unique<Camera>(lookfrom, lookat, vup, 40, aspect);

    // Shapes

    MaterialPtr red = std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.65f, 0.05f, 0.05f)));
    MaterialPtr white = std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.73f)));
    MaterialPtr green = std::make_shared<Lambertian>(
        std::make_shared<ColorTexture>(vec3(0.12f, 0.45f, 0.15f)));
    MaterialPtr light = std::make_shared<DiffuseLight>(
        std::make_shared<ColorTexture>(vec3(15.0f)));

    ShapeList* world = new ShapeList();
    world->add(
        std::make_shared<Rect>(
        0,555,0,555,555,Rect::kYZ,green));
    world->add(
        std::make_shared<Rect>(
        0,555,0,555,0,Rect::kYZ,red));
    world->add(
        std::make_shared<Rect>(
        213,343,227,332,554,Rect::kXZ,light));
    world->add(
        std::make_shared<Rect>(
        0, 555, 0, 555, 555, Rect::kXZ, white));
    world->add(
        std::make_shared<Rect>(
        0,555,0,555,0,Rect::kXZ,white));
    world->add(
        std::make_shared<Rect>(
        0,555,0,555,555,Rect::kXY,white));
    m_world.reset(world);
}

この結果は次のようになります.(rayt207.cpp)

tuto-raytracing-cornelbox-output.png

ノイズのようなものが目立ちますが,これは光源が小さいので,光線が光源に到達しないところが出てきているからです.また,壁がいくつか見えません.これは壁の法線が反対側を向いているからです.そのため光線が当たったら,法線を反対方向に向かせるようにします.Shape クラスを継承した FlipNormals というクラスを追加します.これは別の Shape クラスインスタンスを持っており,hit 関数では,そのインスタンスの hit 関数を呼んで法線を反転させます.

class FlipNormals : public Shape {
public:
    FlipNormals(const ShapePtr& shape)
        : m_shape(shape) {
    }

    virtual bool hit(const Ray& r, float t0, float t1, HitRec& hrec) const override {
        if (m_shape->hit(r, t0, t1, hrec)) {
            hrec.n = -hrec.n;
            return true;
        }
        else {
            return false;
        }
    }

private:
    ShapePtr m_shape;
};

このような仕組みをデザインパターンでは「Decorator パターン」といいます.これを使って,いくつかの四角形の法線を反転させます.

ShapeList* world = new ShapeList();
world->add(
    std::make_shared<FlipNormals>(
        std::make_shared<Rect>(
        0,555,0,555,555,Rect::kYZ,green)));
world->add(
    std::make_shared<Rect>(
    0,555,0,555,0,Rect::kYZ,red));
world->add(
    std::make_shared<Rect>(
    213,343,227,332,554,Rect::kXZ,light));
world->add(
    std::make_shared<FlipNormals>(
        std::make_shared<Rect>(
        0, 555, 0, 555, 555, Rect::kXZ, white)));
world->add(
    std::make_shared<Rect>(
    0,555,0,555,0,Rect::kXZ,white));
world->add(
    std::make_shared<FlipNormals>(
        std::make_shared<Rect>(
        0,555,0,555,555,Rect::kXY,white)));
m_world.reset(world);

結果は次のようになります.(rayt208.cpp)

tuto-raytracing-cornelbox-flip-output.png

全部の壁が見えるようになったので,箱を追加しましょう.箱は新しい物体として追加します.これは四角形の組み合わせで実装できます.

class Box : public Shape {
public:
    Box() {}
    Box(const vec3& p0, const vec3& p1, const MaterialPtr& m)
        : m_p0(p0)
        , m_p1(p1)
        , m_list(std::make_unique<ShapeList>()) {

        ShapeList* l = new ShapeList();
        l->add(std::make_shared<Rect>(
            p0.getX(), p1.getX(), p0.getY(), p1.getY(), p1.getZ(), Rect::kXY, m));
        l->add(std::make_shared<FlipNormals>(std::make_shared<Rect>(
            p0.getX(), p1.getX(), p0.getY(), p1.getY(), p0.getZ(), Rect::kXY, m)));
        l->add(std::make_shared<Rect>(
            p0.getX(), p1.getX(), p0.getZ(), p1.getZ(), p1.getY(), Rect::kXZ, m));
        l->add(std::make_shared<FlipNormals>(std::make_shared<Rect>(
            p0.getX(), p1.getX(), p0.getZ(), p1.getZ(), p0.getY(), Rect::kXZ, m)));
        l->add(std::make_shared<Rect>(
            p0.getY(), p1.getY(), p0.getZ(), p1.getZ(), p1.getX(), Rect::kYZ, m));
        l->add(std::make_shared<FlipNormals>(std::make_shared<Rect>(
            p0.getY(), p1.getY(), p0.getZ(), p1.getZ(), p0.getX(), Rect::kYZ, m)));
        m_list.reset(l);
    }

    virtual bool hit(const Ray& r, float t0, float t1, HitRec& hrec) const override {
        return m_list->hit(r, t0, t1, hrec);
    }

private:
    vec3 m_p0, m_p1;
    std::unique_ptr<ShapeList> m_list;
};

シーンに箱を2つ追加します.

world->add(
    std::make_shared<Box>(vec3(130, 0, 65), vec3(295,165,230), white));
world->add(
    std::make_shared<Box>(vec3(265, 0, 295), vec3(430, 330, 460), white));

結果は次のようになります.(rayt209.cpp)

tuto-raytracing-cornelbox-box-output.png

法線の反転と同じ方法で,移動と回転の機能を追加します.それぞれ TranslateRotate クラスとします.

class Translate : public Shape {
public:
    Translate(const ShapePtr& sp, const vec3& displacement)
        : m_shape(sp)
        , m_offset(displacement) {
    }

    virtual bool hit(const Ray& r, float t0, float t1, HitRec& hrec) const override {
        Ray move_r(r.origin() - m_offset, r.direction());
        if (m_shape->hit(move_r, t0, t1, hrec)) {
            hrec.p += m_offset;
            return true;
        }
        else {
            return false;
        }
    }

private:
    ShapePtr m_shape;
    vec3 m_offset;
};

class Rotate : public Shape {
public:
    Rotate(const ShapePtr& sp, const vec3& axis, float angle)
        : m_shape(sp)
        , m_quat(Quat::rotation(radians(angle), axis)) {
    }

    virtual bool hit(const Ray& r, float t0, float t1, HitRec& hrec) const override {
        Quat revq = conj(m_quat);
        vec3 origin = rotate(revq, r.origin());
        vec3 direction = rotate(revq, r.direction());
        Ray rot_r(origin, direction);
        if (m_shape->hit(rot_r, t0, t1, hrec)) {
            hrec.p = rotate(m_quat, hrec.p);
            hrec.n = rotate(m_quat, hrec.n);
            return true;
        }
        else {
            return false;
        }
    }

private:
    ShapePtr m_shape;
    Quat m_quat;
};

このような移動や回転をさせるようなことを「アフィン変換」といいます.基本は変換する前の状態で当たり判定を行い,結果に対してアフィン変換することで実装することができます.回転はクォータニオンを用いて処理しています.

Decoratorパターンは強力ですが,階層が多くなってしまい,コードが見づらくなってしまいます.例えば,法線反転・移動・回転をする四角形の場合,次のようになります.

world->add(
    std::make_shared<Translate>(
        std::make_shared<Rotate>(
            std::make_shared<FlipNormals>(
                std::make_shared<Rect>(213, 343, 227, 332, 500, Rect::kXZ, red)),
            vec3(0,1,0), 45),
        vec3(0,0,0)));

個人の好みなので,もしかすると上記のコードで十分かもしれません.個人的に好きではないので,少し改善してみます.std::make_shared を書くのも面倒ですし,この書き方だと適用される順番が逆順でわかりづらい印象があります.そこで ShapeBuilder クラスを導入します.このメンバ関数はすべて自己参照を返すので,メンバ関数を連続で呼び出すことができます.これを使って,物体と法線反転などの追加を行えるようにします.

class ShapeBuilder {
public:
    ShapeBuilder() {}
    ShapeBuilder(const ShapePtr& sp)
        : m_ptr(sp) {
    }

    ShapeBuilder& reset(const ShapePtr& sp) {
        m_ptr = sp;
        return *this;
    }

    ShapeBuilder& sphere(const vec3& c, float r, const MaterialPtr& m) {
        m_ptr = std::make_shared<Sphere>(c, r, m);
        return *this;
    }

    ShapeBuilder& rect(float x0, float x1, float y0, float y1, float k, Rect::AxisType axis, const MaterialPtr& m) {
        m_ptr = std::make_shared<Rect>(x0, x1, y0, y1, k, axis, m);
        return *this;
    }
    ShapeBuilder& rectXY(float x0, float x1, float y0, float y1, float k, const MaterialPtr& m) {
        m_ptr = std::make_shared<Rect>(x0, x1, y0, y1, k, Rect::kXY, m);
        return *this;
    }
    ShapeBuilder& rectXZ(float x0, float x1, float y0, float y1, float k, const MaterialPtr& m) {
        m_ptr = std::make_shared<Rect>(x0, x1, y0, y1, k, Rect::kXZ, m);
        return *this;
    }
    ShapeBuilder& rectYZ(float x0, float x1, float y0, float y1, float k, const MaterialPtr& m) {
        m_ptr = std::make_shared<Rect>(x0, x1, y0, y1, k, Rect::kYZ, m);
        return *this;
    }

    ShapeBuilder& rect(const vec3& p0, const vec3& p1, float k, Rect::AxisType axis, const MaterialPtr& m) {
        switch (axis) {
        case Rect::kXY:
            m_ptr = std::make_shared<Rect>(
                p0.getX(), p1.getX(), p0.getY(), p1.getY(), k, axis, m);
            break;
        case Rect::kXZ:
            m_ptr = std::make_shared<Rect>(
                p0.getX(), p1.getX(), p0.getZ(), p1.getZ(), k, axis, m);
            break;
        case Rect::kYZ:
            m_ptr = std::make_shared<Rect>(
                p0.getY(), p1.getY(), p0.getZ(), p1.getZ(), k, axis, m);
            break;
        }
        return *this;
    }
    ShapeBuilder& rectXY(const vec3& p0, const vec3& p1, float k, const MaterialPtr& m) {
        return rect(p0, p1, k, Rect::kXY, m);
    }
    ShapeBuilder& rectXZ(const vec3& p0, const vec3& p1, float k, const MaterialPtr& m) {
        return rect(p0, p1, k, Rect::kXZ, m);
    }
    ShapeBuilder& rectYZ(const vec3& p0, const vec3& p1, float k, const MaterialPtr& m) {
        return rect(p0, p1, k, Rect::kYZ, m);
    }

    ShapeBuilder& box(const vec3& p0, const vec3& p1, const MaterialPtr& m) {
        m_ptr = std::make_shared<Box>(p0, p1, m);
        return *this;
    }

    ShapeBuilder& flip() {
        m_ptr = std::make_shared<FlipNormals>(m_ptr);
        return *this;
    }

    ShapeBuilder& translate(const vec3& t) {
        m_ptr = std::make_shared<Translate>(m_ptr, t);
        return *this;
    }

    ShapeBuilder& rotate(const vec3& axis, float angle) {
        m_ptr = std::make_shared<Rotate>(m_ptr, axis, angle);
        return *this;
    }

    const ShapePtr& get() const { return m_ptr; }

private:
    ShapePtr m_ptr;
};

これを使ってコーネルボックスを作成するようにすると

ShapeList* world = new ShapeList();
ShapeBuilder builder;
world->add(builder.rectYZ(0, 555, 0, 555, 555, green).flip().get());
world->add(builder.rectYZ(0, 555, 0, 555, 0, red).get());
world->add(builder.rectXZ(213, 343, 227, 332, 554, light).get());
world->add(builder.rectXZ(0, 555, 0, 555, 555, white).flip().get());
world->add(builder.rectXZ(0, 555, 0, 555, 0, white).get());
world->add(builder.rectXY(0, 555, 0, 555, 555, white).flip().get());
world->add(builder.box(vec3(0), vec3(165), white)
    .rotate(vec3::yAxis(), -18)
    .translate(vec3(130, 0, 65))
    .get());
world->add(builder.box(vec3(0), vec3(165, 330, 165), white)
    .rotate(vec3::yAxis(), 15)
    .translate(vec3(265, 0, 295))
    .get());
m_world.reset(world);

どうでしょうか? こっちの方がスッキリしている感じがします.
これをレンダリングすると次のようになります.下の画像はサンプル数が 2000 です.(rayt210.cpp)

tuto-raytracing-cornelbox-s2000-output.png

レンダリングにも時間がかかるようになってきました.
次回はいよいよ「モンテカルロレイトレーシング」です.