React-Native and Three.js

thechaudhrysab

Chaudhry Talha 🇵🇸

Posted on October 1, 2024

React-Native and Three.js

Create a new react-native CLI project:

npx react-native init Learn3DApp
Enter fullscreen mode Exit fullscreen mode

It's preferred that you have real devices to see the output. The 3D model will be able to run on an Android emulator, but there are issues with the iOS simulator. The screenshots you'll see in this iOS project are all on a physical device.

Let's install all the required packages:

yarn add three @react-three/fiber
Enter fullscreen mode Exit fullscreen mode

Then run the command below:

npx install-expo-modules@latest
Enter fullscreen mode Exit fullscreen mode

It'll ask you a maximum of two questions. Below is how I have answered each:

  • install-expo-modules @0.10.2 Ok to proceed? (y) y
  • Install the Expo CLI integration? … no

We'll manually install the Expo CLI in a later step.

Below is the screenshot of my install-expo-modules installation:

install-expo-modules

You'll need expo-cli installed globally to install the package below. If you don't have expo-cli use npm install -g expo-cli to install it globally on your system first and then run the command below in your project folder.

expo install expo-gl
Enter fullscreen mode Exit fullscreen mode

The above are the basic packages needed to load a 3D model. But we'll install one more supporting package named Drei, that'll help you with three.js all provided by the same https://docs.pmnd.rs/.

  • Drei provides us with helpers that make it easy to work with 3D. Without Drei, you’ll need to manually set up and configure many common 3D features (e.g., controls, loaders, environment maps), which can be time-consuming and error-prone.

To install the Drei package do:

yarn add @react-three/drei
Enter fullscreen mode Exit fullscreen mode

After all the installations below is how the dependencies look like in my project now: (expo was installed automatically by install-expo-modules)

react-native package dependencies

Next, go to metro.config.js and add this code to it:

  resolver: {
    sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'cjs', 'mjs'],
    assetExts: ['glb', 'gltf', 'png', 'jpg'],
  },
Enter fullscreen mode Exit fullscreen mode

metro.config.js file

For Android just do cd android && ./gradlew clean and for iOS do cd ios && pod install at this point.

We'll now run and test if the expo is linked successfully and only then we'll start working with 3D stuff. As expo-gl is a very important package to be used when trying to run 3D on mobile devices in a react-native environment.

Open App.tsx and remove everything and just have:

import React from 'react';
import Constants from 'expo-constants';

console.log(Constants.systemFonts);

export default function App() {
  return (
    <></>
  );
}
Enter fullscreen mode Exit fullscreen mode

Run the project yarn start --reset-cache. The console.log(Constants.systemFonts); should print this for you in the terminal, ensuring a successful install:

Constants.systemFonts

make sure you see a blank screen without any errors for both iOS and Android.


Now that the boring stuff is out of the way, let's start 3D work. You'll need a 3D model that you'll be loading in the app.

There are many websites where you can find 3D models for free. Just like how images have JPG, PNG, and many different formats, the 3D file format you want to download can be either .gltf or .glb.

I know https://www.cgtrader.com/ which is a nice website to find 3D models in .gltf or .glb formats, you can apply filters to download free models.

I'm going to use https://poly.pizza/bundle/Farm-Animal-Pack-1kUvRTPLzT click on Download GLTF and it'll give you .glb files of different animals.

GLB Format

In the project create src folder, and inside create a models folder. Copy the Horse.glb or any animal file in the models folder.

Copied the horse GLB file in project

Next, we need to have a JSX of the GLB file as well. To do that go to https://gltf.pmnd.rs/ and drop the GLB file and it'll show you a preview with JSX code as shown below:

GLB to JSX

Copy the code and create a new file named Horse.tsx in the Models folder and paste it there.

Horse.tsx

