Babylon.jsとreact-babylonjsで3Dモデルを描画してみた

こんにちは、フロントエンドエンジニアの堀江(@nandemo_3_)です。

2023年4月21日にBabylon.jsのバージョン6がリリースされ、界隈では話題になっておりますが、メタバース業界にいるフロントエンドエンジニアとしては、キャッチアップしておきたいと思い、Babylon.jsを触ってみました。

また、Babylon.jsをReactアプリケーション上で使えるreact-babylonjsも合わせて紹介します。

www.youtube.com

はじめに

今回のアジェンダは以下の3つとなります。

  • Babylon.jsとは
  • Babylon.js Playgroundでチュッパチャップス型の風車の作り方
  • 上記で作成したモデルをreact-babylonjsで描画するデモ

Babylon.jsとは

Babylon.jsは、WebGLを使用して3Dゲームやアプリケーションを開発するためのJavaScriptフレームワークです。

Microsoftによって開発されたOSSで、高速でスムーズなグラフィックス、強力なアニメーション、物理エンジン、音声エンジン、カメラコントロール、そして多くの他の機能を提供しています。また、Babylon.jsは、3Dシーンを容易に操作できるようにする機能が豊富で、WebGLや3Dグラフィックスの知識がなくても、簡単に使うことができます。

WebGLのJavascriptライブラリといえば、Three.jsがあげられますが、2023年4月26日時点のGItHubのスター数を見ても圧倒的に、Three.jsの方が多いですね。

Babylon.jsの強み

Three.jsとBabylon.jsはどちらも、Web上で3DCGを作成するJavaScriptフレームワークですが、それぞれの特徴と、Babylon.jsの強みは何なのでしょうか。

Three.jsは、シンプルなAPIを提供しており、簡単な3Dアプリケーションを素早く開発する場合に、最適な選択肢とされています。

一方、Babylon.jsは物理エンジンとアニメーションエンジンが組み込まれており、3Dゲームといったアプリケーション開発に向いています。また、VRやARをサポートしており、Meta QuestやMicrosoftのHololensとの親和性が高いそうです。

つまり、Babylon.jsは複雑で高度な3Dアプリケーションを開発する場合に、最適な選択肢とされています。

そして、Babylon.jsは公式でPlaygroundが用意されているのも、開発者にとっては非常に嬉しいです。上記でも述べましたが、WebGLや3Dグラフィックスの知識が全くなくても、ちょっとした3DCGなら簡単に作成できるのは良いと思います。

playground.babylonjs.com

チュッパチャップス型の風車を作る

では、Babylon.jsが提供するPlaygroundを使って、どんなことができるのか試してみます。

Babylon.js Playgroundにアクセスすると、すでに地面と球体が作成されています。

四半球を作る

まず、風車の羽となる四半球を作ります。

BABYLON.MeshBuilder.CreateSphereAPIを使うことで、円形のShapeを作ることができます。

四半球にするために、arc0.25を指定しました。sideOrientationBABYLON.Mesh.DOUBLESIDEにしないと、裏面が透明になるので注意が必要です。

var createScene = function() {
    var scene = new BABYLON.Scene(engine);

    //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
    var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);

    // This targets the camera to scene origin
    camera.setTarget(BABYLON.Vector3.Zero());

    // This attaches the camera to the canvas
    camera.attachControl(canvas, true);
    var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);

    const sphere1 = BABYLON.MeshBuilder.CreateSphere("sphere", {arc: 0.25, sideOrientation: BABYLON.Mesh.DOUBLESIDE});
    sphere1.position.y = 2.5;
    sphere1.rotation.y = -Math.PI / 2;

    return scene;
};

シェイプを複製する

次に、作ったシェイプを複製します。cloneメソッドを使うことで複製ができます。固有のプロパティは上書きすることで変更することができます。

var createScene = function() {
    var scene = new BABYLON.Scene(engine);

    //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
    var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);

    // This targets the camera to scene origin
    camera.setTarget(BABYLON.Vector3.Zero());

    // This attaches the camera to the canvas
    camera.attachControl(canvas, true);
    var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);

    const sphere1 = BABYLON.MeshBuilder.CreateSphere("sphere", {arc: 0.25, sideOrientation: BABYLON.Mesh.DOUBLESIDE});
    sphere1.position.y = 2.5;
    sphere1.rotation.y = -Math.PI / 2;

    sphere2 = sphere1.clone("sphere2");
    sphere2.rotation.y = Math.PI / 2;

    return scene;
};

柱を作る

次に、柱を作ります。今回はBABYLON.MeshBuilder.CreateCylinderAPIを使い、円柱を作りました。

より風車の柱っぽくするために上面と底面のを変えています。

