Any way to learn how to integrate React with Babylon?

Hey,

I tried searching through the documentation for the react-babylon repo and found it a bit confusing for me. Does anyone have any tutorials for the project or an efficient way to implement it?

@brianzinn is the daddy of the project and is always happy to help newcomers :slight_smile:

Babylon Basic - Animations ⋅ Storybook (brianzinn.github.io) has some pretty nice examples!

I found react-babylon an extremely good project, but I came from a different scenario and going to react-babylon would be extremely cumbersome to me.

I can give you a different scenario, because we already had a project running BabylonJS without react, and we migrated to react. Also, we already had a group of stateful classes that were responsible to handle aspects of the 3D environment.

Following my constraints, I’ve created a single component to render the canvas, and to work with the react reconciliation. Then I handle everything by the existent classes.

Below is the component.

import { Engine } from '@babylonjs/core/Engines/engine';
import { EngineOptions } from '@babylonjs/core/Engines/thinEngine';
import '@babylonjs/core/Helpers/sceneHelpers';
import '@babylonjs/core/Layers/effectLayerSceneComponent';
import { Vector3 } from '@babylonjs/core/Maths/math';
import { Database } from '@babylonjs/core/Offline';
import '@babylonjs/core/Rendering/boundingBoxRenderer';
import { Scene, SceneOptions } from '@babylonjs/core/scene';
import { CanvasHTMLAttributes, CSSProperties, forwardRef, Ref, RefObject, useEffect, useRef, useState } from 'react';

type OnSceneReadyProp = { scene: Scene };
type OnRenderProp = { scene: Scene };

type SceneComponentProps = CanvasHTMLAttributes<HTMLCanvasElement> & {
    antialias?: boolean,
    engineOptions?: EngineOptions,
    onSceneReady?: (props: OnSceneReadyProp) => void,
    onRender?: (props: OnRenderProp) => void,
    adaptToDeviceRatio?: boolean,
    sceneOptions?: SceneOptions,
    style?: CSSProperties,
};

const SceneComponent = forwardRef((props: Readonly<SceneComponentProps>, ref: Ref<HTMLCanvasElement>) => {
    const _ref = useRef<HTMLCanvasElement>(null);
    const canvasRef = (ref || _ref) as RefObject<HTMLCanvasElement>;

    const {
        antialias = true,
        engineOptions = { preserveDrawingBuffer: true, stencil: true },
        adaptToDeviceRatio = false,
        sceneOptions,
        onRender = () => {},
        onSceneReady = () => {},
        ...rest
    } = props;

    const [ engine, setEngine ] = useState<Engine | undefined>(undefined);
    const [ engineReady, setEngineReady ] = useState(false);
    const [ scene, setScene ] = useState<Scene | undefined>(undefined);
    const [ sceneReady, setSceneReady ] = useState(false);

    useEffect(() => {
        const { antialias, engineOptions, adaptToDeviceRatio, sceneOptions, onRender, onSceneReady } = props;

        if (canvasRef.current) {
            Database.IDBStorageEnabled = true;

            const reference = canvasRef.current;

            const newEngine = new Engine(reference, antialias, engineOptions, adaptToDeviceRatio);
            newEngine.disableManifestCheck = true;
            newEngine.enableOfflineSupport = true;

            const newScene = new Scene(newEngine, sceneOptions);

            newScene.gravity = new Vector3(0, 0, 0);
            newScene.collisionsEnabled = true;

            setScene(newScene);
            setEngine(newEngine);
            setEngineReady(true);

            if (newScene.isReady()) {
                setSceneReady(true);
                if (onSceneReady) {
                    onSceneReady({ scene: newScene });
                }
            } else {
                newScene.onReadyObservable.addOnce((scene) => {
                    if (onSceneReady) {
                        onSceneReady({ scene });
                    }
                });
            }

            newEngine.runRenderLoop(() => {
                if (typeof onRender === 'function') {
                    onRender({ scene: newScene });
                }

                if (newScene.cameras.length > 0) {
                    newScene.render();
                }
            });

            const resize = () => {
                newEngine.resize();
            };

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

            return () => {
                window.removeEventListener('resize', resize);
            };
        }
    }, []);

    return (
        <canvas ref={canvasRef} {...rest} />
    );
});

SceneComponent.displayName = 'SceneComponent';

export { SceneComponent };

Below is just a simplified version on how you have to render the SceneComponent to get the scene to start working.

render() {
    const style = { display: 'block', height: '100%', margin: '0px', width: '100%' };
    const onSceneReady = ({ scene }: { scene: Scene }) => {
        // here is where you have to work start handling your scene
        // it uses directly the scene from Babylon.
        // you can use it here already, save on a state or on react context (or any other data/state management system)
    };

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

Everything else you can get it from BabylonJS documentation to work.

1 Like

hi @bsdacres sorry for the late reply. The documentation is not very good currently (closer to “just plain bad” ™ than good - I do have a completely new site nearly ready that works more like a tutorial, but it’s not ready yet unfortunately.

There are 2 sample projects that share ways to start out:
TypeScript: Create React App Starter Kit (github.com)
JavaScript: Create React App Starter Kit (github.com)

Unfortunately neither of those projects serves as a tutorial, but only a basic getting started guide. Probably something like a blog post that walks through would be useful - the new site will serve as a blog since it uses regular markup whereas the current github pages uses storybook and is not really amenable to tutorial style docs.

1 Like

I found none of that was necessary in practice, so I had removed it from both the hook and react-babylonjs. Did you find that it was necessary in any condition?

Also, your useEffect will only run once since you have an empty dependency, so it is not necessary to destructure your engineOptions twice (and your defaults are lost?).

Resize observer also is better in some situations compared to resize event on window:
babylonjs-hook/babylonjs-hook.tsx

Thanks for sharing - Cool to see people rolling their own components. That’s why I originally created the Babylon doc, because there’s not much to it and it’s fun to customize. Cheers.

1 Like