2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirDesktop製のスマホアプリでLiveView on Babylon.js

Posted at

はじめに

この記事はElixirアドベントカレンダー2025シリーズ2の9日目の記事です。

本記事はElixirDesktopで作ったスマホアプリ上でにBabylon.js組み込んで3DCGをPhoenix LiveViewで操作する方法を解説します

ElixirDesktopとは?

iOS/Androidのアプリ内でPhoenixを起動してWebアプリケーションをネイティブアプリとして開発できます

こちらをどうぞ

Babylon.jsとは?

WebGLを簡単に扱えるようにしたライブラリで、主に3Dゲームエンジン用途で多く使われています

役に立ちそうな資料

なんでLiveView上でBabylon.js?

  1. JS/TSは書きたくないが3DCGをElixirでゴリゴリやりたいから!
  2. Webアプリの枠組みで作ればゲームも作りやすいかもと妄想したから

アプリ作成

とりあえず作っていきましょう

使うかわからんけどDB付きで作ります

mix phx.new babylon --database sqlite3

ライブラリを追加

mix.exs
  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等を読み込むようにします

lib/babylon_web/components/layouts/root.html.heex
<!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が完了した際に実行されます
その中にサンプルコードをまるっと貼り付けします

lib/babylon_web/live/home_live.ex
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

作成したらトップページを切り替えます

lib/babylon_web/router.ex
  scope "/", BabylonWeb do
    pipe_through :browser

-   get "/", PageController, :home
+   live "/", HomeLive
  end

起動

ルーティングも完了したら以下のコマンドでアプリを起動します

iex -S mix

問題なく起動できました!

スクリーンショット 2025-12-05 23.34.20.png

iOS,Androidも無事起動できました

スクリーンショット 2025-12-03 21.18.22.png

LiveViewから操作

これだけだとLiveViewに乗せる意味がないので、LiveViewからボタンをクリックするとスフィアを出すようにします

htmlのボタンをcanvasの上に設置して
ボタンをクリックするとスフィアを横にどんどん出していくようにします

lib/babylon_web/live/home_live.ex
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

これを実行してみます

eee0873b2859a0ed235fbd2e57050de6.gif

ちゃんと動いてますね

最後に

3D部分のみをBabylon、UIやイベントハンドリングをLiveViewとして書くことで規模が大きくなってもシンプルに書けるかなと思います
JSではめんどくさい定期実行とsocket通信による複数ユーザーのリアルタイム更新も簡単にできたりするので、色々できそうで楽しみです

本記事は以上になりますありがとうございました

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?