Babylon React Native WebXR Demo issues

Hi there!

I’ve been trying to get the Babylon React Native WebXR demo found here(Babylon React Native Demo - YouTube, https://playground.babylonjs.com/#LQMZQ7#0) to work, but i’m having some issues. I basically copied most of the code, and merged with the Babylon React Native playground app.

First issue i’m having is related to earcut injection on the MeshBuilder.CreatePolygon function, i’ve imported earcut as described here: https://forum.babylonjs.com/t/how-to-inject-earcut-to-a-react-babylon-project/14905 but still, it’s throwing me this error :

[TypeError: this.bjsEarcut is not a function. (In 'this.bjsEarcut(this._epoints, this._eholes, 2)', 'this.bjsEarcut' is an instance of Object)]

at

 try {
                plane.mesh = MeshBuilder.CreatePolygon(
                  "plane",
                  { shape: plane.polygonDefinition },
                  scene,
                  earcut
                );

Second issue is related to the Device Source Manager observables not receiving updated conditional variables after initial call. I did :

useEffect(() => {
    createInputHandling();
  }, [xrSession]);

  const createInputHandling = useCallback(() => {
    async function inputListen() {
      //   var numInputs = 0;
      deviceSourceManager?.onDeviceConnectedObservable.add((device) => {
        // numInputs++;

        if (device.deviceType === DeviceType.Touch) {
          const touch: DeviceSource<DeviceType.Touch> =
            deviceSourceManager.getDeviceSource(
              device.deviceType,
              device.deviceSlot
            )!;
          touch.onInputChangedObservable.add((touchEvent) => {
         
            if (
              model &&
              xrSession &&
              placementIndicator?.isEnabled() &&
              !modelPlaced
            ) {
              placeModel();
              console.log("model placing input");
              //numInputs--;
            }

            if (
              model &&
              modelPlaced &&
              xrSession &&
              placementIndicator?.isEnabled() &&
              touchEvent.previousState !== null &&
              touchEvent.currentState !== null
            ) {
              console.log("update input");
              // Calculate the differential between two states.
              const diff = touchEvent.previousState - touchEvent.currentState;
              // Single input, do translation.
              //if (numInputs === 1) {
              if (touchEvent.inputIndex === PointerInput.Horizontal) {
                model.position.x -= diff / 1000;
              } else {
                model.position.z += diff / 750;
              }
              // }
              // Multi-input do rotation.
              //   if (
              //     numInputs === 2 &&
              //     touchEvent.inputIndex === PointerInput.Horizontal &&
              //     touchEvent.deviceSlot === 0
              //   ) {
              //     console.log("multi touch");
              //     model.rotate(Vector3.Up(), diff / 200);
              //   }
            }
          });
        } else if (device.deviceType === DeviceType.Mouse) {
          const mouse: DeviceSource<DeviceType.Mouse> =
            deviceSourceManager.getDeviceSource(
              device.deviceType,
              device.deviceSlot
            )!;
          mouse.onInputChangedObservable.add((mouseEvent) => {
            if (mouse.getInput(PointerInput.LeftClick)) {
              return;
            }
          });
        }
      });
    }
    inputListen();
  }, [xrSession, placementIndicator, modelPlaced]);

Initial touch places the model, set modelPlaced to true and disable placementIndicator, which should return false for the same condition on the next touch, however it’s always returning true. I’m not sure if i did some mistake here, or if i’m missing something out. Any inputs would be appreciated. Thanks

Full code:

import React, { useCallback, useEffect, useState } from "react";
import { View, ViewProps, Button, Image, Text } from "react-native";

import {
  EngineView,
  EngineViewCallbacks,
  useEngine,
} from "@babylonjs/react-native";

import {
  AbstractMesh,
  ArcRotateCamera,
  Camera,
  Color3,
  Color4,
  DeviceSource,
  DeviceSourceManager,
  DeviceType,
  HemisphericLight,
  Mesh,
  MeshBuilder,
  Nullable,
  PointerInput,
  Quaternion,
  Scene,
  SceneLoader,
  StandardMaterial,
  Texture,
  TransformNode,
  TubeBuilder,
  Vector3,
  WebXRFeatureName,
  WebXRHitTest,
  WebXRPlaneDetector,
  WebXRSessionManager,
  WebXRTrackingState,
} from "@babylonjs/core";
import "@babylonjs/loaders";
import { StyleProp, StyleSheet, ViewStyle } from "react-native";
// import { MathJaxSvg } from "react-native-mathjax-html-to-svg";
import Slider from "@react-native-community/slider";

//import { Slider, Text, Button, Image } from "react-native-elements";
import { SafeAreaView } from "react-native-safe-area-context";

import * as earcut from "earcut";

const EngineScreen = (props) => {
  const defaultScale = 1;
  const enableSnapshots = false;

  const engine = useEngine();
  const [toggleView, setToggleView] = useState(false);
  const [camera, setCamera] = useState<ArcRotateCamera | undefined>();
  const [rootNode, setRootNode] = useState<TransformNode | undefined>();
  const [scene, setScene] = useState<Scene>();
  const [xrSession, setXrSession] = useState<WebXRSessionManager>();
  const [scale, setScale] = useState<number>(defaultScale);
  const [snapshotData, setSnapshotData] = useState<string>();
  const [engineViewCallbacks, setEngineViewCallbacks] =
    useState<EngineViewCallbacks>();
  const [trackingState, setTrackingState] = useState<WebXRTrackingState>();

  const [placementIndicator, setPlacementIndicator] =
    useState<AbstractMesh | undefined>();
  const [model, setModel] = useState<AbstractMesh>();
  const [modelPlaced, setModelPlaced] = useState<boolean>(false);
  const [planeMat, setPlaneMat] = useState<any>();
  const [deviceSourceManager, setDeviceSourceManager] =
    useState<DeviceSourceManager>();

  useEffect(() => {
    async function init() {
      if (engine) {
        const scene = new Scene(engine);
        setScene(scene);
        scene.createDefaultCamera(true);
        (scene.activeCamera as ArcRotateCamera).beta -= Math.PI / 8;
        setCamera(scene.activeCamera! as ArcRotateCamera);
        //scene.createDefaultLight(true);
        //scene.clearColor = new Color4(0, 0, 0, 0);
        const light = new HemisphericLight(
          "light1",
          new Vector3(0, 5, 0),
          scene
        );
        light.diffuse = Color3.White();
        light.intensity = 1;
        light.specular = new Color3(0, 0, 0);

        // Create the placement indicator
        let placementIndicator = Mesh.CreateTorus(
          "placementIndicator",
          0.5,
          0.005,
          64
        );
        let indicatorMat = new StandardMaterial("noLight", scene);
        indicatorMat.disableLighting = true;
        indicatorMat.emissiveColor = Color3.White();
        placementIndicator.material = indicatorMat;
        placementIndicator.scaling = new Vector3(1, 0.01, 1);
        placementIndicator.setEnabled(false);
        setPlacementIndicator(placementIndicator);

        const rootNode = new TransformNode("Root Container", scene);
        setRootNode(rootNode);

        const transformContainer = new TransformNode(
          "Transform Container",
          scene
        );
        transformContainer.parent = rootNode;
        transformContainer.scaling.scaleInPlace(0.2);
        transformContainer.position.y -= 0.2;

        // import model
        SceneLoader.ImportMeshAsync(
          "",
          "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoxAnimated/glTF-Binary/BoxAnimated.glb"
          //"https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BrainStem/glTF/BrainStem.gltf"
        ).then((result) => {
          const mesh = result.meshes[0];

          mesh.position.y = 1;
          //mesh.lookAt(camera?.position);

          //mesh.scalingDeterminant = 0;
          mesh.parent = transformContainer;
          setModel(mesh);
          //camera?.setTarget(mesh);
        });

        const planeTexture = new Texture(
          "https://i.imgur.com/z7s3C5B.png",
          scene
        );
        planeTexture.hasAlpha = true;
        planeTexture.uScale = 3;
        planeTexture.vScale = 3;
        planeTexture.coordinatesMode = Texture.PROJECTION_MODE;

        const planeMat = new StandardMaterial("noLight", scene);
        planeMat.diffuseTexture = planeTexture;
        setPlaneMat(planeMat);

        const deviceSourceManager = new DeviceSourceManager(engine);
        setDeviceSourceManager(deviceSourceManager);
      }
    }
    init();
  }, [engine]);

  ////////////////
  const resetClick = () => {
    if (model && camera && scene && placementIndicator) {
      if (xrSession) {
        setModelPlaced(false);
        model.setEnabled(false);
        placementIndicator.setEnabled(true);
      } else {
        placementIndicator.setEnabled(false);
        // reset2D()
      }
    }
  };

  useEffect(() => {
    if (rootNode) {
      rootNode.scaling = new Vector3(scale, scale, scale);
    }
  }, [rootNode, scale]);

  useEffect(() => {
    createInputHandling();
  }, [xrSession]);

  const createInputHandling = useCallback(() => {
    async function inputListen() {
      //   var numInputs = 0;
      deviceSourceManager?.onDeviceConnectedObservable.add((device) => {
        // numInputs++;

        if (device.deviceType === DeviceType.Touch) {
          const touch: DeviceSource<DeviceType.Touch> =
            deviceSourceManager.getDeviceSource(
              device.deviceType,
              device.deviceSlot
            )!;
          touch.onInputChangedObservable.add((touchEvent) => {
            if (
              model &&
              xrSession &&
              placementIndicator?.isEnabled() &&
              !modelPlaced
            ) {
              placeModel();

              //placementIndicator?.setEnabled(false);
              console.log("model placing input");
              //numInputs--;
            }

            if (
              model &&
              modelPlaced &&
              xrSession &&
              placementIndicator?.isEnabled() &&
              touchEvent.previousState !== null &&
              touchEvent.currentState !== null
            ) {
              console.log("update input");
              // Calculate the differential between two states.
              const diff = touchEvent.previousState - touchEvent.currentState;
              // Single input, do translation.
              //if (numInputs === 1) {
              if (touchEvent.inputIndex === PointerInput.Horizontal) {
                model.position.x -= diff / 1000;
              } else {
                model.position.z += diff / 750;
              }
              // }
              // Multi-input do rotation.
              //   if (
              //     numInputs === 2 &&
              //     touchEvent.inputIndex === PointerInput.Horizontal &&
              //     touchEvent.deviceSlot === 0
              //   ) {
              //     console.log("multi touch");
              //     model.rotate(Vector3.Up(), diff / 200);
              //   }
            }
          });
        } else if (device.deviceType === DeviceType.Mouse) {
          const mouse: DeviceSource<DeviceType.Mouse> =
            deviceSourceManager.getDeviceSource(
              device.deviceType,
              device.deviceSlot
            )!;
          mouse.onInputChangedObservable.add((mouseEvent) => {
            if (mouse.getInput(PointerInput.LeftClick)) {
              return;
            }
          });
        }
      });
    }
    inputListen();
  }, [xrSession, placementIndicator, modelPlaced]);

  const reset2D = () => {
    if (model && scene && camera) {
      model.setEnabled(true);
      model.position = camera.position.add(
        camera.getForwardRay().direction.scale(scale * 1)
      );
      placementIndicator?.setEnabled(false);
      setModelPlaced(false);
      //model.scalingDeterminant = 0;
      //camera.setTarget(model)
    }
  };

  const placeModel = () => {
    console.log("placeModel");
    if (
      xrSession &&
      placementIndicator?.isEnabled() &&
      scene &&
      model &&
      !modelPlaced
    ) {
      setModelPlaced(true);
      model.rotationQuaternion = Quaternion.Identity();

      model.position = placementIndicator.position.clone();
      placementIndicator?.setEnabled(false);
      model.setEnabled(true);

      //model.scalingDeterminant = 0;
    }
  };

  const trackingStateToString = (
    trackingState: WebXRTrackingState | undefined
  ): string => {
    return trackingState === undefined ? "" : WebXRTrackingState[trackingState];
  };

  const onToggleXr = useCallback(() => {
    (async () => {
      if (xrSession) {
        reset2D();
        await xrSession.exitXRAsync();
      } else {
        if (rootNode !== undefined && scene !== undefined) {
          const xr = await scene.createDefaultXRExperienceAsync({
            disableDefaultUI: true,
            disableTeleportation: true,
          });
          const session = await xr.baseExperience.enterXRAsync(
            "immersive-ar",
            "unbounded",
            xr.renderTarget
          );

          setModelPlaced(false);
          model?.setEnabled(false);

          setXrSession(session);
          session.onXRSessionEnded.add(() => {
            setXrSession(undefined);
            setTrackingState(undefined);
          });

          setTrackingState(xr.baseExperience.camera.trackingState);
          xr.baseExperience.camera.onTrackingStateChanged.add(
            (newTrackingState) => {
              setTrackingState(newTrackingState);
            }
          );
          // Set up the hit test.
          const xrHitTestModule =
            xr.baseExperience.featuresManager.enableFeature(
              WebXRFeatureName.HIT_TEST,
              "latest",
              {
                offsetRay: {
                  origin: { x: 0, y: 0, z: 0 },
                  direction: { x: 0, y: 0, z: -1 },
                },
              }
            ) as WebXRHitTest;

          // Do some plane shtuff.
          const xrPlanes = xr.baseExperience.featuresManager.enableFeature(
            WebXRFeatureName.PLANE_DETECTION,
            "latest"
          ) as WebXRPlaneDetector;
          console.log("Enabled plane detection.");
          const planes: any[] = [];

          xrPlanes.onPlaneAddedObservable.add((webXRPlane) => {
            if (scene) {
              console.log("Plane added.");
              let plane: any = webXRPlane;
              webXRPlane.polygonDefinition.push(
                webXRPlane.polygonDefinition[0]
              );
              try {
                plane.mesh = MeshBuilder.CreatePolygon(
                  "plane",
                  { shape: plane.polygonDefinition },
                  scene,
                  earcut
                );
                let tubeMesh: Mesh = TubeBuilder.CreateTube(
                  "tube",
                  {
                    path: plane.polygonDefinition,
                    radius: 0.005,
                    sideOrientation: Mesh.FRONTSIDE,
                    updatable: true,
                  },
                  scene
                );
                tubeMesh.setParent(plane.mesh);
                planes[plane.id] = plane.mesh;
                plane.mesh.material = planeMat;

                plane.mesh.rotationQuaternion = new Quaternion();
                plane.transformationMatrix.decompose(
                  plane.mesh.scaling,
                  plane.mesh.rotationQuaternion,
                  plane.mesh.position
                );
              } catch (ex) {
                console.error(ex);
              }
            }
          });

          xrPlanes.onPlaneUpdatedObservable.add((webXRPlane) => {
            console.log("Plane updated.");
            let plane: any = webXRPlane;
            if (plane.mesh) {
              plane.mesh.dispose(false, false);
            }

            const some = plane.polygonDefinition.some((p: any) => !p);
            if (some) {
              return;
            }

            plane.polygonDefinition.push(plane.polygonDefinition[0]);
            try {
              plane.mesh = MeshBuilder.CreatePolygon(
                "plane",
                { shape: plane.polygonDefinition },
                scene,
                earcut
              );
              let tubeMesh: Mesh = TubeBuilder.CreateTube(
                "tube",
                {
                  path: plane.polygonDefinition,
                  radius: 0.005,
                  sideOrientation: Mesh.FRONTSIDE,
                  updatable: true,
                },
                scene
              );
              tubeMesh.setParent(plane.mesh);
              planes[plane.id] = plane.mesh;
              plane.mesh.material = planeMat;
              plane.mesh.rotationQuaternion = new Quaternion();
              plane.transformationMatrix.decompose(
                plane.mesh.scaling,
                plane.mesh.rotationQuaternion,
                plane.mesh.position
              );
              plane.mesh.receiveShadows = true;
            } catch (ex) {
              console.error(ex);
            }
          });

          xrPlanes.onPlaneRemovedObservable.add((webXRPlane) => {
            console.log("Plane removed.");
            let plane: any = webXRPlane;
            if (plane && planes[plane.id]) {
              planes[plane.id].dispose();
            }
          });

          xrHitTestModule.onHitTestResultObservable.add((results) => {
            if (results.length) {
              if (!modelPlaced) {
                placementIndicator?.setEnabled(true);
              } else {
                placementIndicator?.setEnabled(false);
              }

              if (placementIndicator) {
                placementIndicator.position = results[0].position;
              }
            }
          });
        }
      }
    })();
  }, [
    rootNode,
    scene,
    xrSession
  ]);

  const onInitialized = useCallback(
    async (engineViewCallbacks: EngineViewCallbacks) => {
      setEngineViewCallbacks(engineViewCallbacks);
    },
    [engine]
  );

  const onSnapshot = useCallback(async () => {
    if (engineViewCallbacks) {
      setSnapshotData(
        "data:image/jpeg;base64," + (await engineViewCallbacks.takeSnapshot())
      );
    }
  }, [engineViewCallbacks]);

  return (
    <>
      <View style={props.style}>
        <Button
          title="Toggle EngineView"
          onPress={() => {
            setToggleView(!toggleView);
          }}
        />
        <Button
          title={xrSession ? "Stop XR" : "Start XR"}
          onPress={() => {
            //desabilitar o model assim que inicia a xrsession, no usecallback não funciona
            if (!xrSession) {
              model?.setEnabled(false);
            }
            onToggleXr();
          }}
        />

        {!toggleView && (
          <View style={{ flex: 1 }}>
            {enableSnapshots && (
              <View style={{ flex: 1 }}>
                <Button title={"Take Snapshot"} onPress={onSnapshot} />
                <Image style={{ flex: 1 }} source={{ uri: snapshotData }} />
              </View>
            )}
            <EngineView
              style={props.style}
              camera={camera}
              onInitialized={onInitialized}
            />
            <Slider
              style={{
                position: "absolute",
                minHeight: 50,
                margin: 10,
                left: 0,
                right: 0,
                bottom: 0,
              }}
              minimumValue={0.2}
              maximumValue={2}
              step={0.01}
              value={defaultScale}
              onValueChange={setScale}
            />
            <Text
              style={{
                fontSize: 12,
                color: "yellow",
                position: "absolute",
                margin: 10,
              }}
            >
              {trackingStateToString(trackingState)}
            </Text>
          </View>
        )}
        {toggleView && (
          <View
            style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
          >
            <Text style={{ fontSize: 24 }}>EngineView has been removed.</Text>
            <Text style={{ fontSize: 12 }}>
              Render loop stopped, but engine is still alive.
            </Text>
          </View>
        )}
      </View>
    </>
  );
};

const BabylonXrTest = ({ route, navigation }) => {
  const [toggleScreen, setToggleScreen] = useState(false);
  //const { topic } = route.params;

  return (
    <>
      <SafeAreaView style={{ flex: 1, backgroundColor: "white" }}>
        {!toggleScreen && <EngineScreen style={{ flex: 1 }} />}
        {toggleScreen && (
          <View
            style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
          >
            <Text style={{ fontSize: 24 }}>EngineScreen has been removed.</Text>
            <Text style={{ fontSize: 12 }}>
              Engine has been disposed, and will be recreated.
            </Text>
          </View>
        )}
        <Button
          title="Toggle EngineScreen"
          onPress={() => {
            setToggleScreen(!toggleScreen);
          }}
        />
      </SafeAreaView>
    </>
  );
};

export default BabylonXrTest;

const styles = StyleSheet.create({
  engineView: {
    backgroundColor: "transparent",
  },
});

Adding @brianzinn

How did you import earcut. This works for me in non-native:

import * as Earcut from 'earcut';

// then you pass in Earcut to the earcutInjection parameter in the creation method.

I have this in my package.json:

"earcut": "^2.2.2",

It’s the 6th parameter and I think you have it as the 4th parameter:
MeshBuilder | Babylon.js Documentation (babylonjs.com)

edit: you imported earcut OK. the * export can have any name.

1 Like
import { 
  WebXRFeatureName,
  WebXRSessionManager, 
  WebXRTrackingState, 
  WebXRPlaneDetector,
} from "@babylonjs/core/XR";
import * as Earcut from 'earcut';

import {
  Mesh,
  MeshBuilder,
  TubeBuilder,
  Quaternion
} from "@babylonjs/core"

plane.mesh = MeshBuilder.CreatePolygon(
  "plane",
  { shape: plane.polygonDefinition },
  scene,
  Earcut
);

I imported earcut as above and got this error

ERROR  [TypeError: this.bjsEarcut is not a function. (In 'this.bjsEarcut(this._epoints, this._eholes, 2)', 'this.bjsEarcut' is an instance of Object)]
ERROR  Error: Exception in HostFunction: Object is not a function

TypeError: Object is not a function
    at anonymous (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:485781:31)
    at anonymous (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:485755:44)
    at CreatePolygon (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:485562:45)
    at anonymous (http://localhost:8081/app/screens/ar-vr/ar/engine-screen.bundle?platform=android&app=io.gritglobal.stackfiwallet&modulesOnly=true&dev=true&minify=false&runModule=true&shallow=true:60:105)
    at anonymous (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:324714:49)
    at anonymous (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:513753:57)
    at forEach (native)
    at anonymous (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:513744:31)
    at anonymous (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:413891:32)
    at anonymous (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:324714:49)
    at renderFunction (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=io.gritglobal.stackfiwallet&modulesOnly=false&runModule=true:423457:54), js engine: hermes

package.json:

    "@babylonjs/core": "~5.17.0",
    "@babylonjs/loaders": "~5.17.0",
    "@babylonjs/react-native": "^1.3.5",
    "@babylonjs/react-native-iosandroid-0-69": "^1.3.5",
    "earcut": "~2.2.2",
    "react": "18.0.0",
    "react-native": "0.69.6",

Could you force earcut 2.2.2 ? just to check and adding @RaananW our build guru

I did and the outcome stayed the same.

Good afternoon,
I tried opening the playground posted by @mihuron in his question, (https://playground.babylonjs.com/#LQMZQ7#0) but it does not seem to work. The playground shows me errors such as:

Property ‘onAfterDeviceConnectedObservable’ does not exist on type ‘DeviceSourceManager’. Did you mean ‘onDeviceDisconnectedObservable’?

Property ‘previousState’ does not exist on type ‘IKeyboardEvent | IWheelEvent | IPointerEvent’.

Is there an updated version of the demo playground of Babylon React Native shown in the presentation video?
Thank you!

Try this https://playground.babylonjs.com/#LQMZQ7#1
I commented Bind touch event tho.

1 Like

Sorry if you did already - can you share a project reproduction? I want to see why the earcut injection doesn’t work as expected.

You can try this GitHub - thangld322/BabylonReactNativeSample

yarn install
yarn start
yarn android

It will install the app to your phone.
When you click the Start XR button, it will give this logs:

 LOG  Running "BabylonReactNativeSample" with {"rootTag":1}
 LOG  BJS - [11:15:17]: Babylon Native (v5.20.0) launched
 LOG  Enabled plane detection.
 WARN  The flags generateDepth/generateStencilBuffer are mismatched. Depth and stencil are combined in one texture
 LOG  Plane added.
 ERROR  [TypeError: this.bjsEarcut is not a function. (In 'this.bjsEarcut(this._epoints, this._eholes, 2)', 'this.bjsEarcut' is an instance of Object)]

@RaananW the playground still has the error @fede17 has asked tho.
Property ‘previousState’ does not exist on type ‘IKeyboardEvent | IWheelEvent | IPointerEvent’.
Can you take a look at it too? Thank you

1 Like

Hello,
I have downloaded the Babylon React Native sample from the GitHub repository (GitHub - BabylonJS/BabylonReactNativeSample), followed the instructions and managed to run it on my device. What the scene basically does is loading a gltf model, and it is very cool already. The instrictions in the github repository then indicate:

Once the project is running, open App.tsx using your favorite code editor (we recommend using vscode). Under EngineScreen.useEffect , you can setup your Babylon scene the same way you do when using the Babylon.js Playground.

What I was honestly expecting was to be able to reproduce pieces of code written in the playground in the native application without any code changes, just as it is shown in the demo video (https://www.youtube.com/watch?v=x6baGsEQhcU). What I see, however, it is this does not seem to be possible, and there is an amount of code changes required in order to load any playground scene in App.tsx. Is there anything I am doing wrong or something I have not really understood from the demo/the github repo instructions?
Thank you very much.

cc @BabylonNative

Hi @thangld
The version of BabylonReactNative and Babylon.js seem to be a little old.
Can you please try with Release 1.4.0 · BabylonJS/BabylonReactNative · GitHub
and latest babylon.js package?

Hi @Cedric. Thanks for responding.
I have managed to fix the earcut issue already.
I use typescript so it should have been installed as

"@types/earcut": "2.1.1"

not

"earcut": "2.2.2"

and imported as

import earcut from "earcut";

not

import * as Earcut from 'earcut';

It works now. Hope this will help others.

6 Likes

For future googlers, since this is the top search result for this.bjsEarcut is not a function

I ran into the same error when using vite and trying to import earcut. I spent a few hours trying to import earcut in various ways and always getting this.bjsEarcut is not a function in production with vite (dev mode works)

For now I just copied earcut’s code into my local project and set window.earcut equal to it before babylon gets imported. This is working, albeit with opting out of npm dependency management. Wanted to share what I learned in case it can save someone else time.

Briefly, in-case people are still hitting this issue - what seems to work for me is using:

import * as earcut from 'earcut';

Then passing the reference to PolygonMeshBuilder as earcut.default, e.g:

new PolygonMeshBuilder("name", vector_list, scene, earcut.default);

I am using ES6 imports with rollup with various plugins to package/tree-shake node/npm stuff for the web so not sure if this is applicable outside of this scenario, but thought it may help someone.

earcut package version is 2.2.4 from NPM

4 Likes