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?
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
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.
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.
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.
@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.
@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…
About scene.executeWhenReady
and scene.onReadyObservable.addOnce
. The method executeWhenReady
runs first than the observable.