var createScene = function() {
    var scene = new BABYLON.Scene(engine);

    //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
    var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);

    // This targets the camera to scene origin
    camera.setTarget(BABYLON.Vector3.Zero());

    // This attaches the camera to the canvas
    camera.attachControl(canvas, true);
    var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);

    const sphere1 = BABYLON.MeshBuilder.CreateSphere("sphere", {arc: 0.25, sideOrientation: BABYLON.Mesh.DOUBLESIDE});
    sphere1.position.y = 2.5;
    sphere1.rotation.y = -Math.PI / 2;

    sphere2 = sphere1.clone("sphere2");
    sphere2.rotation.y = Math.PI / 2;

    const cone = BABYLON.MeshBuilder.CreateCylinder("cone", {height: 4, diameterTop: 0.1, diameterBottom: 0.3}, scene);

    return scene;
};

アニメーションをつける

最後に、アニメーションをつけます。アニメーションが分かりやすいように半球に色も着けます。

Babylon.jsあるあるかもしれませんが、複数のシェイプに対してアニメーションをつける場合はTransformNodeを作成し、これを親に設定します。

そして、そのTransformNodeにアニメーションを適用すると正しく動きます。

自分は、メッシュごとに同じアニメーションを適用したため、おかしな動きになりました。

これで、チュッパチャップス型の風車が完成しました。(もっと風車っぽい形にしたかった。。)

Playgroundはこちらです。

var createScene = function() {
    var scene = new BABYLON.Scene(engine);

    //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
    var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);

    // This targets the camera to scene origin
    camera.setTarget(BABYLON.Vector3.Zero());

    // This attaches the camera to the canvas
    camera.attachControl(canvas, true);
    var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);

    const root = new BABYLON.TransformNode();

    const material1 = new BABYLON.StandardMaterial("material", scene);
    material1.diffuseColor = new BABYLON.Color3(1, 0, 0);
 
    const material2 = new BABYLON.StandardMaterial("material", scene);
    material2.diffuseColor = new BABYLON.Color3(1, 1, 0);

    const sphere1 = BABYLON.MeshBuilder.CreateSphere("sphere", {arc: 0.25, sideOrientation: BABYLON.Mesh.DOUBLESIDE});
    sphere1.position.y = 2.5;
    sphere1.rotation.y = -Math.PI / 2;
    sphere1.parent = root
    sphere1.material = material1

    sphere2 = sphere1.clone("sphere2");
    sphere2.rotation.y = Math.PI / 2;
    sphere2.material = material2

    const cone = BABYLON.MeshBuilder.CreateCylinder("cone", {height: 4, diameterTop: 0.1, diameterBottom: 0.3}, scene);

    const animSphere = new BABYLON.Animation(
        "sphereAnimation", 
        "rotation.y", 
        30, 
        BABYLON.Animation.ANIMATIONTYPE_FLOAT, 
        BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
    );

    const sphereKeys = []; 

    // キーフレーム (アニメーションキー) が 0 のとき、
    // rotation.y に 0 を設定
    sphereKeys.push({
        frame: 0,
        value: 0
    });

    // キーフレームが 30 (30 FPS なので 1 秒後のこと) のとき、
    // rotation.y に 2PI (360度) を設定し一回転を表す
    sphereKeys.push({
        frame: 30,
        value: 2 * Math.PI
    });


    animSphere.setKeys(sphereKeys);

    root.animations = [];
    root.animations.push(animSphere);

    scene.beginAnimation(root, 0, 30, true);
    return scene;
};

所感

ちょっとアニメーションの所に躓きましたが、思ったより簡単にチュッパチャップス風の風車を作ることができました。

UnityやUnreal、Blenderなどの経験がなくても、Javascript、Typescriptの知識があれば簡単にモデリングができるのは非常に良いですし、思いついたものをパッと具現化できるので、アウトプットが捗りそうだなと思いました。

3Dゲーム向けということもあり、シューティングゲームを作った猛者もいる様なので、Flashゲームみたいなものを作りたいと思いました。

react-babylonjsでチュッパチャップス風の風車を描画する

続きまして、先ほど作ったモデルをWeb上で描画したいと思います。

今回使ったライブラリはreact-babylonjsです。

github.com

three.jsを使う場合は、react-three-fiberが良いそうですが、今回はBabylon.jsを採用しているためreact-babylonjsを使いました。

ライブラリインストール

必要なライブラリはこちらです。(Reactアプリのセットアップは省略します)

yarn add @babylonjs/core
yarn add @@babylonjs/gui
yarn add react-babylonjs

シーンを作る

まず、シーンを作成します。Engineコンポーネントでラップするのがお決まりのようです。

シーンに、カメラや光源、地面、シェイプなどのコンポーネントを内包することで、3D空間が描画されます。

