React Native with Canvas rendering

I’ve been working on code to render panorama images in React Native. The code is working in standard React on the Web and I’ve been trying to figure out how to port this to iOS through the Babylon React Native.

My code uses a Canvas in React Web. I’ve added React Native Canvas to support Canvas objects. I am having issues connecting the Engine rendering job to the Canvas that’s created in React Native. Babylon React Native has its own lifecycle manager through useEngine() that doesn’t let me pass in parameters var engine = new Engine(canvas, true);. Could someone please explain how to connect the useEngine provided engine to my Canvas?

return (
      <canvas id="renderCanvas"></canvas>
    )

It then constructs a Babylon engine and creates the scene with an image I pull in from response.data.signed_url

            // get the canvas DOM element
            var canvas = document.getElementById('renderCanvas');
        
            // load the 3D engine
            var engine = new Engine(canvas, true);
        
            var createScene = function () {
                var scene = new Scene(engine);
                var dome = new PhotoDome(
                    "testdome",
                    response.data.signed_url,
                    {
                        resolution: 32,
                        size: 1000,
                        // useDirectMapping = true
                    },
                    scene
                );
                // dome.fovMultiplier = 2.0
                addSphericalPanningCameraToScene(scene, canvas);
        
                return scene;
            };
        
            // call the createScene function
            var scene = createScene();
        
            // run the render loop
            engine.runRenderLoop(function(){
                scene.render();
            });
        
            // the canvas/window resize event handler
            window.addEventListener('resize', function(){
                engine.resize();
            });

