ロボットシミュレータ自作物語 - Day 2: arrayfireで線形代数 & Rustの生ポインタ

引っ越しの準備に追われてあまりコードを書いている時間がありません。

TL; DR

arrayfireでArrayの中身をRustの配列として取り出すのがちょっとめんどくさい

今日の目標

キーボード入力によってティーポットをぐるぐる動かせるようにします。

成果物

f:id:iTakeshi:20170221123803g:plain

gliumでキー入力

キー入力は、描画される各フレームごとにglium::glutin::Event::KeyboardInputとして取得できます。KeyboardInput

pub enum Event {
    // ...
    KeyboardInput(ElementState, ScanCode, Option<VirtualKeyCode>),
    // ...
}

として定義されているので、ここからVirtualKeyCodeを取り出してやるとどのキーが押されたか判定できます。

// application main loop
loop {
    for ev in display.poll_events() {
        use glium::glutin::{ElementState, Event, VirtualKeyCode};
        match ev {
            Event::Closed => return, // Windowが閉じられたらloopを抜ける→アプリケーション終了
            Event::KeyboardInput(ElementState::Pressed, _, Some(code)) => {
                match code {
                    VirtualKeyCode::S => model_z -= 1.0, // move backward
                    VirtualKeyCode::W => model_z += 1.0, // move forward
                    // other moves
                    _ => (), // 他のキーに対するfallbackを用意しておかないとコンパイルが通らない
                }
            },
            _ => ()
        }
    }
}

arrayfireでモデル変換行列を計算

※脚注※ この記事を書いている最中に気づいたことですが、gliumの依存crateとしてnalgeblaという線形代数ライブラリが入っており、普通はこっちを使うのだと思います。しかもnalgebraにはCGで使う基本的な変換行列(translation, rotation, transformation, …)を一発で生成できる関数も入っているようなので書き換える予定です。

前回の記事でも紹介したこの記事にあるような変換行列を用意し、Vertex shaderに渡してやるとティーポット動かしたり回したりできます。三次元空間を扱うのになぜ4x4の行列なのか?という疑問があるかもしれませんが、この4行目・4列目を加えることによって複数の変換行列に対して単純に内積を求めるだけですべての変換を適用できるというすばらしいメリットのためです(以下のコード中のpositionpitchについて、紙と鉛筆で内積を取ってみましょう!)

線形代数演算にはarrayfire-rustを使用します。これはCargoで入れる前にいくつかdependencyがありますので、READMEを参照して準備してください。 注意点として、OpenGLに渡す行列はすべてColumn-major orderです。要するに「行が横」ではなく「列が横」です*1。したがって、position行列で座標は4目ではなく4目になり、回転行列ではsinの符号が直感とは逆になっています。

extern crate arrayfire as af
fn model_matrix(pos: &[f32; 3], rot: &[f32; 3]) -> [[f32; 4]; 4] {
    use af::{Array, Dim4};

    // ティーポットモデルは -100 <= x, y, z <= 100 の座標空間で作られているので、1/100に縮小
    let base = Array::new(&[
        0.01, 0.0 , 0.0 , 0.0,
        0.0 , 0.01, 0.0 , 0.0,
        0.0 , 0.0 , 0.01, 0.0,
        0.0 , 0.0 , 0.0 , 1.0f32,
    ], Dim4::new(&[4, 4, 1, 1]));

    // 平行移動
    let position = Array::new(&[
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        pos[0], pos[1], pos[2], 1.0f32,
    ], Dim4::new(&[4, 4, 1, 1]));

    // x軸まわりの回転
    let pitch = Array::new(&[
        1.0, 0.0, 0.0, 0.0,
        0.0,  rot[0].cos(), rot[0].sin(), 0.0,
        0.0, -rot[0].sin(), rot[0].cos(), 0.0,
        0.0, 0.0, 0.0, 1.0f32,
    ], Dim4::new(&[4, 4, 1, 1]));

    // y軸まわりの回転
    let yaw = Array::new(&[
         rot[1].cos(), 0.0, rot[1].sin(), 0.0,
        0.0, 1.0, 0.0, 0.0,
        -rot[1].sin(), 0.0, rot[1].cos(), 0.0,
        0.0, 0.0, 0.0, 1.0f32,
    ], Dim4::new(&[4, 4, 1, 1]));

    // z軸まわりの回転
    let roll = Array::new(&[
         rot[2].cos(), rot[2].sin(), 0.0, 0.0,
        -rot[2].sin(), rot[2].cos(), 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0f32,
    ], Dim4::new(&[4, 4, 1, 1]));

    use af::matmul;
    use af::MatProp::NONE;
    let res = matmul(&matmul(&matmul(&matmul(&roll, &yaw, NONE, NONE), &pitch, NONE, NONE), &position, NONE, NONE), &base, NONE, NONE);

    // 生ポインタを利用してRust配列の取り出し。次節にて解説。
    unsafe {
        *(res.device_ptr() as *const [[f32; 4]; 4])
    }
}

※脚注※ この実装では各種変換行列をすべて内積しひとつにまとめてからOpenGLに渡していますが、それぞれの変換行列をOpenGLに渡してVertex shaderでかけざんしてもらってもいいです、というかそっちのほうが速いような気がするのでこのコードは消滅する予定です。

Rustの生ポインタ

arrayfireはRustのFFI(Foreign Fuction Interface)を利用しています。つまり、arrayfire (C++ Ver.)をbackendとして、arrayfire-rustがそのwrapperになっています。mutmulで行列の内積を計算すると、その結果はC++が確保したメモリの中にあり、Rustからは直接利用できません。af::Array::device_ptr()は、この計算結果に対するC++のポインタ(unsigned long long)を返します。Rustからこの値を取得するためには、

  1. まずポインタが指している型が何なのかを指定し: res.device_ptr() as *const [[f32; 4]; 4]
  2. dereferenceし: *(...)
  3. さらに、外部メモリを参照する行為は闇のパワーを利用する業なのでそのことを明示します: unsafe { ... }

Step1で型名[[f32; 4]; 4]の前についている*constは生ポインタの指す値を定数として利用するときに使い、逆にその値を書き換える場合には*mutとして参照します。その他詳しいことは公式docへ。

Day 3に向けて

よくわからないままコードを書き足してきたのでぐちゃぐちゃです(この記事にコードスニペットしか載っておらずgithubへのリンクがないのはそのためです)。 必要な範囲でリファクタリングしてGitHubに公開します。そしてまだBlenderがよくわかってないのでそっちの勉強もしていきます。

*1:なぜこんな紛らわしいことになっているのかと思わないでもないですが、おそらくGPU上で性能良く演算するためにはこっちのほうが良いのでしょう