OpalでThree.jsを使う
この記事はOpal Advent Calendar 2016とThree.js Advent Calendar 2016の6日目の記事です。両方のアドベントカレンダーが空いていたので、こう、くっつけたらどうなるかなっていう…。安易な発想ですいません。
Opalとは
OpalはJavaScriptで書かれたRuby処理系です。ブラウザ上でRubyのコードを動かすことができます。
とりあえず検索してみる
opalとthree.jsで検索してみるとthree.rbというリポジトリが引っかかりました。1年ほど更新がないですが、とりあえず先人はいるみたいです。
とりあえず動かしてみる
リポジトリをgit cloneします。demoというディレクトリがあるので、これを試してみましょうか。Rackアプリになっているようなので、rackupで起動してブラウザで見てみます。
$ cd demo/
$ bundle install
$ bundle exec rackup
$ open http://localhost:9292/
結果
高速で回転する立方体が出現しました。(これはgifなので本物はもっと滑らかです) どうやら動いているようです。
demo/app/main.rbが立方体を動かすコードで(以下抜粋)、これがJavaScriptにコンパイルされてブラウザ上で動いているわけですね。
aspect_ratio = $$.innerWidth / $$.innerHeight
scene = THREE::Scene.new
camera = THREE::PerspectiveCamera.new(field_of_view: 75, aspect_ratio: aspect_ratio, near: 0.1, far: 1000)
renderer = THREE::WebGLRenderer.new
renderer.set_size($$.innerWidth, $$.innerHeight)
$document.body << renderer.dom_element.to_n
geometry = THREE::BoxGeometry.new(width: 1, height: 1, depth: 1)
material = THREE::MeshBasicMaterial.new( color: 0x00ff00 )
cube = THREE::Mesh.new(geometry.to_n, material.to_n)
scene.add(cube.to_n)
camera.position.z = 5
render = proc do
$$.requestAnimationFrame(render)
cube.rotation.x += 0.1
cube.rotation.y += 0.1
renderer.render(scene.to_n, camera.to_n)
end
render.call
Opalは下回りがJavaScriptである都合上、IntegerとFloatの区別がないとか、StringがmutableでないなどRubyと細かい違いはありますが、使用感は完全にRubyです。
バージョンについて
1年前のプロダクトということで、どのバージョンを使っているのか気になりますね。three.rb.gemspecを見ると、Opalは0.7.xのようです(最新は0.10.x)。three.jsの方は、demo/index.htmlを見るとCDN上のr73を読み込んでいます(最新はr82)。
ということで、three.jsのバージョンを上げるのは簡単そうです。demo/index.htmlを書き換え、r82を読み込むようにしてみます。特に問題なく動きました。
Opalのバージョンを上げる
問題はOpalの方で、gemspecの指定を0.10.0に変えてdemo側でbundle updateすると、立方体が出なくなってしまいました。うーむ、どうしましょうかね。
とりあえず最新の情報を得たいのでhttp://opalrb.org/を見ます。そうするとRack tutorialという項があり、どうやらこれが最新の書き方のようです。[^1]
これにそってdemo/config.ruを書き直してみます。
require 'bundler'
Bundler.require
run Opal::Server.new { |server|
server.main = 'main.rb'
server.append_path 'app'
}
…いや、だめですね。これだとdemo/index.htmlが読み込まれないのでThree.jsがロードされません。デベロッパーコンソールにも「THREE is not defined」と出ています。
index.htmlをサーブしたい
チュートリアルの最後にLeran moreというリンクがあるので、opal-sprocketsのREADMEを見ます。これを見るとindex.htmlは必須と書いてありますが、さっきのチュートリアル手順では無くても動いたので、なんか情報が古そうな気がします。うーん、ソースを読まないとだめそうですね。
リロードしたページのソースを表示すると<title>Opal Server</title>
という行があるので、opal-sprocketsおよびopal gemの中身をgrepします。[^2] opal gemに該当の行がありました。自前のindex.htmlを置けないという仕様は考えにくいので、index.htmlが見つからなければデフォルトのものをサーブするのだと推測します。[^3] このへんを読むと雰囲気的にindex_pathという設定項目な気がするので以下のようにします。
require 'bundler'
Bundler.require
run Opal::Server.new { |server|
server.main = 'main.rb'
server.append_path 'app'
server.index_path = 'index.html'
}
Three.jsがちゃんとロードされるようになりました。
あと一歩
しかしまだ立方体は出ません。ロードされているリソースを見るとmain.rb自体はちゃんとコンパイルされていそうですが、mainを実行する部分が呼ばれていない雰囲気です。チュートリアルの方を別ポートで動かして(bundle exec rackup -p 8888
)見比べてみると、arrayとかrangeとかその他もろもろを読み込むscriptタグが生成されていることが分かります。どうやらserver.rbのこの行がポイントのようです。
<body>
#{javascript_include_tag @server.main}
</body>
lib/opal/sprockets/erb.rbというファイルがあるので、なんとなくerbが使えそうな予感がします。index.htmlをindex.html.erbにリネームし、main.jsを直接ロードしていた部分を以下に置き換えます。
<%= javascript_include_tag @server.main %>
config.ruのindex_pathも合わせてindex.html.erbにし、rackサーバを再起動すると…
やったね!!
まとめ
ということで動くようにしたバージョンを https://github.com/yhara/three.rb/tree/opal_0_10 に置いておきました。なんというかThree.rb自体の解説が全くできなかったですが今回はここまでです。
Opalはドキュメントの更新に手が回っていないことがありますが、処理系自体の完成度はかなり高いので、直せば動くというか、やりたいことができる可能性はそれなりに高いです。何か動かないものがあればTwitterの@yharaにmentionしてもらえれば調査を手伝えるかもしれません。
[^1]: と言いたいところですがtypoがあって動かなかったのでプルリクしました
[^2]: こういう時のために ln -s ~/.rbenv/versions/x.x.x/lib/ruby/gems/x.x.x/gems ~/gems
としておくとgemの中身がすぐ読めて便利です
[^3]: こういうマジカルな仕様にすると今回みたいに挙動を追うのが大変なので、面倒でもindex.htmlは必須にしたほうが分かりやすくて良いと思う