To solve the errors we'll do the following:

  • Change from '@react-three/drei' to from '@react-three/drei/native'
  • Remove props and {...props}
  • Remove the last line in code useGLTF.preload('/Horse.glb');
  • Give file path with require in const { nodes, materials, animations } = useGLTF(...
  • Made the Model function export default and it's name to Horse from Model

The screenshot below is how my full Horse.tsx is looking now:

Horse.tsx after changes

Don't worry about the nodes, materials, animations red underline as these are just typescript things asking for specific types.

Next, open App.tsx and add the following code in it:

import React, { Suspense } from 'react';
import { Canvas } from '@react-three/fiber/native';
import { OrbitControls } from '@react-three/drei/native';

import { View } from 'react-native';
import Horse from './src/models/Horse';

const App = () => {
  const renderHorseCanvas = () => {
    return (
      <Canvas shadows>
        <directionalLight position={[5, 10, 15]} intensity={1} castShadow />
        <directionalLight position={[-10, 10, 15]} intensity={1} />
        <directionalLight position={[10, 10, 15]} intensity={1} />
        <Suspense fallback={null}>
          <Horse />
          <mesh
            receiveShadow
            rotation={[-Math.PI / 2, 0, 0]}
            position={[0, -1, 0]}>
            <planeGeometry args={[10, 10]} />
            <shadowMaterial opacity={0.5} />
          </mesh>
        </Suspense>
        <OrbitControls enableZoom={false} />
      </Canvas>
    );
  };

  return <View style={{ flex: 1 }}>{renderHorseCanvas()}</View>;
};

export default App;

Enter fullscreen mode Exit fullscreen mode

Re-run the project and you'll see the horse model being rendered.

First render of the horse

It seems like there is a divide between the shadow and the actual horse body. But it's nothing that a minor position repositioning cannot fix. Let's change the renderHorseCanvas code to the one below:

  const renderHorseCanvas = () => {
    return (
      <Canvas shadows>
       //... remaining code
          <Horse position={[0, -1, 0]} />
          //... remaining code
      </Canvas>
    );
  };
Enter fullscreen mode Exit fullscreen mode

Position fixed of the render

This is how you render a 3D using three js in your react-native app.

Bonus: Animations

The horse we've added is still and doesn't do anything. This glb file includes animations like Idle, Jump, Run, Walk, etc.

In Horse.tsx we had a line of code const { actions } = useAnimations(animations, group); which we'll use.

In Horse.tsx before the main return ( add this code:

//... remaining code
  const { actions, names } = useAnimations(animations, group);
  console.log('animations names: ', names);
  // animations names:  ["Armature|Death", "Armature|Idle", "Armature|Jump", "Armature|Run", "Armature|Walk", "Armature|WalkSlow"]
  useEffect(() => {
    // Play the "Idle" animation
    const idleAction = actions['Armature|WalkSlow'];
    if (idleAction) {
      idleAction
        .reset()
        .setLoop(LoopRepeat, Infinity) // Set loop mode and infinite repetitions
        .fadeIn(0.5)
        .play();
    }
    return () => {
      // Stop the "Idle" animation when the component is unmounted
      if (idleAction) idleAction.fadeOut(0.5);
    };
  }, [actions]);
//... remaining code
Enter fullscreen mode Exit fullscreen mode

Here is the full code:

Full code of animation

You can see all the available animations of any GLB or GLTF file with the console.log('animations names: ', names);. I have the player Armature|WalkSlow animation on LoopRepeat to give it a cool walking horse feel which you can see below, you can also add a different name from the animations names and see how they look.

Opening on an Android device I see this cool animation:

Animation preview

Extras:

Here are some things and additional resources that might be of help:


I was constantly getting this print in the terminal as soon as the 3D model renders:

LOG EXGL: gl.pixelStorei() doesn't support this parameter yet!

This is okay as it'll not impact anything for production like it'll not crash that's for sure. If your app is crashing there must be some other issue. I found a solution https://github.com/expo/expo/issues/11063 I would not say it's ideal but it works.

The solution states that in App.tsx or the file where you have the Canvas from @react-three/fiber add a prop to it as:

      <Canvas
        shadows={true}
        onCreated={state => {
          const _gl = state.gl.getContext();
          const pixelStorei = _gl.pixelStorei.bind(_gl);
          _gl.pixelStorei = function (...args) {
            const [parameter] = args;
            switch (parameter) {
              case _gl.UNPACK_FLIP_Y_WEBGL:
                return pixelStorei(...args);
            }
          };
        }}>
//... rest of the code
Enter fullscreen mode Exit fullscreen mode

Full code of gl.pixelStorei log

This magic potion will make the log go away.


Here is a resource to find 3D models for free: https://www.cgtrader.com/free-3d-models


Have a project and want me to look at, you can hire me on upwork: https://www.upwork.com/freelancers/~01e4982cf39ced7048

If you'd like to support you can do so via https://buymeacoffee.com/chaudhrytalha

I also write on medium so you can follow me there too: https://ibjects.medium.com/

💖 💪 🙅 🚩
thechaudhrysab
Chaudhry Talha 🇵🇸

Posted on October 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

React-Native and Three.js
javascript React-Native and Three.js

October 1, 2024