Create a new react-native CLI project:
npx react-native init Learn3DApp
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
Then run the command below:
npx install-expo-modules@latest
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:
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
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
After all the installations below is how the dependencies look like in my project now: (expo
was installed automatically by install-expo-modules
)
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'],
},
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 (
<></>
);
}
Run the project yarn start --reset-cache
. The console.log(Constants.systemFonts);
should print this for you in the terminal, ensuring a successful install:
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.
In the project create src
folder, and inside create a models
folder. Copy the Horse.glb
or any animal file in the models
folder.
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:
Copy the code and create a new file named Horse.tsx
in the Models folder and paste it there.
To solve the errors we'll do the following:
- Change
from '@react-three/drei'
tofrom '@react-three/drei/native
' - Remove
props
and{...props}
- Remove the last line in code
useGLTF.preload('/Horse.glb');
- Give file path with
require
inconst { nodes, materials, animations } = useGLTF(...
- Made the Model function
export default
and it's name toHorse
fromModel
The screenshot below is how my full Horse.tsx
is looking now:
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;
Re-run the project and you'll see the horse model being rendered.
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>
);
};
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
Here is the full code:
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:
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
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/
Top comments (0)