カメラや光源だけでもかなり沼だったので、今回は最も簡単な設定にしました。

import React, { FC } from 'react';

import "@babylonjs/core/Physics/physicsEngineComponent"  // side-effect adds scene.enablePhysics function
import "@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent"; // side-effect for shadow generator
import { Vector3 } from '@babylonjs/core/Maths/math.vector';

import { Scene, Engine } from 'react-babylonjs';
import ChupaShupsWindmill from './ChupaShupsWindmill';

const App: FC = () => {
  return (
    <div>
      <header>
        <Engine antialias={true} adaptToDeviceRatio={true} canvasId="canvas">
          <Scene>
            <freeCamera name="camera" position={new Vector3(0, 5, -10)} target={Vector3.Zero()} />
            <hemisphericLight name="light" intensity={0.7} direction={Vector3.Up()} />
            <ChupaShupsWindmill />
          </Scene>
        </Engine>
      </header>
    </div>
  );
}
export default App;

チュッパチャップス風の風車を作る

上記のBabylon.js Playgroundで作ったモデルを、react-babylonjsで再作成します。

Babylon.js Playgroundのコーディングをそのまま移行できると思っていましたが、JSXに変換する必要があります。

球体を作る場合はsphere、円柱はcylinderといった様にコンポーネントとなっています。プロパティはそのまま指定できるので、これは分かりやすかったです。

また、Reactでアニメーションを実現する上でHooksを使う必要があります。

import React, {FC, useEffect, useRef} from "react";
import { Animation, Vector3 } from '@babylonjs/core'
import { Color3 } from '@babylonjs/core/Maths/math.color';
import { useScene } from 'react-babylonjs'

type ArcSphereProps = {
    name: string;
    rotation: Vector3;
    diffuseColor?: Color3
}

function getSpinAnimation() {
    const keys = [
      {
        frame: 0,
        value: 0,
      },
      {
        frame: 30,
        value: 2 * Math.PI,
      },
    ]
  
    const animation = new Animation('animation', 'rotation.y', 30, 0, 1)
    animation.setKeys(keys)
  
    return [animation]
  }

const ChupaShupsWindmill: FC = () => {
    const groupRef = useRef(null)
    const scene = useScene()
    const position = Vector3.Zero()

    useEffect(() => {
        const playAnimation = () => {
            if (groupRef.current) {
                const group = groupRef.current
                const animations = getSpinAnimation()
                scene!.beginDirectAnimation(
                group,
                animations,
                0,
                30,
                true
                )
            }
        }
        playAnimation()
      }, [groupRef, scene])

    return (
        <>
            <transformNode
                name="group"
                ref={groupRef}
                position={position}
            >
                <ArcSphere
                    name="windmill1"
                    rotation={new Vector3(undefined, -Math.PI / 2, undefined)}
                    diffuseColor={Color3.Red()}
                />
                <ArcSphere
                    name="windmill2"
                    rotation={new Vector3(undefined, Math.PI / 2, undefined)}
                    diffuseColor={Color3.Yellow()}
                />
            </transformNode>
            <cylinder
                name="cylinder"
                height={4}
                diameterTop={0.1}
                diameterBottom={0.3}
            >
                <standardMaterial name="cylinder-material" specularPower={16}
                    diffuseColor={Color3.White()}
                />
            </cylinder>
        </>
    )

}

const ArcSphere = ({name, rotation, diffuseColor}: ArcSphereProps) => {
    return (
        <sphere
            name={name}
            arc={0.25}
            sideOrientation={2}
            position={new Vector3(undefined, 2.5, undefined)}
            rotation={rotation}
            >
            <standardMaterial name={`${name}-material`} specularPower={16}
                diffuseColor={diffuseColor}
            />
        </sphere>
    )
}

export default ChupaShupsWindmill;

実行するとこんな感じになります。

ソースコード

ソースコード全文はこちらのリポジトリをご参照ください。

github.com

所感

react-babylonjsのドキュメントが充実しておらず探り探りではあったものの、Playgroundと同じモデルを作ることができました。

Playgroundのような気軽さはないですが、ReactでBabylog.jsを使って3Dモデルを描画できれば、3Dゲームも作れるかもしれませんし、3Dベースのホームページや3Dアバターの表示なども行えるのかなと思いました。

ただ、アニメーションを多用した時のパフォーマンスはどうなるのか検証は必要かと思いました。

最後に

今回は、Babylon.jsのPlaygroundやreact-babylonjsを用いて3Dモデルの作成をしました。

v6の機能のキャッチアップと、元々やりたかったVRMの表示ができなかったので次回以降やりたいと思います。

最後まで読んでくださりありがとうございました。

参考サイト

github.com

doc.babylonjs.com