glslkit
GLSLの #include 解決とリフレクション抽出を行うRuby gem。シェーダのコンパイル
エラーが、#include先の元ファイル名と行番号を持ったRubyの例外として上がる。
なぜこれが要るのか
GLSLには #include が無く、シェーダをファイル単位でモジュール分割できない。
コピー&ペーストで共通コードを重複させるか、1ファイルに書き殴るかの二択になりやすい。
また、コンパイルエラーはドライバから「平坦化後の行番号」でしか返らないため、
#includeを手動展開している場合、エラー行から元のファイルを探し当てるのは
それ自体が調査作業になる。glslkitは#include解決を肩代わりし、平坦化前の
ファイル名・行番号をエラーに復元して返す。
構成
1リポジトリ・3gem構成。core/が将来 ruby.wasm
にバンドルされる際、Rails側のコードと依存を持ち込まないための分離。
| gem | ディレクトリ | 実行時依存 | 内容 |
|---|---|---|---|
glslkit |
core/ |
なし |
#include解決、リフレクション抽出、マニフェスト生成 |
glslkit-rails |
rails/ |
glslkit, railties >= 7.1, propshaft >= 1.3.2
|
Propshaft統合、ビューヘルパー、ライブリロード |
glslkit-webgl |
webgl/ |
glslkit |
ruby.wasm上で動くWebGL2ランタイム(ブラウザ向け) |
glslkit/
├── core/ # gem: glslkit
├── rails/ # gem: glslkit-rails
├── webgl/ # gem: glslkit-webgl
├── examples/rails-demo/ # ライブリロードの実演用Railsアプリ
└── spec/schema/reflection-v1.json # JSON Schema (Draft 2020-12)
インストールと最小の使用例
gem "glslkit"require "glslkit"
resolver = Glslkit::Resolvers::FileSystem.new(load_paths: ["app/shaders"])
source = Glslkit::Preprocessor.new(resolver: resolver).process("pbr.frag")
source.code # => 平坦化済みGLSL (String)。#include解決済み、#lineディレクティブ付き
source.reflection # => Glslkit::Reflection (attributes / uniforms / uniform_blocks / outputs)
source.digest # => SHA256 hex (String)Railsで使う場合、wasm上でWebGLを扱う場合はそれぞれのREADMEを参照:
-
core/README.md —
#include解決、リフレクション抽出、マニフェスト生成の詳細 - rails/README.md — Propshaft統合、ビューヘルパー、rakeタスク
- webgl/README.md — ruby.wasm上でのWebGL2ランタイム、rbwasmでのパッケージング
glslkit-webglのページはfile://ではなくHTTP経由で開くこと。 ruby.wasmが
エントリの.rbをfetchで読み込むため、file://ではSafariが確実に失敗する
(Fetch API cannot load file:///... due to access control checks.)。Chromeは
現状緩めに動くが将来同じ制限が入る可能性がある。python3 -m http.server 8000
等でローカルサーバを立ててhttp://localhost:8000/...として開くこと(詳細は
webgl/README.md参照)。
ライブリロード
.glsl/.vert/.fragを保存すると、ページをリロードせずにシェーダだけ
差し替わる。壊すと直前の描画を保ったまま元ファイル名・行番号でエラーが出て、
直すとリロード無しで復帰する。実際に動かして試せる最小Railsアプリを
examples/rails-demo/に用意している
(development専用)。
実測値(Chrome)
- 描画: 60fps / 30秒1801フレームでフレーム落ちゼロ / 最悪フレーム17.7ms
- GC: 計測中に12回発生したが、発生フレームも通常フレームと区別がつかなかった
起動コスト
ruby.wasmのバイナリ(ruby+stdlib.wasm)は約9MBある。
- 初回訪問(キャッシュ無し): 約9MBの転送 + 起動まで中央値約1,130ms
- 2回目以降(HTTPキャッシュ有効): 中央値117〜178ms(fetch+compile / VM初期化 / 最初の行の実行、の合計。計測環境により倍近い差が出る)
いずれもChrome実測。ネットワーク環境により初回は大きく変動する。
ruby.wasmにはstdlibを絞ったビルドもあり、glslkit-webglが実際に使うstdlibは
限られるため、カスタムビルドで転送量を減らせる可能性がある(未検証)。
できないこと
以下は実測に基づく制約であり、設計上の欠陥ではない(詳細は
webgl/README.md参照):
- 毎フレームの頂点データ更新
- CPUスキニング
- モーフターゲットのCPU補間
- パーティクル(CPU側で毎フレーム更新するもの)
100,000要素の転送だけで18.8msかかり、60fpsのフレーム予算を超える。 ジオメトリは静的に保ち、変形はシェーダ側(GPU)で行い、行列やウェイトだけを uniformとして送ること。
検証環境
- Chrome: 描画・フレーム安定性・起動コスト・ライブリロードを実測
- Safari: 描画のみ確認(neonサンプルが正しく動作)。フレーム性能とライブリロードは未計測
いずれもmacOS。他のブラウザ・OSは未確認。
