niba1122.dev

Rust+WebAssemblyでMDNのWebGLチュートリアルを写経する

December 12, 2019

WebAssemblyで3Dを描画してみたかったので、WebGLの学習も兼ねてMDNのWebGLチュートリアルをRust+WebAssemblyで写経してみました。 ちなみに、MDNのWebGLチュートリアルはGitHubのコード例があるのですが、コード例だけがリファクタされてしまったためにサイト上のコードと大きく食い違っています。今回はGitHubのコードを参考にしました。

image.png

実際のコードや動作は以下です。

写経しながらハマった点などを簡単に説明します。

必要なもの

  • rustup
  • wasm-pack
  • npm

プロジェクト作成

https://rustwasm.github.io/docs/book/game-of-life/hello-world.html#clone-the-project-template に従ってプロジェクトを作成しました。RustのプロジェクトのサブディレクトリにWebアプリを作り、そこからWebAssemblyを読み込んで実行する構成になっています。wasm-packを使ってコンパイルした生成物をimport文で指定すると、webpackがいい感じに繋ぎ込んでくれます。エコシステムが整備されてかなり簡単になりました。

JavaScriptのブラウザAPIはどうやって呼ぶ?

wasm-bindgenを使います(上のサンプルにはデフォルトで入っています)。wasm-bindgenはJavaScriptのブラウザAPIとRustを繋ぎこむライブラリです。これを用いることで、ブラウザAPIをJavaScriptライクにRustから呼び出すことができます。

例)

let document = web_sys::window()
    .unwrap()
    .document()
    .unwrap();

let canvas = document
    .get_element_by_id(id)
    .unwrap()
    .dyn_into::<web_sys::HtmlCanvasElement>()
    .unwrap();

wasm-bindgenとブラウザAPIのインターフェース非常に似ていますが、やはり言語による違いがあるのでいくつか紹介していきたいと思います。

関数名の違い

基本的にはキャメルケース→スネークケースでいけますが、引数の型ごとに別のメソッド名になっている場合もあります。

JavaScript Rust
bufferData buffer_data_with_array_buffer_view
vertexAttribPointer vertex_attrib_pointer_with_i32
uniformMatrix4fv uniform_matrix4fv_with_f32_array
drawElements draw_elements_with_i32

参照渡し

JavaScriptでオブジェクトを受け取る関数は、基本的に構造体を参照を受け取る形になっています。

例)

context.link_program(&shader_program)

Option型

JavaScriptでnullの可能性がある場合はOption型になっています。これらを全てハンドリングする必要があります。 (今回は大着して大体unwrap()しちゃってます)

let some_dom: Option<web_sys::Element> = document
     .get_element_by_id(id)

キャスト

DOMを取得する関数の戻り値はElement型なので、特定のDOMの型にキャストする必要があります。

let canvas = document
    .get_element_by_id(id)
    .unwrap()
    .dyn_into::<web_sys::HtmlCanvasElement>()
    .unwrap();

ビルトイン配列の型

Float32Arrayなどのビルトインされた配列型は、プリミティブ型ではなくjs-sysの型にする必要があります。

let vertices = [
    //...
]
context.buffer_data_with_array_buffer_view(
    WebGlRenderingContext::ARRAY_BUFFER,
    &js_sys::Float32Array::view(&vertices),
    WebGlRenderingContext::STATIC_DRAW
);

定数

WebGlRenderingContextに同じ名前で定義されています。

例)

WebGlRenderingContext::COMPILE_STATUS

行列計算ライブラリ

チュートリアルでは座標変換の行列計算をするためにgl-matrixというライブラリを使っています。JavaScriptのライブラリをそのまま使うわけにはいかないので、今回はnalgebra-glmというライブラリを使いました。(ちなみに、今回のサンプル程度なら自分で実装することも全然可能です)

Projection Matrix

extern crate nalgebra_glm as glm;

let field_of_view = 45.0 * std::f32::consts::PI / 180.0;
let aspect = canvas.client_width() as f32 / canvas.client_height() as f32;
let z_near = 0.1;
let z_far = 100.0;

let projection_matrix = glm::perspective(aspect, field_of_view, z_near, z_far);
let vec_projection_matrix = projection_matrix.iter().map(|v| *v).collect::<Vec<_>>();

Model View Matrix

extern crate nalgebra_glm as glm;

let model_view_matrix = glm::translate(&glm::Mat4::identity(), &glm::TVec3::new(-0.0, 0.0, -6.0));

非同期処理

図形をアニメーションで動かすために、requestAnimationFrameを再帰的に呼び出す必要があります。 wasm-bindgenのサンプルを参考にして実装しました。

let f = Rc::new(RefCell::new(None));
let g = f.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
    draw_scene(
        &context,
        &shader_program,
        vertex_position,
        vertex_normal,
        &program_projection_matrix,
        &program_model_view_matrix,
        &program_normal_matrix,
        &position_buffer,
        &cube_vertices_index_buffer,
        &cube_vertices_normal_buffer,
        start_time,
        get_current_time()
    );

    request_animation_frame(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));

request_animation_frame(g.borrow().as_ref().unwrap());

上記の書き方はアニメーションを途中停止させるケースを考慮しており、仮に停止させた場合にはクロージャが破棄されます。これを考慮しない場合はもうちょっとだけ簡単に書けますが、この記事では扱いません。

まとめ

非同期処理の実装でRustのライフタイムを考慮するのに苦労しましたが、wasm-packに含まれるwasm-bindgenがブラウザのAPIをしっかりサポートしているおかげで無事に写経できました。wasm-bindgen便利です。