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?

1 Like

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

1 Like

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

1 Like

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.

2 Likes

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.

3 Likes

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.

3 Likes

@brianzinn Sorry for the even later response… hahaha
In fact, I only let all that code as fallback.

I’ve update to the following version

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

To the Babylon itself I didn’t find it necessary. But to my project it was, because I wanted to get the scene from a callback. I also found that executeWhenReady was running early than any other observer (most of the times isReady would run earlier). I may check it again if it has any sort of effect or just a “placebo effect”.

You are right about useEffect and it has been removed on newer versions.

Thanks for sharing about the ResizeObserver. This is something that we still have sort of lag while using.

2 Likes

@brianzinn I have checked about your question. On all my tests, scene.isReady() have returned true. So, the else is unnecessary. However, I kept that as fallback, I have no courage to remove and see an eventual error… :sweat_smile:

About scene.executeWhenReady and scene.onReadyObservable.addOnce. The method executeWhenReady runs first than the observable.