수정삭제날짜 : 2023.11.9 16:51

[babylon.js] 바빌론 리액트에서 사용하기 + vite로 리액트 시작하기 1

포스트 썸네일 사진

리액트에서 바빌론을 실행하는 예제는 ms에 없어서 babylon.js 공식문서에서 찾아보았다.

바빌론 공식문서에 있는 예제를 한번 따라 만들어 보자.

https://doc.babylonjs.com/communityExtensions/Babylon.js+ExternalLibraries/BabylonJS_and_ReactJS


Vite를 이용하여 리액트 시작하기

맨 처음 프로젝트를 만들 때 이전까지는 webpack에서 구성하거나 CRA를 사용하였는데,

이번에는 vite로 한 번 구현해 보았다. 웹팩에서 직접 만드는 것과는 다르게 vite는 매우 간단하게 구성할 수 있었다.


프로젝트를 넣을 폴더를 만든 뒤 해당 폴더에서

npm create vite@latest my-vite

를 입력하면 몇가지 옵션을 선택할 수 있게 한다.

여기서 나는 리액트에서 바빌론을 사용하고, Typescript를 이용할 것이기 때문에 아래 사진과 같이 설정했다.

바빌론 옵션 설정 사진

여기서 SWC는 Rust로 작성된 JavaScript 컴파일러로 바벨보다 더 좋은 성능으로 코드를 트랜스파일 할 수 있다고 한다.

이후 필요한 모듈들을 다운로드 한다.

npm install

만들어진 프로젝트의 소스폴더에서

App.tsx, main.tsx를 제외한 css파일은 삭제하였고, 내용도 비워서 빈 페이지로 시작할 것이다.

깔끔하게 비워진 이미지


바빌론 엔진이 적용될 캔버스 생성

바빌론은 3D 렌더링 엔진으로 canvas html요소에 엔진을 적용시킬 수 있다.

src아래에 components 폴더를 생성하고 SceneComponent.tsx 컴포넌트를 생성하자


리턴되는 태그를 canvas로 변경하고, 해당 canvas html엘리먼트에 바빌론 엔진을 적용하기 위해 useRef로 해당 요소를 가져온다.

ref를 가져올 때 HTMLCanvasElement를 제네릭으로 적어주지 않으면 MutableRefObject가 되기 때문에 반드시 적어준다.

import React, {useRef} from "react";

function SceneComponent() {
    const reactCanvas = useRef<HTMLCanvasElement>(null);

    return <canvas ref={reactCanvas} />;
}

export default SceneComponent;

이후 useEffect를 이용하여 바빌론엔진을 해당 컴포넌트가 렌더링 될 때 적용시켜 보자.

이전에 작성한 자습서 보기와 같이 보면서 제작하니 쉽게 이해할 수 있었다.


캔버스에 바빌론 엔진 적용하고 장면(Scene) 만들기

첫 번째로 사용할 바빌론 엔진을 가져오고, 같이 사용할 장면을 만드는 Scene도 함께 가져온다.

가져온 바빌론 엔진에 canvas ref의 current를 넣어주면 요소에 바빌론 엔진을 적용할 수 있다.


이후 메시들이 떠다닐 공간인 장면을 생성한다.

import React, {useRef, useEffect} from "react";
import {Engine, Scene} from "@babylonjs/core";

function SceneComponent() {
    const reactCanvas = useRef<HTMLCanvasElement>(null);

    useEffect(()=>{
        const {current : canvas} = reactCanvas;
        const engine = new Engine(canvas);
        const scene = new Scene(engine);

    }, [])

    return <canvas ref={reactCanvas} />;
}

export default SceneComponent;


캔버스 화면 그리기

화면을 표시하려면 프레임마다 계속 화면을 갱신해 주는 코드를 넣어야 한다.

engine의 runRenderLoop 메서드를 이용하여 화면을 매 프레임 그릴 수 있다.

그리고 컴포넌트가 사라질 때 엔진을 제거하도록 클린업 함수를 추가하였다.

import React, {useRef, useEffect} from "react";
import {Engine, Scene} from "@babylonjs/core";

function SceneComponent() {
    const reactCanvas = useRef<HTMLCanvasElement>(null);

    useEffect(()=>{
        const {current : canvas} = reactCanvas;
        const engine = new Engine(canvas);
        const scene = new Scene(engine);

        engine.runRenderLoop(() => {
            if (typeof onRender === "function") onRender(scene);
            scene.render();
        });
        
        //화면을 리사이즈 하는 함수도 추가하였다. 이후 클린업 할 때 같이 사라진다.
        const resize = () => {
            scene.getEngine().resize();
        };

        if (window) {
            window.addEventListener("resize", resize);
        }

        return () => {
            scene.getEngine().dispose();
            if (window) {
                window.removeEventListener("resize", resize)
            }
        };
    }, [])

    return <canvas ref={reactCanvas} />;
}

export default SceneComponent;


렌더 이후 동작할 함수 실행부 만들기

