react-babylonjs icon indicating copy to clipboard operation
react-babylonjs copied to clipboard

Using two Engine elements does not result in two independent views

Open nene opened this issue 1 year ago • 10 comments

I have the following simple component that creates a scene with a cube and a gizmomanager:

export function Area3D() {
  return (
    <Engine antialias adaptToDeviceRatio>
      <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()}
        />
        <gizmoManager
          positionGizmoEnabled={true}
          usePointerToAttachGizmos={true}
        />
        <box name="box1">
          <standardMaterial
            name="material1"
            diffuseColor={Color3.FromHexString("#EEB5EB")}
          />
        </box>
      </Scene>
    </Engine>
  );
}

And then inside my app I render two instances of that component:

function App() {
  return (
    <>
      <Area3D />
      <Area3D />
    </>
  );
}

I would expect this to result in 2 completely independent scenes on two canvases.

What I actually get are two canvases that are linked with each other in some mysterious way. Like when I click the cube on the first canvas, the gizmo appears in the second canvas, like so:

Screenshot 2025-01-05 at 13 03 30

When I then click the cube in second canvas I discover that I now got two gizmos, with one Gizmo controlling the cube in the first canvas and the other controlling the cube in other canvas.

Screenshot 2025-01-05 at 13 07 36

It's not only a problem with gizmos. Various other things go wrong as well, like loading a model in one scene loads it also to another scene. This box with gizmo was just the simplest case I came up with, which seems to indicate some more fundamental issue here. Though the fundamental issue might just be in my lack of experience with 3D graphics.

This might be a problem with BabylonJS itself, though when I searched about this, I came away with a conclusion that it should be possible to have two engines running two different scenes on separate canvases. But I might have misinterpreted what I read.

PS. The goal in my actual app is to render two completely different scenes on two different canvases.

Using:

  • react-babylonjs 3.2.2
  • BabylonJS 7.42.0
  • React 18.3.1

nene avatar Jan 05 '25 11:01 nene

Looks like this is actually a problem with BabylonJS itself. I created a scene with plain babylonJS on another canvas alongside the react-babylonjs scene. It kind of works, I guess, but as soon as I add a gizmomanager to it, the gizmomanager in the other canvas breaks :(

nene avatar Jan 05 '25 21:01 nene

I would suspect something like usePointerToAttachGizmos is attaching to window pointer events, while you only want canvas events. Did you try without that?

brianzinn avatar Jan 05 '25 23:01 brianzinn

I tried the following, but the exact same problem happens (the two gizmos appear in the second canvas):

export function Area3D() {
  const meshRef = useRef<Mesh>(null);
  const gizmoRef = useRef<GizmoManager>(null);

  useEffect(() => {
    // a terrible hack
    setTimeout(() => {
      if (gizmoRef.current && meshRef.current) {
        gizmoRef.current.attachToMesh(meshRef.current);
      }
    }, 1000);
  }, [gizmoRef, meshRef]);

  return (
    <Engine antialias adaptToDeviceRatio>
      <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()}
        />
        <gizmoManager
          ref={gizmoRef}
          positionGizmoEnabled={true}
          usePointerToAttachGizmos={false}
        />
        <box name="box1" ref={meshRef}>
          <standardMaterial
            name="material1"
            diffuseColor={Color3.FromHexString("#EEB5EB")}
          />
        </box>
      </Scene>
    </Engine>
  );
}

Note the use of setTimeout(). I was trying to use useEffect() to wait for both gizmo and mesh refs to become instantiated. That does happen, but it doesn't trigger re-render, so the hook never gets called again. Am I doing something wrong here or the refs can't really be relied upon when using react-babylonjs? How would you achieve attaching the gizmo without using refs?

nene avatar Jan 06 '25 07:01 nene

hi @nene,

Am I doing something wrong here or the refs can't really be relied upon when using react-babylonjs?

Good example BTW that shows the issue. It's clear what you are trying to accomplish.

That's more React specific than this library. Setting a ref doesn't trigger a re-render. I also think refs don't work in a useEffect dependency (like it's the same instance). I think you get a lint warning if you use ref.current. If you want a way to trigger a rerender then one way is to change the ref setter to a function:

const [ready, setReady] = useState<boolean | null>(null);
const gizmoRef = useRef();
useEffect(() => {
    if (gizmoRef.current) {
       console.log('gizmo ready', gizmoRef.current);
    }
}, [ready])

<gizmoManager ref={(gizmo) => {
          gizmoRef.current = gizmo
          setReady(true)
        }}
  ...
</gizmoManager>

brianzinn avatar Jan 06 '25 18:01 brianzinn

Thanks for the clarification. Looks like my mental model of refs has been terribly distorted.

I guess my confusion stems from useEffect() running after an initial render. And after that initial render the DOM elements have been created and so the ref.current values are also set. Or at least so it seems to be nearly always... well... I don't really know of any other occasion than this situation I encountered today.

Looks like the difference is where this useEffect() is situated. In my code example I placed it in a component that itself renders to main <Engine> component. And there when useEffect() is called after render, the gizmoRef.current is null. I guess that happens because the the <canvas> element gets rendered to the DOM, but the BablylonJS hasn't actually rendered the scene yet.

However when I wrap this useEffect() and the gizmo inside a sub-component (that is placed inside <Engine>), then the gizmoRef.current will no more be null when the useEffect() hook runs. Presumably because now the initial render (after which useEffect() runs) is actually a BabylonJS render, and so all BabylonJS objects have been created.

nene avatar Jan 06 '25 21:01 nene

I think it was you that had asked in the forum the same question. That seems to have stalled there after being assigned. I'll wait until that thread continues - I'm watching it. Thanks for posting a repro there (if it was you). 😄

brianzinn avatar Jan 11 '25 23:01 brianzinn

Yep. It was me :)

On Sun, Jan 12, 2025, 01:46 Brian Zinn @.***> wrote:

I think it was you that had asked in the forum the same question. That seems to have stalled there after being assigned. I'll wait until that thread continues - I'm watching it. Thanks for posting a repro there (if it was you). 😄

— Reply to this email directly, view it on GitHub https://github.com/brianzinn/react-babylonjs/issues/333#issuecomment-2585481785, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA43OOHZXBY3H7FTRGPVCD2KGUOJAVCNFSM6AAAAABUUAXQMSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKOBVGQ4DCNZYGU . You are receiving this because you were mentioned.Message ID: @.> [ { @.": "http://schema.org", @.": "EmailMessage", "potentialAction": { @.": "ViewAction", "target": " https://github.com/brianzinn/react-babylonjs/issues/333#issuecomment-2585481785", "url": " https://github.com/brianzinn/react-babylonjs/issues/333#issuecomment-2585481785", "name": "View Issue" }, "description": "View this Issue on GitHub", "publisher": { @.***": "Organization", "name": "GitHub", "url": " https://github.com" } } ]

nene avatar Jan 12 '25 00:01 nene

There is an update in the forum. It's a bit bizarre the answer, if you ask me. Can you confirm it works and then if that's the solution we can look at how to address the utility layer?

brianzinn avatar Jan 15 '25 16:01 brianzinn

Yep. Tested it. It does work.

I first tried something like:

<>
  <utilityLayerRenderer ref={layerRef} />
  <gizmoManager
    utilityLayer={layerRef.current}
    keepDepthUtilityLayer={layerRef.current}
    positionGizmoEnabled={!!layerRef.current}
  />
</>

But that resulted in the same behavior as before. It seems that it's crucial to set the utility layer at construction time.

When I implemented it procedurally, then it worked. Did something like this:

function MyComp() {
  const managerRef = useRef<GizmoManager | null>(null);
  const scene = useScene();

  useEffect(() => {
    if (scene) {
      const utilityLayer = new UtilityLayerRenderer(scene);
      const gm = new GizmoManager(scene, 1, utilityLayer, utilityLayer);
      gm.positionGizmoEnabled = true;
      managerRef.current = gm;
    }
  }, [scene]);

  return null;
}

nene avatar Jan 16 '25 09:01 nene

ok - i'm probably just not recognizing those properties in the reconciler - to make a declarative version possible. I should maybe just assign all unrecognized props as a fallthrough. That will work on a lot of things besides observerables.

brianzinn avatar Jan 16 '25 17:01 brianzinn