正規直交基底

前節では \(z\) 軸を基準にランダムな方向を生成する方法を導出した。次は物体表面の法線を基準に同じことを行いたい。

相対座標

互いに直交する三つの単位ベクトルの集合を正規直交基底 (orthonormal basis) と呼ぶ。例えばデカルト座標系の \(x\), \(y\), \(z\) 軸方向の単位ベクトルからなる集合は ONB である。私はよく忘れてしまうのだが、現実世界における物体の位置と傾きを決めるには、基準となる ONB がどこかに必要となる。同様に仮想空間にある物体の位置と傾きを記述するには ONB をどこかに配置しなければならない。写真とはカメラから見たシーンの相対的な位置と傾きを写したものであり、カメラとシーンが同じ座標系で表されている限りどんな座標系でも問題はない。

原点を \(\textbf{O}\) として、デカルト座標系の単位ベクトルを \(\textbf{x}\), \(\textbf{y}\), \(\textbf{z}\) とする。私たちが「位置 \((3, -2, 7)\)」 と言うとき本当に意味しているのは、 \[ {\footnotesize \text{位置}} = \mathbf{O} + 3\mathbf{x} - 2\mathbf{y} + 7\mathbf{z} \] である。原点 \(\textbf{O}'\) と基底ベクトル \(\textbf{u}\), \(\textbf{v}\), \(\textbf{w}\) を使ってこの位置を表すのであれば、 \[ {\footnotesize \text{位置}} = \mathbf{O}' + u\mathbf{u} + v\mathbf{v} + w\mathbf{w} \] となるように実数 \((u, v, w)\) を選ぶことになる。

正規直交基底の構築

グラフィクス入門の講義を取ると、座標系や 4×4 の座標変換行列についての説明に長い時間が費やされる。これを無視してはいけない。グラフィクスで重要な概念なのだから! しかし今の私たちにはこの概念は必要ない。今考えているのは法線 \(\textbf{n}\) を基準とした特定の分布に従うランダムな方向の生成である。方向は基準となる原点を持たないので、原点を考える必要ない。ただし \(\textbf{n}\) に直交して互いに直行する二つの余接ベクトルは必要になる。

モデルによっては各面の余接ベクトルが一つ以上保存されている場合もある。モデルの余接ベクトルが一つしかない場合には、ONB の計算に少し手間がかかる。長さが \(0\) より大きく \(\textbf{n}\) と平行でない適当なベクトルを \(\textbf{a}\) とすれば、\(\textbf{n}\) と垂直で互いに直交する二つのベクトル \(\textbf{s}\), \(\textbf{t}\) は次の式から得られる: \[ \begin{aligned} \mathbf{t} & = \text{unit\_vector}(\mathbf{a} \times \mathbf{n}) \\ \mathbf{s} & = \mathbf{t} \times \mathbf{n} \end{aligned} \]

このやり方は間違っていないが、モデルを読み込んでも \(\textbf{a}\) は得られない点が問題となる。よく考えずに適当な \(\textbf{a}\) を選ぶと、\(\textbf{n}\) に平行な \(\textbf{a}\) を選んでしまう可能性がある。これに対処するためによく使われるのが、if を使って \(\textbf{n}\) がある軸に平行かどうかを判定し、もし平行でなければその軸を \(\textbf{a}\) として採用するという方法である:

if absolute(n.x > 0.9)
  a  (0, 1, 0)
else
  a  (1, 0, 0)

ONB \(\textbf{s}\), \(\textbf{t}\), \(\textbf{n}\) が計算できたら、次に \(z\) 軸を基準にしたランダムな方向ベクトル \(\text{random}(x,y,z)\) を生成する。すると法線 \(\textbf{n}\) を基準にしたランダムな方向ベクトルは \[ x \mathbf{s} + y \mathbf{t} + z \mathbf{n} \] と表せる。

カメラからレイを得るときにも同じような数式を使ったことに気が付くかもしれない。あの処理はカメラの座標系への座標変形とみなすことができる。

ONB を表すクラス

ONB のためにクラスを作るべきだろうか、それともユーティリティ関数で十分だろうか? 私には分からない。ただクラスがユーティリティ関数よりずっと複雑になることはなさそうなので、クラスを作る:

class onb {
public:
  onb() {}
  inline vec3 operator[](int i) const { return axis[i]; }

  vec3 u() const { return axis[0]; }
  vec3 v() const { return axis[1]; }
  vec3 w() const { return axis[2]; }

  vec3 local(double a, double b, double c) const {
    return a*u() + b*v() + c*w();
  }

  vec3 local(const vec3& a) const {
    return a.x()*u() + a.y()*v() + a.z()*w();
  }

  void build_from_w(const vec3&);

public:
  vec3 axis[3];
};

void onb::build_from_w(const vec3& n) {
  axis[2] = unit_vector(n);
  vec3 a = (fabs(w().x()) > 0.9) ? vec3(0,1,0) : vec3(1,0,0);
  axis[1] = unit_vector(cross(w(), a));
  axis[0] = cross(w(), v());
}
リスト 3.18 [onb.h] 正規直交基底を表すクラス

これを使って lambertian マテリアルを描き直そう:

bool scatter(
  const ray& r_in,
  const hit_record& rec,
  color& alb,
  ray& scattered,
  double& pdf
) const {
  onb uvw;
  uvw.build_from_w(rec.normal);
  vec3 direction = uvw.local(random_cosine_direction());
  scattered = ray(rec.p, unit_vector(direction), r_in.time());
  alb = albedo->value(rec.u, rec.v, rec.p);
  pdf = dot(uvw.w(), scattered.direction()) / pi;
  return true;
}
リスト 3.19 [material.h] 正規直交基底を使った scatter 関数

次の画像が得られる:

図 3.9: 正規直交基底を使った scatter 関数で計算したコーネルボックス

これでようやく、ライトに向けてサンプルを多く飛ばす準備が整った。