👶

BabylonjsをNextjs14で使う。

2024/02/28に公開

threejsよりbabylonjs派なんですが、なんせ情報が少ない。

ということで、babylonjs人口を増やすべく、

なるべく多くbabylonjsの情報を増やしていきたいっっっ⭐️

Babylonjsとは?

chatgpt曰く

Babylon.jsは、ウェブブラウザ上で3Dグラフィックスを表示するための強力なJavaScriptフレームワークです。WebGLをベースにしており、リアルタイムの3Dアプリケーション、ゲーム、インタラクティブなビジュアライゼーションを開発するための豊富な機能を提供しています。Babylon.jsを使用することで、デベロッパーは簡単に複雑な3Dシーンを構築し、アニメーション、物理エンジンの統合、ユーザーインタラクションなど、多岐にわたる機能を活用することができます。

Babylon.jsの主な特徴には以下のようなものがあります:

  • パフォーマンス: 高いパフォーマンスを実現するために最適化されています。
  • 使いやすさ: 直感的なAPIと豊富なドキュメント、チュートリアルが提供されており、3D開発の入門からプロフェッショナルまで幅広くサポートします。
  • 機能の豊富さ: マテリアル、ライティング、カメラ、アニメーション、物理エンジン統合など、多彩な3Dグラフィックス機能を提供します。
  • コミュニティ: アクティブなコミュニティによるサポートと、多くのユーザーによる貢献があります。

Babylon.jsは、ウェブベースの3Dアプリケーション開発において、手軽に始められる一方で、高度な3D機能やパフォーマンスを必要とするプロジェクトにも適しています。

とのことです。
これです。

Babylon.js: Powerful, Beautiful, Simple, Open - Web-Based 3D At Its Best

なるべく行数を減らしたい。

なんでbabylonを使っているかというと。

webglを使い出した頃調べていた限りでは、babylonjsの方がthreejsよりも
少ない行数で立方体を置けたから。

Babylon.jsの例

// Babylon.jsを使ってシーンをセットアップする
import { Engine, Scene, ArcRotateCamera, HemisphericLight, MeshBuilder } from 'babylonjs';

// キャンバス要素を取得
const canvas = document.getElementById('renderCanvas') as HTMLCanvasElement;

// Babylon.js エンジンを初期化
const engine = new Engine(canvas, true);

// シーンを作成
const scene = new Scene(engine);

// カメラを作成
const camera = new ArcRotateCamera('camera', Math.PI / 2, Math.PI / 4, 10, new BABYLON.Vector3(0, 0, 0), scene);
camera.attachControl(canvas, true);

// 照明を追加
const light = new HemisphericLight('light', new BABYLON.Vector3(1, 1, 0), scene);

// 立方体を作成
const box = MeshBuilder.CreateBox('box', {size: 2}, scene);

// レンダーループを設定
engine.runRenderLoop(() => {
  // 立方体を回転させる
  box.rotation.x += 0.01;
  box.rotation.y += 0.01;

  // シーンを描画
  scene.render();
});

// ウィンドウのリサイズに対応
window.addEventListener('resize', () => {
  engine.resize();
});

Three.jsの例

// Three.jsを使ってシーンをセットアップする
import * as THREE from 'three';

// シーンを作成
const scene = new THREE.Scene();

// カメラを作成
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

// レンダラーを作成
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 立方体のジオメトリとマテリアルを作成
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

// 立方体メッシュを作成し、シーンに追加
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// アニメーション関数を作成
function animate() {
  requestAnimationFrame(animate);

  // 立方体を回転させる
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;

  // シーンをレンダリング
  renderer.render(scene, camera);
}

// アニメーションを開始
animate();

// ウィンドウのリサイズに対応
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

今見るとそんなに変わらない気もするんですが、

なぜか当時はbabylonがいいなと思ったので、

僕の中でデファクトスタンダードになってます。

.svgのロードなどで部分できにthree使ったりもしてるんですが、、、、

BabylonjsをNextjsで使う。

BabylonjsをNextjsで使う。ということで、
特に特別なことはしておらず、公式のドキュメント通りにやってます。

ちなみにApp routerです。

Babylon.js docs

ただ、最近はtypescriptを使うようにしているので、それだけ変えた。

babylonCanvasコンポーネントを作る

"use client";

import React, { useEffect, useRef } from "react";
import * as BABYLON from "@babylonjs/core";
import styles from "./BabylonCanvas.module.scss";

import * as TestSakamotoApp from "@/Test/Test";

/**
 * BabylonCanvasPropsの型定義。
 *
 * @interface
 */
interface BabylonCanvasProps {
  antialias?: boolean; // アンチエイリアス
  engineOptions?: BABYLON.EngineOptions; // エンジンのオプション
  adaptToDeviceRatio?: boolean; // デバイスの比率に合わせる
  sceneOptions?: BABYLON.SceneOptions; // シーンのオプション
  onRender?: (scene: BABYLON.Scene) => void; // レンダリング時の処理
  onSceneReady?: (scene: BABYLON.Scene) => void; // シーンが準備できた時の処理
  [key: string]: any; // その他のPropsを受け入れる
}