useEffect에 실행부 코드를 추가하였다. 실행하는 함수는 프롭스로 받아올 onScreanReady이다. 이 함수는 이후 설명할 예정이다.

메시를 추가하고 화면이 그려졌을 때 캔버스 내부에서 행해질 모든 동작을 여기서 실행시킨다.

onRender또한 props에서 받아온다.

props의 type들은 babylon-hook에서 선언되어 가져와 사용한다.

import React, {useRef, useEffect} from "react";
import {Engine, Scene} from "@babylonjs/core";
import { BabylonjsProps } from "babylonjs-hook"; //프롭스의 타입
function SceneComponent({ onSceneReady, onRender } : BabylonjsProps) {
    const reactCanvas = useRef<HTMLCanvasElement>(null);

    useEffect(()=>{
        const {current : canvas} = reactCanvas;
        const engine = new Engine(canvas);
        const scene = new Scene(engine);

        //화면이 준비된 후 화면에 들어갈 모든 메시와 동작들을 실행시키는 부분
        //함수의 구현의 props에서 받아오며 아래에서 구현한다.
        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();
        };

        if (window) {
            window.addEventListener("resize", resize);
        }

        return () => {
            scene.getEngine().dispose();
            if (window) {
                window.removeEventListener("resize", resize)
            }
        };
    }, [])

    return <canvas ref={reactCanvas} />;
}

export default SceneComponent;


작동부 함수 제작

App.tsx에서 생성한 SceneComponent.tsx를 가져와서 집어넣고, props로 줄 onSceneReady 함수와 onRender 함수를 만들자.

BabylonjsProps에서 정의된 두 함수는 매개변수로 scene을 받는다. BabylonjsProps를 클릭하고 f12를 눌러서 확인할 수 있다.

export declare type BabylonjsProps = {
    antialias?: boolean;
    engineOptions?: EngineOptions;
    adaptToDeviceRatio?: boolean;
    renderChildrenWhenReady?: boolean;
    sceneOptions?: SceneOptions;
    onSceneReady: (scene: Scene) => void;
    /**
     * Automatically trigger engine resize when the canvas resizes (default: true)
     */
    observeCanvasResize?: boolean;
    onRender?: (scene: Scene) => void;
    children?: React.ReactNode;
};


onScreenReady 함수는 캔버스가 준비되었을 때 맨 처음 실행되는 함수이다.

해당 부분에서 카메라와 광원을 세팅하고 박스와 바닥을 생성한다.


onRender 함수는 렌더 될 때 마다 실행되는 함수이며, 박스가 회전하게 끔 구현하였다.

자세한 내용은 주석에 작성했다.

import {
  HemisphericLight,
  ArcRotateCamera,
  MeshBuilder,
  Vector3,
  Scene,
  Mesh,} from "@babylonjs/core";
import SceneComponent from "./components/SceneComponent";

export default function App() {
  let box: Mesh;

  //화면이 준비되었을 경우 맨 처음 실행되는 함수
  const onSceneReady = (scene: Scene) => {
    // ArcRotateCamera로 대상을 중심으로 회전하는 카메라를 생성
    // 세부 속성은 자습서에 작성한 바와 동일하다.
    const camera = new ArcRotateCamera(
      "camera1",
      (3 * Math.PI) / 2,
      Math.PI / 50,
      550 * 0.015,
      Vector3.Zero(),
      scene 
   );
    
    const canvas = scene.getEngine().getRenderingCanvas();
    camera.attachControl(canvas, true);
    //반구형 광원을 세팅
    const light = new HemisphericLight("light", new Vector3(0, 1, 0), scene);
    light.intensity = 0.7;
    //박스를 2의 크기로 생성하고 위치를 1로 만들었다.
    //박스의 정 가운데 부분이 원점이므로 바닥위에 붙어있게 하기 위함.
    box = MeshBuilder.CreateBox("box", { size: 2 }, scene);
    box.position.y = 1;
    //바닥을 생성
    MeshBuilder.CreateGround("ground", { width: 6, height: 6 }, scene);  };

  //이후 계속 렌더링 될 때 마다 실행될 함수. 박스를 계속 회전 시킴
  const onRender = (scene: Scene) => {
    if (box !== undefined) {
      const deltaTimeInMillis = scene.getEngine().getDeltaTime();
      const rpm = 10;
      //박스를 y축을 기준으로 계산 값만큼 계속 회전시킨다.
      box.rotation.y += (rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000);
    }
  };

  return (
    <div>
      <SceneComponent  onSceneReady={onSceneReady} onRender={onRender} />
    </div>
  );
}


구현 이미지


이것으로 리액트에서 바빌론엔진을 이용해서 3D렌더를 구현해 보았다.

blender에서 만든 오브젝트를 가져올 수도 있다고 하니, 만들어진 오브젝트를 붙여 넣어 화려하게 꾸밀 수 도 있을 것 같다.

다음 프로젝트를 만들 때 한 번 활용해 봐야 겠다.

댓글 펼치기 0