Rust Advent Calendar 2017の1日目です。
初日から変化球という感じですが、申込み時点で初日と最終日しか空いていなかったのでご容赦ください。
はじめに
WebAssemblyによるRustでのWebフロントエンド開発に興味があり、ちょっとだけ記事を書いたりしてきました。
つい先日、Emscriptenに依存せずにWebAssemblyを生成するwasm32-unknown-unknownというターゲットが公式に追加されたりもして(参考:wasm32-unknown-unknown landed & enabled)、Webフロントエンド開発でRustとWebAssemblyが実用的に使われる日が徐々に近づいているように感じます。
WebAssemblyが盛り上がっているところですが、Webブラウザ上でのコンピュータグラフィックスとしてはWebGL2という新しい規格の実装が進んでいます。
本稿の執筆時点では、Google ChromeとFirefox、Safari(有効化設定が必要)でWebGL2がサポートされています。
WebGL2の概要については以下の記事などでまとめられています。
さて本題ですが、今年の春にRust(WebAssembly)とWebGL2を使ったデモ( https://likr.github.io/rust-webgl2-example/ )を公開していました。
カラフルな立方体がクルクル回るという簡単なサンプルです。
これの解説をすると言ったまま年末になってしまったので、この機会に書いておこうと思います。
Rust+EmscriptenでWebGL
はじめに、どのようにしてRustでWebGL2プログラムを書いていくのかの方針について説明します。
EmscriptenでC++のプログラムをJavaScriptにコンパイルする場合、OpenGL ESで記述したコードをWebGLに変換して、そのままWebブラウザで動かすことができます。
Rustでも、asmjs-unknown-emscriptenあるいはwasm32-unknown-emscriptenのターゲットであれば、Emscriptenの提供するAPIを使って同様のことが可能です。
gleamはRust用のOpenGLバインディングです。
gleamを使ってOpenGL ES向けのプログラムを書いておけば、EmscriptenがそれらのコードをWebGLに変換してくれます。
OpenGL(ES)自体は低レイヤー機能の提供しかしないので、より楽にCGプログラミングを行うためにGLFWなどのライブラリがよく利用されます。
Rustでは、GLFWを意識してそれらの機能をPure Rustで実装したglutinというcrateがあります。
glutinはEmscripten環境もサポートしていて、EmscriptenのAPIを意識することなくCGプログラミングをすることができます。
ただし、現時点でglutinは、WebGL1のみをサポートしているので、WebGL2の機能を使うためには他の手段を考える必要があります。
Emscripten自体はWebGL2をサポートしているので、そのAPIを直接使ってやればRustでもWebGL2を利用することができます。
RustでEmscriptenのAPIにアクセスするためのcrateがいくつか公開されているので、その一つであるemscripten-sysを使います。
以下ではコードの一部について具体的に説明していきます。
ソースコードの全体は https://github.com/likr/rust-webgl2-example で公開しています。
RustでWebAssemblyを生成して実行する方法などは、たくさん解説があると思うのでそれらをご参照ください。
WebGL2コンテキストの作成
はじめに、WebGL2のコンテキストを作成します。
JavaScriptだったら、canvas.getContext('webgl2')
のように書くところです。
Emscriptenでは、emscripten_webgl_create_context
の引数であるEmscriptenWebGLContextAttributes
構造体のmajorVersion
に2
を設定することでWebGL2コンテキストを取得できます。
WebGL2コンテキストを取得するコードの全体を以下に示します。
unsafe fn get_gl_context() -> GlPtr {
let mut attributes: EmscriptenWebGLContextAttributes = std::mem::uninitialized();
emscripten_webgl_init_context_attributes(&mut attributes);
attributes.majorVersion = 2;
let handle = emscripten_webgl_create_context(std::ptr::null(), &attributes);
emscripten_webgl_make_context_current(handle);
gl::GlesFns::load_with(|addr| {
let addr = std::ffi::CString::new(addr).unwrap();
emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _
})
}
はじめに、EmscriptenWebGLContextAttributes
構造体を作成します。
そして、emscripten_webgl_init_context_attributes
によってデフォルト値に初期化し、majorVersion
の値を2に変更します。
次に、emscripten_webgl_create_context
とemscripten_webgl_make_context_current
を呼び出し、最後にload_with
のコールバックでemscripten_GetProcAddress
を呼び出します。
emscripten_GetProcAddress
はドキュメント化されていない関数なので、今後もこの通りで良いかは不明瞭なところです。
この辺りの初期化処理はglutinを参考にしています。
gl
を取得してしまえば、例えば以下のようにOpenGL ESの機能を呼び出すことができます。
fn load_shader(gl: &GlPtr, shader_type: GLenum, source: &[&[u8]]) -> Option<GLuint> {
let shader = gl.create_shader(shader_type);
if shader == 0 {
return None;
}
gl.shader_source(shader, source);
gl.compile_shader(shader);
let compiled = gl.get_shader_iv(shader, gl::COMPILE_STATUS);
if compiled == 0 {
let log = gl.get_shader_info_log(shader);
println!("{}", log);
gl.delete_shader(shader);
return None;
}
Some(shader)
}
OpenGL APIのエラー処理をOptionやResultで整理してやることもできそうですね。
OpenGL ESの機能に関しては、Rust + WebAssemblyに固有の話ではないので、今回は詳細を省きます。
gleamのドキュメントなどをご参照ください。
WebGL2としてコンテキストを作成したことで、GLSL ES 3.0でシェーダーを書くことができるようになっています。
フレーム毎の処理
JavaScriptでのWebGLをやったことのある人なら、requestAnimationFrame
を使った処理は行なっているかと思います。
requestAnimationFrame
を使うことで、60FPSでのアニメーションや画面が非アクティブの時に処理を止めるといったことが簡単にできるようになっています。
Emscriptenの環境では、emscripten_set_main_loop
関数によって同様の処理が可能です。
emscripten_set_main_loop
のC++での型定義は以下のようになっています。
typedef void (*em_callback_func)(void);
void emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop);
内部的には、第二引数に0を与えるとrequestAnimationFrame
が、1以上の値を与えるとsetTimeout
が使用されます。
第一引数はフレーム毎に呼び出されるコールバック関数です。
さて、型定義によるとコールバック関数は引数を取らないことがわかります。
WebGLでアニメーションをする場合などは、アプリケーションの状態を保持しておき、フレーム毎の処理でその状態を反映する必要がありますので、これでは困ります。
今回のサンプルでは、フレーム毎に立方体の回転角度を更新して描画を行なっているのがそれに該当します。
グローバル変数を使うというアプローチもありますが、さすがにRustを使っているのにstatic mut
でグローバル変数を安易に作りたくはないですよね。
emscripten_set_main_loop
のコールバックに引数をとるバージョンとして、emscripten_set_main_loop_arg
があります。
型定義は以下の通りです。
typedef void (*em_arg_callback_func)(void*);
void emscripten_set_main_loop_arg(em_arg_callback_func func, void *arg, int fps, int simulate_infinite_loop);
こちらは、コールバック関数の引数をvoid*
として与えることができます。
つまり、Rustでもemscripten_set_main_loop_arg
にコールバック関数と引数を与えてやれば良いわけです。
これらをemscripten-sysで提供されるAPIに基づいて実装していきましょう。
今回は、アプリケーションの状態をApp
という構造体で保持することにします。
Context
にはgl
を持たせておくことで、フレーム毎に再描画処理を行うことができます。
プログラムの概形は以下のようになります。
type GlPtr = std::rc::Rc<gl::Gl>;
#[repr(C)]
struct App {
gl: GlPtr,
// 省略
}
impl App {
fn new(gl: GlPtr) -> App {
// 初期化処理
App {
gl: gl,
}
}
}
extern fn step(app: *mut std::os::raw::c_void) {
unsafe {
let mut app = &mut *(app as *mut App);
// appを使った処理
}
}
fn main() {
unsafe {
let gl = get_gl_context();
let mut app = App::new(gl);
let ptr = &mut app as *mut _ as *mut std::os::raw::c_void;
emscripten_set_main_loop_arg(Some(step), ptr, 0, 1);
}
}
EmscriptenのAPIに渡すstep
関数とApp
構造体はそれぞれextern
と#[repr(C)]
をつけることでFFIに対応する必要があります。
あとは、Appを拡張していけば簡単なCGプログラミングはできそうですね。
おわりに
さて、今回はRustでWebGL2と言いつつも、EmscriptenのAPIを使ったRustプログラミングという感じでした。
ただし、RustでWebGL2をやるために必要なEmscriptのAPIは、WebGLコンテキストの作成と画面ループ処理の部分だけでした。
逆に言えば、これらの処理さえ隠蔽すれば、ネイティブのOpenGL ESと全く同じコードでWebGLプログラムを書くことも可能です。
実際にglutinはそのように実装されていますね。
まだまだこの辺りはRust、C++、JavaScriptの世界をそれぞれ理解しなければ難しいところがあります。
ですが、一度フレームワークのようなものが確立されてしまえば、Rustのパワーで効率良いWebGLプログラミングが可能になるかもしれません。
来年もRustとWebの発展から目が離せませんね!