ロボットシミュレータ自作物語 - Day 2: arrayfireで線形代数 & Rustの生ポインタ
引っ越しの準備に追われてあまりコードを書いている時間がありません。
TL; DR
arrayfireでArray
の中身をRustの配列として取り出すのがちょっとめんどくさい
今日の目標
キーボード入力によってティーポットをぐるぐる動かせるようにします。
成果物
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列目を加えることによって複数の変換行列に対して単純に内積を求めるだけですべての変換を適用できるというすばらしいメリットのためです(以下のコード中のposition
とpitch
について、紙と鉛筆で内積を取ってみましょう!)
線形代数演算には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からこの値を取得するためには、
- まずポインタが指している型が何なのかを指定し:
res.device_ptr() as *const [[f32; 4]; 4]
- dereferenceし:
*(...)
- さらに、外部メモリを参照する行為は闇のパワーを利用する業なのでそのことを明示します:
unsafe { ... }
Step1で型名[[f32; 4]; 4]
の前についている*const
は生ポインタの指す値を定数として利用するときに使い、逆にその値を書き換える場合には*mut
として参照します。その他詳しいことは公式docへ。
Day 3に向けて
よくわからないままコードを書き足してきたのでぐちゃぐちゃです(この記事にコードスニペットしか載っておらずgithubへのリンクがないのはそのためです)。 必要な範囲でリファクタリングしてGitHubに公開します。そしてまだBlenderがよくわかってないのでそっちの勉強もしていきます。