Babylon with React - unable to load Model

Hello guys :slight_smile:
I’m a React developer, totally new to Babylon.js but will be using it for my next project so trying to get stuck in!
I have taken a look at the docs, and particularly this.
I’ve been successful in creating a scene and drawing simple objects.
The issue is, I cannot import a gLTF model. I get a 404 not found response every time. Presumably the issue is the rootUrl pathname but I cannot get it right.

Here is my code below…
(this is slightly simplified. In my actual code I’m wrapping Model in a HOC using Suspense to show a loading state, but as I can’t even get the pathname right I figured simple is easier :joy:)

import React, { useRef, useState } from 'react';
import {
  Engine,
  Scene,
  useBeforeRender,
  useClick,
  useHover,
} from 'react-babylonjs';
import { Vector3, Color3 } from '@babylonjs/core';

const DefaultScale = new Vector3(1, 1, 1);
const BiggerScale = new Vector3(1.25, 1.25, 1.25);

const SpinningBox = ({ name, position, color, hoveredColor }) => {
  // access Babylon scene objects with same React hook as regular DOM elements
  const boxRef = useRef(null);

  const [clicked, setClicked] = useState(false);
  useClick(() => setClicked(() => !clicked), boxRef);

  const [hovered, setHovered] = useState(false);
  useHover(
    () => setHovered(true),
    () => setHovered(false),
    boxRef
  );

  // This will rotate the box on every Babylon frame.
  const rpm = 5;
  useBeforeRender((scene) => {
    if (boxRef.current) {
      // Delta time smoothes the animation.
      const deltaTimeInMillis = scene.getEngine().getDeltaTime();
      boxRef.current.rotation.y +=
        (rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000);
    }
  });

  return (
    <box
      name={name}
      ref={boxRef}
      size={2}
      position={position}
      scaling={clicked ? BiggerScale : DefaultScale}
    >
      <standardMaterial
        name={`${name}-mat`}
        diffuseColor={hovered ? hoveredColor : color}
        specularColor={Color3.Black()}
      />
    </box>
  );
};

export const InteractiveMap = () => (
  <div>
    <Engine antialias adaptToDeviceRatio canvasId="babylonJS">
      <Scene>
        <arcRotateCamera
          name="camera1"
          target={Vector3.Zero()}
          alpha={Math.PI / 2}
          beta={Math.PI / 4}
          radius={8}
        />
        <hemisphericLight
          name="light1"
          intensity={0.7}
          direction={Vector3.Up()}
        />
        <Model sceneFilename="scene.gltf"
          rootUrl = './'
          position = { new Vector3(0.02, 0, 0) }
         />
        <SpinningBox
          name="left"
          position={new Vector3(-2, 0, 0)}
          color={Color3.FromHexString('#EEB5EB')}
          hoveredColor={Color3.FromHexString('#C26DBC')}
        />
        <SpinningBox
          name="right"
          position={new Vector3(2, 0, 0)}
          color={Color3.FromHexString('#C8F4F9')}
          hoveredColor={Color3.FromHexString('#3CACAE')}
        />
      </Scene>
    </Engine>
  </div>
);

export default { InteractiveMap };

The scene.gltf file is currently located in the same folder as the InteractiveMap component.
If anyone can help, that would be great!

Many thanks and I look forward to playing more with this when I sort the import issue :grin:

I use babylonjs with react but vanilla without anything like react-babylonjs but in this case it mostly looks like an url issue.
I suggest that you check network calls in your inspector the url that is actually called and check if it is the right path.

Same here, I don’t like to use the third party libs when they need more learning time than they save coding time.
With just babylon and react (or vue, any other front-end framework i believe), I know that in order to import GLTF files, you need not only to import babylonjs, but also babyonjs-loaders. (see here)
Maybe it’s not needed with react-babylonjs, but I don’t know that.

I think your rootURL is wrong indeed. From my experience, GLB import are not computed at buildtime, but at runtime. So the " ./ " root path you wrote is probably referring to your public/ folder, not the one your component lives in. Try putting your glb (or gltf, sorry I get confused) there instead.
Let us know if that helps, and sorry if I got it wrong !

hi @Olly_Bolland - welcome to the forum!

What Faber said about the rootUrl is likely the culprit - using a root url also I would recommend instead of ./ to use /, since if you are using react router that it will try loading relative to current path, whereas a leading slash is an absolute URL within the public folder. If you are deploying to a subdirectory then you can use the public URL. I think he’s referencing the wrong loaders above, since you are using @babylonjs/core then you want to ES6 import from @babylonjs/loaders (not babylonjs-loaders).

The other thing is that you are missing the load for side effects:

import "@babylonjs/loaders/glTF";

Let us know if you have any follow up questions or if you can provide a more detailed error response. Look in the network tab and if your gltf response is the index.html content then you know that it’s not able to serve the static asset from your public folder :slight_smile: that is a very common issue lots of people come across on a CRA project.

1 Like

Thank you everyone for your replies :slight_smile:
I wasn’t expecting such an active community, so nice to see all of these comments.
I have now successfully loaded the model, thank you for all your help.
So my follow up question is this…
I have my Model component successfully loaded and rendering. What I want to do, is render several instances of the same component but with different props. I have tried this in the return statement…

{ [...Array(5)].keys().map((i) => (
   <Model sceneFilename="scene.gltf"
          rootUrl = './'
          position = { new Vector3(0, 0, i) }
         />
))}

This doesn’t work however, it only renders a single instance of the Model. I expect because this is actually loading the component a new each time, rather than mapping several instances of the same component.
So my follow up question is how can this be achieved in Babylon?

I was able to do it with an array and a For loop. Try something like this:

You can definitely import a mesh from the same file multiple times. I’m not using the JSX component here but I would imagine it should work. I did have trouble getting them to import when I tried a single stacked expression like yours, using the map method…don’t quote me on that, I couldn’t tell you why exactly, or what I may be missing.

But I was able to do it this way, by creating the array separately, and calling the import directy under a for loop. Here’s the code pasted in text. You could try replacing the “BABYLON.SceneLoader.ImportMesh” call with your JSX <Model statement inside the for loop {using either “i” or “keys”, obviously}

{
   const arr = Array(5).keys();

    for (const key of arr) {
        BABYLON.SceneLoader.ImportMesh(
            "",
            "./", 
            "FerarriTestarossa.gltf", 
            scene,
            function(meshes){
                const car = meshes[0];
                console.log(car);
                car.position.y = key;
            }
        )
    }
}

Thanks @3dwebgs :slight_smile:
I’ve ditched the react-babylon library for the time being in favour of the imperative approach.
I have success importing files inside a loop like you suggested above, thanks for that!
My next question is how do you go about doing this dynamically?
For example, let’s say you’ve just imported these cars in a loop and rendering them onto the canvas like you demonstrated above. What if I want to click a button which runs a fetch to a database to fetch new x and y coordinates for the cars? Currently I’m fetching the data successfully, but I don’t know how to tell Babylon to re-render with the new data

You will need to track all of the imported meshes in state. Once you have set the positions on those mesh/node objects they will update on the babylon scene, which doesn’t need the re-render you would typically need with React.

Hi back ! Just for the sake of it, maybe we could split it in a different thread ? The topic has deviated a little, so if the initial question has been solved, we could close this conversion, and discuss the follow-up in a new question linking to this one ? :slight_smile:
It’s just for other users that might have similar issues, so that they can find their answer easier.