WebGL
rust
Emscripten
WebGL2
WebAssembly

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の概要については以下の記事などでまとめられています。
* WebGL 2.0の概要
* なにが変わるの WebGL 2.0 - 新しい次世代 WebGL 実装について知ろう

さて本題ですが、今年の春に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構造体のmajorVersion2を設定することで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_contextemscripten_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の発展から目が離せませんね!