/**
 * Babylon.jsを使用して3DシーンをレンダリングするReactコンポーネント。
 *
 * @param {BabylonCanvasProps} props - コンポーネントに渡されるProps。
 * @returns {JSX.Element} Canvas要素。
 */

const BabylonCanvas = ({
  antialias,
  engineOptions,
  adaptToDeviceRatio,
  sceneOptions,
  onRender,
  onSceneReady,
  ...rest
}: BabylonCanvasProps): JSX.Element => {
  const reactCanvas = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const { current: canvas } = reactCanvas;

    if (!canvas) return;

    const engine = new BABYLON.Engine(
      canvas,
      antialias,
      engineOptions,
      adaptToDeviceRatio
    );
    engine.setHardwareScalingLevel(1 / window.devicePixelRatio);
    const scene = new BABYLON.Scene(engine, sceneOptions);
    if (scene.isReady()) {
      onSceneReady?.(scene);
    } else {
      scene.onReadyObservable.addOnce((scene) => onSceneReady?.(scene));
    }

    engine.runRenderLoop(() => {
      if (typeof onRender === "function") onRender(scene);
      scene.render();
    });

    const resize = () => {
      scene.getEngine().resize();
      TestSakamotoApp.Functions.resizeBabylonCamera(
        scene.activeCamera as BABYLON.Camera
      );
    };

    window.addEventListener("resize", resize);

    return () => {
      scene.getEngine().dispose();
      window.removeEventListener("resize", resize);
    };
  }, [
    antialias,
    engineOptions,
    adaptToDeviceRatio,
    sceneOptions,
    onRender,
    onSceneReady,
  ]);

  return (
    <canvas ref={reactCanvas} {...rest} className={styles.babylonCanvasEl} />
  );
};

export default BabylonCanvas;

page.tsxを作る

page.tsx

"use client";

import React, { use } from "react";
import * as BABYLON from "@babylonjs/core";
import * as TestSakamotoApp from "@/Test/Test";

const Page = () => {

  const onSceneReady = async (scene: BABYLON.Scene) => {
    scene.clearColor = BABYLON.Color4.FromHexString("#000000ff");

    const camera = new BABYLON.ArcRotateCamera(
      "camera",
      Math.PI / 2,
      Math.PI / 2,
      2,
      new BABYLON.Vector3(0, 0, 0),
      scene
    );
    camera.setPosition(new BABYLON.Vector3(0, 0, -10));
    camera.attachControl(scene.getEngine().getRenderingCanvas(), true);
    camera.mode = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
    TestSakamotoApp.Functions.resizeBabylonCamera(camera);

    const canvas = scene.getEngine().getRenderingCanvas();
    const engine = scene.getEngine();

    // This creates a light, aiming 0,1,0 - to the sky (non-mesh)
    const light = new BABYLON.HemisphericLight(
      "light",
      new BABYLON.Vector3(0, 1, 0),
      scene
    );

    // Default intensity is 1. Let's dim the light a small amount
    light.intensity = 0.7;

const createTestBox = (
  scene: BABYLON.Scene,
  props = {
    num: 1,
    size: 0.1,
    distance: 0.1,
    rotateSpeed: 0.2,
    disableLight: false,
    color: new BABYLON.Color3(0.5, 0.5, 0.5),
  } as {
    num: number;
    size: number;
    distance: number;
    rotateSpeed?: number;
    disableLight?: boolean;
    color?: BABYLON.Color3;
  }
) => {
  for (let index = 0; index < props.num; index++) {
    const testBox = BABYLON.MeshBuilder.CreateBox(
      `testbox_${index}_in_${location.pathname}`,
      { size: props.size },
      scene
    );
    testBox.position.x = index * props.distance;
    const mat = new BABYLON.StandardMaterial(
      `test
      boxMat_${index}_in_${location.pathname}`,
      scene
    );
    mat.emissiveColor = props.color || new BABYLON.Color3(0.5, 0.5, 0.5);
    testBox.material = mat;

    mat.disableLighting = props.disableLight || false;

    scene.registerBeforeRender(() => {
      testBox.rotation.x += props.rotateSpeed || 0.2;
      testBox.rotation.y += props.rotateSpeed || 0.2;
    });
  }
};

    createTestBox(scene, {
      num: 1,
      size: 1.5,
      distance: 1,
      rotateSpeed: 0.05,
      disableLight: true,
      color: BABYLON.Color3.FromHexString("#0000ff"),
    });

  };

  const onRender = (scene: BABYLON.Scene) => {
    // console.log("🟢 onRender");
  };

  return (
    <>
     
          <TestSakamotoApp.Components.BabylonCanvas
            antialias
            onSceneReady={onSceneReady}
            // onRender={onRender}
            id="my-canvas"
          />
  
    </>
  );
};

export default Page;

p5jsでいうと?

p5jsでいうと、僕の感覚では、

onSceneReady()は、ほぼsetup()です。

onRender()は、ほぼdraw()です。

例では、onSceneReady()でscene.registerBeforeRender(() => {}書いてますが、、、、、

以上、いったんこんなもんか!!!!

Discussion