The main function takes in the

      var addSphericalPanningCameraToScene = function (scene, canvas) {
      // Set cursor to grab.
      scene.defaultCursor = "grab";
  
      // Add the actual camera to the scene.  Since we are going to be controlling it manually,
      // we don't attach any inputs directly to it.
      // NOTE: We position the camera at origin in this case, but it doesn't have to be there.
      // Spherical panning should work just fine regardless of the camera's position.
      var camera = new FreeCamera("camera", Vector3.Zero(), scene);
  
      
  
      // Ensure the camera's rotation quaternion is initialized correctly.
      camera.rotationQuaternion = Quaternion.Identity();
  
      // The spherical panning math has singularities at the poles (up and down) that cause
      // the orientation to seem to "flip."  This is undesirable, so this method helps reject
      // inputs that would cause this behavior.
      var isNewForwardVectorTooCloseToSingularity = v => {
          const TOO_CLOSE_TO_UP_THRESHOLD = 0.99;
          return Math.abs(Vector3.Dot(v, Vector3.Up())) > TOO_CLOSE_TO_UP_THRESHOLD;
      }
  
      // Local state variables which will be used in the spherical pan method; declared outside 
      // because they must persist from frame to frame.
      var ptrX = 0;
      var ptrY = 0;
      var inertiaX = 0;
      var inertiaY = 0;
  
      // Variables internal to spherical pan, declared here just to avoid reallocating them when
      // running.
      var priorDir = new Vector3();
      var currentDir = new Vector3();
      var rotationAxis = new Vector3();
      var rotationAngle = 0;
      var rotation = new Quaternion();
      var newForward = new Vector3();
      var newRight = new Vector3();
      var newUp = new Vector3();
      var matrix = new Matrix.Identity();
  
      // The core pan method.
      // Intuition: there exists a rotation of the camera that brings priorDir to currentDir.
      // By concatenating this rotation with the existing rotation of the camera, we can move
      // the camera so that the cursor appears to remain over the same point in the scene, 
      // creating the feeling of smooth and responsive 1-to-1 motion.
      var pan = (currX, currY) => {
        // Helper method to convert a screen point (in pixels) to a direction in view space.
        var getPointerViewSpaceDirectionToRef = (x, y, ref) => {
            Vector3.UnprojectToRef(
                new Vector3(x, y, 0), 
                canvas.width, 
                canvas.height,
                Matrix.Identity(),
                Matrix.Identity(), 
                camera.getProjectionMatrix(),
                ref);
            ref.normalize();
        }
  
        // Helper method that computes the new forward direction.  This was split into its own
        // function because, near the singularity, we may to do this twice in a single frame
        // in order to reject inputs that would bring the forward vector too close to vertical.
        var computeNewForward = (x, y) => {
            getPointerViewSpaceDirectionToRef(ptrX, ptrY, priorDir);
            getPointerViewSpaceDirectionToRef(x, y, currentDir);
  
            Vector3.CrossToRef(priorDir, currentDir, rotationAxis);
  
            // If the magnitude of the cross-product is zero, then the cursor has not moved
            // since the prior frame and there is no need to do anything.
            if (rotationAxis.lengthSquared() > 0) {
                rotationAngle = Vector3.GetAngleBetweenVectors(priorDir, currentDir, rotationAxis);
                Quaternion.RotationAxisToRef(rotationAxis, -rotationAngle, rotation);
  
                // Order matters here.  We create the new forward vector by applying the new rotation 
                // first, then apply the camera's existing rotation.  This is because, since the new
                // rotation is computed in view space, it only makes sense for a camera that is
                // facing forward.
                newForward.set(0, 0, 1);
                newForward.rotateByQuaternionToRef(rotation, newForward);
                newForward.rotateByQuaternionToRef(camera.rotationQuaternion, newForward);
  
                return !isNewForwardVectorTooCloseToSingularity(newForward);
            }
  
            return false;
        }
  
        // Compute the new forward vector first using the actual input, both X and Y.  If this results
        // in a forward vector that would be too close to the singularity, recompute using only the
        // new X input, repeating the Y input from the prior frame.  If either of these computations
        // succeeds, construct the new rotation matrix using the result.
        if (computeNewForward(currX, currY) || computeNewForward(currX, ptrY)) {
            // We manually compute the new right and up vectors to ensure that the camera 
            // only has pitch and yaw, never roll.  This dependency on the world-space
            // vertical axis is what causes the singularity described above.
            Vector3.CrossToRef(Vector3.Up(), newForward, newRight);
            Vector3.CrossToRef(newForward, newRight, newUp);
  
            // Create the new world-space rotation matrix from the computed forward, right, 
            // and up vectors.
            matrix.setRowFromFloats(0, newRight.x, newRight.y, newRight.z, 0);
            matrix.setRowFromFloats(1, newUp.x, newUp.y, newUp.z, 0);
            matrix.setRowFromFloats(2, newForward.x, newForward.y, newForward.z, 0);
  
            Quaternion.FromRotationMatrixToRef(matrix.getRotationMatrix(), camera.rotationQuaternion);
        }
      };
  
      // The main panning loop, to be run while the pointer is down.
      var sphericalPan = () => {
        pan(scene.pointerX, scene.pointerY);
  
        // Store the state variables for use in the next frame.
        inertiaX = scene.pointerX - ptrX;
        inertiaY = scene.pointerY - ptrY;
        ptrX = scene.pointerX;
        ptrY = scene.pointerY;
      }
  
      // The inertial panning loop, to be run after the pointer is released until inertia
      // runs out, or until the pointer goes down again, whichever happens first.  Essentially
      // just pretends to provide a decreasing amount of input based on the last observed input,
      // removing itself once the input becomes negligible.
      const INERTIA_DECAY_FACTOR = 0.9;
      const INERTIA_NEGLIGIBLE_THRESHOLD = 0.5;
      var inertialPanObserver;
      var inertialPan = () => {
          if (Math.abs(inertiaX) > INERTIA_NEGLIGIBLE_THRESHOLD || Math.abs(inertiaY) > INERTIA_NEGLIGIBLE_THRESHOLD) {
              pan(ptrX + inertiaX, ptrY + inertiaY);
  
              inertiaX *= INERTIA_DECAY_FACTOR;
              inertiaY *= INERTIA_DECAY_FACTOR;
          }
          else {
              scene.onBeforeRenderObservable.remove(inertialPanObserver);
          }
      };
  
      // Enable/disable spherical panning depending on click state.  Note that this is an 
      // extremely simplistic way to do this, so it gets a little janky on multi-touch.
      var sphericalPanObserver;
      var pointersDown = 0;
      scene.onPointerDown = () => {
          pointersDown += 1;
          if (pointersDown !== 1) {
              return;
          }
  
          // Disable inertial panning.
          scene.onBeforeRenderObservable.remove(inertialPanObserver);
  
          // Switch cursor to grabbing.
          scene.defaultCursor = "grabbing";
  
          // Store the current pointer position to clean out whatever values were left in
          // there from prior iterations.
          ptrX = scene.pointerX;
          ptrY = scene.pointerY;
  
          // Enable spherical panning.
          sphericalPanObserver = scene.onBeforeRenderObservable.add(sphericalPan);
      }
      scene.onPointerUp = () => {
          pointersDown -= 1;
          if (pointersDown !== 0) {
              return;
          }
  
          // Switch cursor to grab.
          scene.defaultCursor = "grab";
  
          // Disable spherical panning.
          scene.onBeforeRenderObservable.remove(sphericalPanObserver);
  
          // Enable inertial panning.
          inertialPanObserver = scene.onBeforeRenderObservable.add(inertialPan);
      }
    };

