Babylon.jsを、Freshで動かしたいということを少し前から考えていた。
以前トライした時には、Preactとのつなぎ込みをうまくできずに断念した。
しかし、Fresh(Deno) で Bootstrap 5 をプラグインで使うを書き進めていたら、これを転用できると思い至った。
なのでやってみる。
この実装にあたり、DenoコミュニティとBabylon.jsコミュニティにいくつか質問を投げ込ませていただいた。感謝。
実装物はこちら。
github - Octo8080X/fresh-babylon-physics
最終的には、以下のような画面が完成します。
とりあえず動かしてみたい場合はこちら。
fresh-babylon-physics.deno.dev
参考
実装
Havokを使った物理シミュレーションをブラウザでできることは確認できているので、今回はバックエンド処理で試す。
処理の流れとしては次のようにする。
--- config: theme: base themeVariables: primaryColor: "#ffffff" primaryTextColor: '#888888' lineColor: "#ffffff" secondaryColor: "#ffffff" tertiaryColor: "#ffffff" --- sequenceDiagram participant browser as ブラウザ participant server as サーバー(Deno) browser ->> server: html, js 他取得リクエスト server -->> browser: 要求レスポンス返却 browser ->> server: 物理シミュレーション処理リクエスト server -->> browser: 物理シミュレーション結果返却 browser ->> browser: 結果に基づいてBabylon.jsレンダリング browser ->> browser: 結果に基づいてグラフレンダリング
サーバーサイド 物理シミュレーション処理API
型情報の記述が少々長いですが、以下のような実装です。
1 | import { FreshContext } from "$fresh/server.ts"; |
ボールを落とすX座標とZ座標を引数に、ボールと箱、地面(平面)のサイズなどの情報と、300ステップの座標と回転のログを取得する。
非ブラウザ環境で実行するにあたって、NullEngine
を使うのがポイント。
非ブラウザだからレンダリングするわけでもないので、カメラ要らなそうですが「必要」。
というわけで、適当なカメラを設定する。
new BABYLON.HavokPlugin(false, havok)
の部分、第一引数をfalseにするのはこの実装では必須。
これを設定すると、scene.render()
を呼び出すたびに、一定時間経過したものとして処理してくれる。
ブラウザの場合、engine.runRenderLoopの中で適当な間隔を持ちscene.render()
を含む処理を呼び出す。
この場合、実際の経過時間を踏まえた物理演算になるがAPIの動作として不適切。
なぜなら、例えば4秒間の物理シミュレーションをするために4秒要するため。
これを回避するため、new BABYLON.HavokPlugin(false, havok)
とする。
この実装で300ステップ約4秒程度の物理シミュレーションを1秒切って処理できる。
フロント側実装
フロント側の実装は、以下の用意する。
- Canvasをマウントさせる(SSRされる)HTML(jsx)
- APIリクエストと、UIのハンドリングをする islands(js)
- API から帰ってきた物理シミュレーション内容に基づくBabylon.jsを含むプラグイン(js)
以下紹介。
Canvasをマウントさせる(SSRされる)HTML(jsx)
1 | import BabylonSimulationControl from "../islands/BabylonSimulationControl.tsx"; |
babylonMount chartMount の2つdivは、Canvasのマウント先とします。
APIリクエストと、UIのハンドリングをする islands(js)
1 | /// <reference lib="dom" /> |
ポイントになるのは、以下の部分。
APIのレスポンスから、グラフの作成とbabylon.jsのセットアップを行う。
1 | const res = await fetch(`/api/sim?x=${valueX}&z=${valueZ}`, { |
処理の中で、生DOMを触る処理をしているが、もう少しうまい(正しい)やり方もありそう。
今回はこの通りだが、正しい方法は見直したい。
API から帰ってきた物理シミュレーション内容に基づくBabylon.jsを含むプラグイン(js)
プラグインは以下のように実装します。
1 | import { Plugin } from "$fresh/server.ts"; |
babylon.jsを直接扱うコードは以下の通りです。
1 | import * as BABYLON from "npm:@babylonjs/core/index.js"; |
プラグインによって、ブラウザ読み込むjsで、windowにbabylon.jsの処理を呼び出す口を登録しておきます。
登録した処理は、islandsでAPIのレスポンスを引数に呼び出して表示開始します。
今回、サーバー側でボールと箱と地面が1フレーム目から存在している構造になっています。
拡張してよく見られる雨のように新しいオブジェクトが次々振ってくるようなシミュレーションも、原理的には可能です。
Fresh の上で Babylon.jsを動かし、物理シミュレーションをサーバーに任せる構造を作ってみました。
この構成でないとできないこととして、表示する時点で座標の遷移を表示できています。
これはAPIですでに計算済みだからです。
個人的に、物理演算をサーバーさせることを目的とした理由の1つは、多人数参加のゲームのためです。
どうしても、ブラウザでの物理演算は、各クライアントで同じ結果を生まないこともあるので、中央管理したかったのです。
近いうちにこれを踏まえて簡単なゲームを作りたいところです。
今回、作ったものはfresh-babylon-physics.deno.devで公開しています。
Deno Deployでホストしていますが、動くのはなかなか感動でした。
WASMを含むnpmパッケージもDenoで動くようになったと。
今回は300ステップの動作をAPIで返しています。
テストする中で、36000ステップまで返すことができました。
オブジェクト数が今回少なかったのもありますが、かなり長い時間のシミュレーションできることも見込めました。
では。