glslkit-webgl

glslkit-webgl は、ruby.wasmでブラウザ上で動くRubyプログラム向けのWebGL2 ランタイム。glslkitのリフレクションマニフェストを読み込み、WebGLの参照を キャッシュし、シェーダのコンパイルエラーをRubyの例外として報告する。

HTTP経由で開くこと

glslkit-webglを使うページはfile://ではなくHTTP経由で開くこと。 ruby.wasmはRubyのエントリファイルをfetch()で読み込むため、ブラウザは file://オリジンに対してこれを制限する:

Cross origin requests are only supported for HTTP.
Fetch API cannot load file:///.../neon.rb due to access control checks.

Safariはこのエラーで確実に失敗する。Chromeは現状file://でも動くが、 将来にわたって保証されているわけではない。ローカルでディレクトリを配信し、 http://として開くこと:

python3 -m http.server 8000

http://localhost:8000/webgl/sample/index.htmlとして開く。 glslkit-webgl.js自身もfile://オリジンを検出し、同じ警告をページ上に表示する。

Quick start

このリポジトリをHTTP経由で配信し(上記参照)、webgl/sample/index.htmlを開く。 このサンプルは、webgl/sample/shaders/cube.{vert,frag}からrake glslkit:embed で生成された、アプリケーション側のJavaScriptを使わない回転するテクスチャ付き キューブを描画する。webgl/sample/neon.htmlは同じ方法で生成された、 アニメーションするフラグメントシェーダのレイマーチングデモ。 webgl/sample/neon-error.htmlcommon/sdf.glsl(#includeされるファイルで あり、トップレベルのシェーダではない)を意図的に壊し、CompileErrorが 元のincludeされたファイルと行番号を報告することを実演する。正常なシェーダに 戻して描画を開始するボタンも付いている。

<canvas id="canvas" width="640" height="480"></canvas>
<script src="/path/to/glslkit-webgl.js" data-entry="./app.rb"
        data-ruby-version="4.0" data-wasm-wasi-version="2.10.1"></script>

このシムはruby.wasmを起動するだけ。JS::RequireRemoteがRubyファイルを fetchし、描画ロジックはすべてRuby側に残る。window.glslkitWebGLReadyは エントリファイルの読み込みが終わった時点で確定する。パッケージ化したwasm ビルドでは、両方のgemを通常通りインストールすればよい。

単体のruby.wasmビルドへのパッケージング(rbwasm)

上記のQuick StartはDefaultRubyVM/JS::RequireRemoteを使っており、実行時に HTTP経由で.rbソースファイルをfetchするためビルド手順は不要。代わりに glslkit-webgl(と依存するglslkit)を単一の.wasmバイナリに事前にバンドル したい場合、想定している経路はruby_wasm gemのrbwasm build CLIを使う方法:

gem install ruby_wasm
# Gemfile: gem "glslkit-webgl"
bundle exec rbwasm build -o app.wasm

これはruby_wasm/rbwasmの標準的な手順であり、glslkit-webgl固有の追加の パッケージング要件は無い。通常のbundle installだけでglslkit-webglと 依存するglslkitがRubyGemsから正しく解決される。

このリリースを準備したサンドボックス環境では、著者はrbwasm buildを最後 まで実行できなかった。 ただしこの障害はglslkit-webglとは無関係: rbwasm buildは最初にネイティブの「baseruby」ツールチェーンをビルドするが、 このサンドボックスのカーネル拡張へのアクセスが制限されていたため、CRuby 自身のdtrace/probes.dのコンパイルで失敗した(Failed to query kext info (MAC policy error 0x1)error: unable to open output file '/dev/fd/7': 'Operation not permitted')。これはどのgemのコードにも到達する遥か手前で 起きており、この環境でバンドルするどんなgemでも同様に失敗するはず。 制約の無いマシンで成功した場合は報告してもらえるとありがたい。

ランタイムAPI

require "glslkit/webgl"

ctx = Glslkit::WebGL.context("#canvas")
manifest = Glslkit::Manifest.parse(MANIFEST_JSON)
program = ctx.program(manifest, "main", vertex: VERTEX_SOURCE,
  fragment: FRAGMENT_SOURCE,
  source_maps: {vertex: vertex_map, fragment: fragment_map})
geometry = ctx.geometry(program: program, attributes: {
  a_position: {data: positions, components: 3},
  a_uv: {data: uvs, components: 2}
}, indices: indices)
texture = ctx.texture2d(width: 2, height: 2, data: rgba_pixels, unit: 0)

matrix = Array.new(16, 0.0)
ctx.loop do |seconds|
  Glslkit::WebGL::Matrix.rotation_z!(matrix, seconds)
  program.set(:u_transform, matrix)
  program.set(:u_texture, texture.unit)
  texture.bind
  ctx.draw(geometry)
end

未知のuniformや不正なelement_countは即座に例外を投げる。GLSLの最適化で 削除されたlocationは黙って無視される。ジオメトリデータは型付き配列のfromで 一度だけアップロードされ、意図的にミューテーションAPIは無い。

シェーダエラーとソースマップ

各ステージのGlslkit::SourceMapsource_maps:に渡す。認識できたドライバの 行番号は元のincludeされたファイルに解決される。CompileErrorstagefilelineraw_logを公開する。未知のログ形式でも例外は投げるが、 filelinenilになる。リンク失敗はLinkErrorを投げる。JSのログの値は パース前に#to_sで変換する。

デバッグモード

Glslkit::WebGL.debug = trueにすると、各フレームの終わりにgetErrorを 1回呼ぶ。デフォルトはfalseで、getErrorはWebGLのコマンドキューを同期 させるため、有効なままにしないこと。

性能とスコープの制限

以下のCPU側のワークロードはv0.1では未対応:

  • 毎フレームの頂点データ差し替え
  • CPUスキニング
  • モーフターゲットのCPU補間
  • パーティクル(CPU側で毎フレーム更新するもの)

設計時の計測では、100,000要素のfloatの転送だけで18.8msかかり、60fpsの フレーム予算を超える。ジオメトリは静的に保ち、頂点の変形はシェーダ側で行い、 行列とウェイトだけをuniformとして送ること。フレームごとのボーン行列計算の コストは未計測。

このランタイムはWebGL2専用。WebGL1へのフォールバック、シーングラフ、 マテリアルシステム、glTF/VRMローダーは無い。

JSの値の安全な扱い(R6)

JS::Objectかもしれない値に対して#classを呼ばないこと。JavaScriptの プリミティブはReflect.hasで例外になることがある。判定にはis_a?(JS::Object) を、表示には#inspect#typeof#to_sを使う。実装のRubyファイルに対して 意図しない.class呼び出しをスキャンするテストがある。

通常のCRubyでは、ruby.wasmのjsライブラリが必要なため、即座に明確な メッセージ付きで失敗する。