Matrix Transformations

Chapter4

The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer (Pragmatic Bookshelf)

The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer (Pragmatic Bookshelf)

github.com

Transform

Transform が備えるべき要件は以下の通り

  • 逆変換を取得可能
  • Point3D, Vector3D および Transform に対して適用可能

これらを実現するためにはどのような実装が良いかを考える。

逆変換を取得可能

Transform の実装にあたっては、そのままだと Matrix4x4 を wrap した程度に思えるけれど、後々のことを考えると悩ましい点がある。その点とは逆行列の扱い。Matrix4x4inverse() を実装したのだからそれを使うだけに思えるが、毎回 inverse() を呼んで逆行列を計算するは極めて非効率だ。

そのため、Transform には通常の Matrix4x4 を表す mat の他に逆行列 inv も持たせる。

pub struct Transform {
    mat: Matrix4x4,
    inv: Matrix4x4,
}

メンバの matinv を勝手にいじられるとまずいので pub は付けない。

直接のアクセスを禁止したため、inv逆行列を取得できるようにしておく。

    pub fn inv(&self) -> &Matrix4x4 {
        &self.inv
    }

また、Tranform 作成時には同時に逆変換も作成しておく必要がある。その際、 translation, scaling, rotation は inverse() を使わずにもっと簡単な方法で計算できる。

translation の場合は逆方向に同じだけ移動してやれば良いから、移動成分の符号を変える。

    pub fn translation(x: f32, y: f32, z: f32) -> Self {
        let mat = Matrix4x4::new([
            1.0, 0.0, 0.0, x,
            0.0, 1.0, 0.0, y,
            0.0, 0.0, 1.0, z,
            0.0, 0.0, 0.0, 1.0,
        ]);
        let inv = Matrix4x4::new([
            1.0, 0.0, 0.0, -x,
            0.0, 1.0, 0.0, -y,
            0.0, 0.0, 1.0, -z,
            0.0, 0.0, 0.0, 1.0,
        ]);
        Transform { mat, inv }
    }

scaling は s 倍に対して 1/s 倍で元にもどるから、

    pub fn scaling(x: f32, y: f32, z: f32) -> Self {
        assert!(x != 0.0);
        assert!(y != 0.0);
        assert!(z != 0.0);

        let mat = Matrix4x4::new([
            x, 0.0, 0.0, 0.0,
            0.0, y, 0.0, 0.0,
            0.0, 0.0, z, 0.0,
            0.0, 0.0, 0.0, 1.0,
        ]);
        let inv = Matrix4x4::new([
            1.0 / x, 0.0, 0.0, 0.0,
            0.0, 1.0 / y, 0.0, 0.0,
            0.0, 0.0, 1.0 / z, 0.0,
            0.0, 0.0, 0.0, 1.0,
        ]);
        Transform { mat, inv }
    }

のようにする。ここで 0 倍のスケーリングには逆行列が存在しない。公開 API ならちゃんとエラーを返すようにすべきだが、ここでは assert で済ませておく。

rotation を表す行列は直交行列なので transpose をとればそれが逆行列になる。以下は x 軸まわりの回転の場合。

    pub fn rotation_x(a: f32) -> Self {
        let mat = Matrix4x4::new([
            1.0,     0.0,      0.0, 0.0,
            0.0, a.cos(), -a.sin(), 0.0,
            0.0, a.sin(),  a.cos(), 0.0,
            0.0, 0.0, 0.0, 1.0,
        ]);
        let inv = mat.transpose();
        Transform { mat, inv }
    }

上記以外(現在のところ shearing のみ)に限り、逆行列の計算に inverse() を呼ぶ。

Vector3D, Point3D および Transform に対して適用可能

Transform の適用は * で行えるように、Vector3DPoint3D および Transform のそれぞれに対して Mul trait を実装する。Vector3DPoint3D に関しては単に行列をかけてやれば良いだけで特筆すべきことは何もない。一方で Transform 同士の場合には通常の変換 mat だけはなく逆変換を表す inv も考慮しなければならない。

2 つの行列  A B の積  AB逆行列 (AB)^{-1} = B^{-1}A^{-1} になる。よって

    fn mul(self, t: &Transform) -> Self::Output {
        Transform {
            mat: &self.mat * &t.mat,
            inv: &t.inv * &self.inv,
        }
    }

 AB に相当する Transform が得られ、ここでも inverse() を使わずに済む。

ところで、Transform にわざわざ逆行列 inv を持たせているが、これを適用するには

t.inv() * &p;

のように逆行列を取得し、それを直接 Point3D なり Vector3D にかけてやる必要がある。しかもこれでは逆変換を Transform に対して適用することができない。

本来的には逆変換を表す Transform を作成する API を用意すべきなのだろうが、基本的に逆変換はローカルな使用にとどまり、複雑な操作が不要であることからより簡易な形である行列の直接利用で済ませることにした。

f:id:mtXTJocj:20191222174113p:plain