cc @ryantrem

From what I understood, you wont render through a canvas but through a viewEngine where you will set the camera to render.

The samples of the readme is explaining this part here: BabylonReactNative/README.md at master · BabylonJS/BabylonReactNative · GitHub

Hey @Alexc - For the most part, WebAPI does not exist in React Native, so code that you want to share between the web and React Native should largely only use the Babylon.js API. Babylon provides abstractions for a lot of functionality, where on the web it uses WebAPI under the hood and in React Native it uses custom native implementations.

For example, Babylon.js has a DynamicTexture class that exposes a canvas interface for drawing text/images/shapes/etc. This is partially implemented for Babylon Native (@Cedric can provide more details as needed), but conceptually if you use this class for canvas type operations, that code should work both in the web and in React Native.

That said, from the code you shared above, it looks like the only thing you do with the canvas is get the width/height off of it. I think a different thing you can do that is equivalent but will work in both the web and React Native is to use another Babylon.js abstraction: engine.getRenderWidth() and engine.getRenderHeight().

Another example of an abstraction that Babylon.js provides is scene.onPointerDown (and related functions) for dealing with input. I see you are using this, which is good, but this functionality is not quite finished in Babylon Native. @PolygonalSun has been working on this and is very close to having it all working (I think he’s aiming to have everything in place in the coming weeks). Until then, that part of the code won’t work in React Native. You can either wait for that support to be added soon, or you could switch to use the lower level abstraction (DeviceSourceManager) which does work in React Native today.

1 Like

Thank you all so much for the fast responses and advice. I took the suggested feedback:

  1. changed canvas height/wdith size to a 500x500 for testing
  2. set up useEngine scene
const engine: Engine = useEngine();

var createScene = function () {
      var scene = new Scene(engine);
      var dome = new PhotoDome(
        'testdome',
        // response.data.signed_url,
        require('./images/testimages5.jpg'),
        {
          resolution: 32,
          size: 1000,
          // useDirectMapping = true
        },
        scene,
      );
      // dome.fovMultiplier = 2.0
      addSphericalPanningCameraToScene(scene);

      return scene;
    };

    // call the createScene function
    var scene = createScene();

    // run the render loop
    engine.runRenderLoop(function () {
      scene.render();
    });
  1. commented out pointer down/up until @PolygonalSun changes are out.

The use engine object is returning null right now, and I am getting an error when trying to access it.

 ERROR  TypeError: null is not an object (evaluating '_this._engine.scenes')

This error is located at:
    in PanoViewer (at TabThreeScreen.tsx:189)
  1. You need to create the scene inside a useEffect that checked whether the engine is non-null (useEngine creates the engine instance synchronously). See the example in the docs: @babylonjs/react-native - npm
  2. You shouldn’t manually run a render loop as useEngine does this automatically under the hood.
1 Like

This is the rest of the code. I added the useEffect wrapper similar to the tutorial. The useEngine still returns a null object and breaks the command run when the null object is produced.

This is in my main component function.
const engine = useEngine();

The following is within the useEffect after render.

useEffect(() => {


    // load the 3D engine
    console.log(engine)
    if (engine) {
  
      var scene = new Scene(engine);
      var dome = new PhotoDome(
        'testdome',
        // response.data.signed_url,
        require('./images/testimages5.jpg'),
        {
          resolution: 32,
          size: 1000,
          // useDirectMapping = true
        },
        scene,
      );

   
      addSphericalPanningCameraToScene(scene);
      
    }
  }, [engine]);

I don’t see anything obviously wrong with that last snippet. If React Native isn’t giving you the call stack in a yellow/red box, then I’d recommend commenting out blocks of code iteratively to find the exact line in your code that is resulting in the error.

Hi @Alexc just checking in, are you still having issues?