はじめに
この記事はElixirアドベントカレンダー2025シリーズ2の9日目の記事です。
本記事はElixirDesktopで作ったスマホアプリ上でにBabylon.js組み込んで3DCGをPhoenix LiveViewで操作する方法を解説します
ElixirDesktopとは?
iOS/Androidのアプリ内でPhoenixを起動してWebアプリケーションをネイティブアプリとして開発できます
こちらをどうぞ
Babylon.jsとは?
WebGLを簡単に扱えるようにしたライブラリで、主に3Dゲームエンジン用途で多く使われています
役に立ちそうな資料
なんでLiveView上でBabylon.js?
- JS/TSは書きたくないが3DCGをElixirでゴリゴリやりたいから!
- Webアプリの枠組みで作ればゲームも作りやすいかもと妄想したから
アプリ作成
とりあえず作っていきましょう
使うかわからんけどDB付きで作ります
mix phx.new babylon --database sqlite3
ライブラリを追加
defp deps do
[
...
- {:bandit, "~> 1.5"}
+ {:bandit, "~> 1.5"},
+ {:desktop_setup, github: "thehaigo/desktop_setup", only: :dev}
]
end
以下のコマンドを実行します
mix deps.get
mix desktop.install
mix desktop.setup.ios
mix desktop.setup.android
サンプルの実行
これのサンプルを実行します
まずbabylon.jsをcdn経由で読み込みます、app.jsの上にdefer付きで追加してbabylonを読み込んでからhooks等を読み込むようにします
<!DOCTYPE html>
<html lang="en">
<head>
...
+ <script defer src="https://preview.babylonjs.com/babylon.js">
+ </script>
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
</script>
...
</head>
...
</html>
次にbabylon_webの下にliveフォルダを作成してliveviewを以下のように作ります
tailwindが使えるのでclass="w-screen h-screen"にしてcanvasを画面全体にします
Canvas内を書き換えるためLiveViewの更新で初期化されないようにphx-update="ignore"を指定します
LiveView1.1の新機能ColocatedHookを使用し同一ファイルJS Hookを定義して.Renderで参照できるようにします
mountedでページ読み込みが完了してLiveViewのmountが完了した際に実行されます
その中にサンプルコードをまるっと貼り付けします
defmodule BabylonWeb.HomeLive do
use BabylonWeb, :live_view
def render(assigns) do
~H"""
<canvas class="w-screen h-screen" id="renderCanvas" phx-hook=".Render" phx-update="ignore">
</canvas>
<script :type={Phoenix.LiveView.ColocatedHook} name=".Render">
export default {
mounted() {
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const createScene = () => {
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera(
"Camera",
Math.PI / 2,
Math.PI / 2, 2,
BABYLON.Vector3.Zero(),
scene
);
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight(
"light",
new BABYLON.Vector3(1, 1, 0),
scene
);
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {});
return scene;
}
const scene = createScene();
engine.runRenderLoop(() => { scene.render();});
window.addEventListener("resize", () => { engine.resize(); });
}
}
</script>
"""
end
def mount(_params, _session, socket) do
{:ok, socket}
end
end
作成したらトップページを切り替えます
scope "/", BabylonWeb do
pipe_through :browser
- get "/", PageController, :home
+ live "/", HomeLive
end
起動
ルーティングも完了したら以下のコマンドでアプリを起動します
iex -S mix
問題なく起動できました!
iOS,Androidも無事起動できました
LiveViewから操作
これだけだとLiveViewに乗せる意味がないので、LiveViewからボタンをクリックするとスフィアを出すようにします
htmlのボタンをcanvasの上に設置して
ボタンをクリックするとスフィアを横にどんどん出していくようにします
defmodule BabylonWeb.HomeLive do
use BabylonWeb, :live_view
def render(assigns) do
~H"""
<canvas class="w-screen h-screen" id="renderCanvas" phx-hook=".Render" phx-update="ignore">
</canvas>
+ <.button phx-click="spawn" class="btn z-2 fixed bottom-4 left-4">Spawn</.button>
<script :type={Phoenix.LiveView.ColocatedHook} name=".Render">
export default {
mounted() {
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const createScene = () => {
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera(
"Camera",
Math.PI / 2,
Math.PI / 2, 2,
BABYLON.Vector3.Zero(),
scene
);
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight("light",
new BABYLON.Vector3(1, 1, 0), scene);
- const shpere = BABYLON.MeshBuilder.CreateSphere("sphere", {});
return scene;
}
const scene = createScene();
engine.runRenderLoop(() => { scene.render();});
window.addEventListener("resize", () => { engine.resize(); });
+ this.handleEvent("spawn", ({count}) => {
+ BABYLON.MeshBuilder.CreateSphere(`sphere${count}`, {}).position.set(count - 1,0,0);
+ })
}
}
</script>
"""
end
def mount(_params, _session, socket) do
- {:ok, socket}
+ {:ok, assign(socket, :count, 0)}
end
+ def handle_event("spawn", _params, socket) do
+ count = socket.assigns.count + 1
+
+ socket
+ |> push_event("spawn", %{count: count})
+ |> assign(:count, count)
+ |> then(&{:noreply, &1})
+ end
end
これを実行してみます
ちゃんと動いてますね
最後に
3D部分のみをBabylon、UIやイベントハンドリングをLiveViewとして書くことで規模が大きくなってもシンプルに書けるかなと思います
JSではめんどくさい定期実行とsocket通信による複数ユーザーのリアルタイム更新も簡単にできたりするので、色々できそうで楽しみです
本記事は以上になりますありがとうございました


