BabylonJS with React-Native Tutorial

Getting started with BabylonJS in react-native mobile app

Implementing BabylonJS in a react-native mobile app. Create sims like avatar creating mobile app with BabylonJS react native.

Chaudhry Talha 🇵🇸
17 min readJul 12, 2024

We’ll be doing the following three things in this article:

  1. Loading a basic 3D model in our react-native app
    This will set the base and will help you get the very basics of BabylonJS with React-Native.
  2. Play pause animations on button press
    Using a horse 3D model we’ll play with animation where we can make the horse walk and jump.
  3. Changing shirt and hairstyles of a 3D Model
    Just like how in Sims game or main avatar creating apps we’ll have a quick look on how to do it.

Installations:

My react is v18.2.0, and react-native is v0.74.2. For which the follow library versions are supported so install the following libraries:

yarn add @babylonjs/core@7.12.0
yarn add @babylonjs/react-native@1.8.5
yarn add @babylonjs/loaders@7.12.0
yarn add @babylonjs/react-native-iosandroid-0-71@1.8.5
yarn add react-native-permissions@4.1.5

The babylonjs/react-native-iosandroid-0–71 package is needed for babylonjs/react-native as it’s Babylon React Native iOS and Android Runtime. My react-native is v0.74.2, hence I’m installing the 0-71.

You can check which one you need to install by going to https://github.com/babylonjs/BabylonReactNative?tab=readme-ov-file#supported-versions.

Perquisites:

Basic understanding of 3D from both designing and development POV is good to have.
For example: I had basic idea of things like sphere, cylinder, squares etc are the shapes which are used to bend and create a shape in 3D say I’m making a rugby ball I’ll take a sphere and modify it by transforming it’s shape and other properties. If I have to give it the same look as a read rugby ball I have to add some material to it. This was just what was in my mind about 3D and I have seen people working on softwares like Blender etc. So, no in-depth knowledge is required.

BabylonJS basics is a must as I’ll not be getting into the details of say what BabylonJS engine is etc, because this tutorial is focused on implementing a 3D model with react-native. I recommend watching this https://www.youtube.com/watch?v=e6EkrLr8g_o as it’ll cover some basics before starting this tutorial.

BabylonJS React Native

If you’re not a 3D designer, you’ll need to work with a 3D designer so that they can export the correct file format of the 3D file which you’re going to then import in your react-native project. There is a section at the end titled Finding free 3D models:, where you can find links to websites for free .gltf files.

We’ll be using the file format .gltf which is widely used as there are others like .glb or .obj etc. The .gltf is a JSON structured file which is very easy to read or find information like what materials, animations etc are available.

If you have a .gltf file you can preview it online too using platforms like: https://gltf-viewer.donmccurdy.com/.

We’ll upload the .gltf file to the cloud or any CDN and get a URL that ends in .gltf (Recommended way), I’ve uploaded on cloud and below are the links of the two main GLTF files I’ll be using in this tutorial:

  const basicGLTFURL = "https://raw.githubusercontent.com/thechaudharysab/babylonjspoc/main/src/assets/Client.gltf";
const horseGLTFURL = "https://raw.githubusercontent.com/thechaudharysab/babylonjspoc/main/src/assets/Horse.gltf";

If I view all of the 3D models .gltf files I’m going to use in this project on the gltf-viewer, I get these and each starts with the default first animation they are assigned.

App Structure:

I’ve a single screen app, which I’m going to use App.tsx as my main screen. I’ve created src/assets folder to store all assets files and below is how my App.tsx looks initially:

//... all required imports

function App(): React.JSX.Element {

const basicGLTFURL = "https://raw.githubusercontent.com/thechaudharysab/babylonjspoc/main/src/assets/Client.gltf";

return (
<View style={styles.container}>
{/* ... UI Code Coming Soon ... */}
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
});

export default App;

Loading a basic 3D model in our react-native app

Let’s first load our basicGLTFURL. Add the following variables and imports in App.tsx screen:

//... Other imports
import { useEngine } from '@babylonjs/react-native';
import { Camera, Scene } from "@babylonjs/core";
import '@babylonjs/loaders/glTF';

function App(): React.JSX.Element {

const engine = useEngine();
const [scene, setScene] = useState<Scene>();
const [camera, setCamera] = useState<Camera>();

//... Remaining code

In the code, we are importing a hook named useEngine, that’ll allow us to utilize the Babylon.js engine. Then from BabylonJS we have imported a Scene and a Camera, both of which are required to view a 3D object in BabylonJS where scene is the scene in which our 3D model will be loaded and camera will be the main camera that’ll display a part of that scene.

Next replace the {/* … UI Code Coming Soon … */} code comment with the <EngineView… line below:

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

//... Code before
return (
<View style={styles.container}>
<EngineView camera={camera} />
</View>
);

//... Remaining code

The EngineView with camera prop will load a view with BabylonJS engine inside.

Next we’ll create a function where we’ll load the .gltf file.

//... Other imports
import { ArcRotateCamera, Camera, Scene, SceneLoader, HemisphericLight, Vector3 } from "@babylonjs/core";

//... Code before
const renderbasicGLTF = () => {
SceneLoader.LoadAsync(basicGLTFURL, undefined, engine).then((loadedScene) => {
if (loadedScene) {
setScene(loadedScene);

// Light
const light = new HemisphericLight("light", new Vector3(0, 1, 0), loadedScene);
light.intensity = 0.7;

// Camera
const camera = new ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2, -6, new Vector3(0, 2, 0), loadedScene, true);
camera.attachControl(true);
setCamera(camera);

} else {
console.error("Error loading loadedScene.");
}
}).catch((error) => {
console.error("Error loading scene: ", error);
});
};

//... Remaining code

In the code above, we’ve use SceneLoader from @babylonjs/core to LoadAsync the .gltf file. This returns a promise with the loadedScene. Which means now we have the scene with our 3D character inside a babylon engine. So, we added a Light and a Camera (You can see more about adjusting light and camera in a scene and what the properties are with them from BabylonJS docs and adjust for your model accordingly). The camera.attachControl(true); will enable gesture control on the 3d Model, means you can rotate zoom in/out on the 3D character.

As I need the. 3D model to be loaded as soon as I open the screen, so I’ll call the above function in a useEffect in App.tsx:

  useEffect(() => {
if (engine) {
renderbasicGLTF();
}
}, [engine]);

Below is the screenshot of my App.tsx code as of now:

Let’s run it and see. I’ll be running it on Android emulator, but before we do that, add Renderer maximum under OpenGL ES API under Emulator Settings > Advanced as show below:

Running the android app and I see this as output:

You can see a 3D character, with an animation and you can rotate and zoom in and out as well to some extent. This is how you can load a basic model in BabylonJS react-native.

Adding a horse 3D model and playing with animations

We’ll first add a horse 3D model and the file format we need if .gltf. We’ll do the following:

  • Import Horse 3D model (.gltf file)
  • Give horse a default Idle animation
  • Create buttons to trigger different animations
  • and some more…

Open App.tsx or any screen file where you want to render this 3D model, and add the following code in it:

//... Other imports

import { useEngine, EngineView } from '@babylonjs/react-native';
import { ArcRotateCamera, Camera, Scene, SceneLoader, Color4, AnimationGroup, Nullable } from "@babylonjs/core";
import '@babylonjs/loaders/glTF';

function App(): React.JSX.Element {

//... Other code like engine, scene and camera

// (1.)
const renderHorseGLTF = () => {
SceneLoader.LoadAsync(horseGLTFURL, undefined, engine).then((loadedScene: Scene) => {
if (loadedScene) {
setScene(loadedScene);
// (1.1) Camera & Light
loadedScene.createDefaultCameraOrLight(true, undefined, true);
(loadedScene.activeCamera as ArcRotateCamera).alpha += Math.PI;
setCamera(loadedScene.activeCamera!);
// (1.2) Getting an animation and starting it
const idleAnim = loadedScene.getAnimationGroupByName('Idle');

if (idleAnim) {
idleAnim.start(true, 1.0);
}

} else {
console.error("Error loading loadScene.");
}
}).catch((error) => {
console.error("Error loading scene: ", error);
});
}

useEffect(() => {
if (engine) {
renderHorseExperiments();
}
}, [engine]);

// (2.)
const startWalkAnimation = () => {
//... coming soon
};

const stopWalkAnimation = () => {
//... coming soon
};

const doJumpAnimation = () => {
//... coming soon
};

const randomizeBGColor = () => {
//... coming soon
};

return (
<View style={styles.container}>
<EngineView camera={camera} displayFrameRate={true} />
{/* (3.) */}
<View style={styles.absoluteView}>
<TouchableOpacity style={styles.buttonContainer} onPress={startWalkAnimation}>
<Text>Start Walking</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.buttonContainer} onPress={stopWalkAnimation}>
<Text>Stop Walking</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.buttonContainer} onPress={doJumpAnimation}>
<Text>Jump</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.buttonContainer} onPress={randomizeBGColor}>
<Text>Random BG Color</Text>
</TouchableOpacity>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
absoluteView: {
position: 'absolute',
bottom: '10%',
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'center',
},
buttonContainer: {
backgroundColor: '#61dafb',
borderWidth: 1,
padding: 4,
}
});

export default App;

The above code is the same as what we did in the basic example. I’ve added numbering in comments and below is each code element quick description:

(1.) Same as previously, we created a function named renderHorseGLTF to which we have provided a horseGLTFURL and the engine.
(1.1) Then instead of creating light and camera separately, I’ve used the createDefaultCameraOrLight which is good enough for my use-case. It’ll depend on your model and how you want to set it to check for what camera angle, ration and light it looks the best in.
(1.2) I’ve fetched an animation named Idle, from the horse .gltf file. Started it with loop true and speed 1.0.

The question of “How do I know there is an animation named Idle in the .gltf file?” Well there can be two ways to find that; one is opening a GLTF file in any editor and you can see it’s all JSON. There will be a lot of arrays in it like meshes, materials, and animations etc. If you see everything has a "name" property and for animations there is Idle, Idle_2, Idle_Headlow and many more as show below:

You can either find name like this or can add this function below:

  const logAnimationNames = (animationGroups: AnimationGroup[]) => {
console.log("---------------ANIMATIONS--------------------");
animationGroups.forEach(animationGroup => {
console.log(animationGroup.name);
});
}

Which you can call on loadedScene as logAnimationNames(loadedScene.animationGroups); as show in the commented line in the code. Also the output on the right shows all the animations the horse has.

(2.) I’ve added some empty functions which as clear by the name will do their job. The startWalkAnimation will change animation from Idle to Walk and stopWalkAnimation will set the animation back to Idle again. Just for fun we’ll have a doJumpAnimation which will make the horse do a jump and then go back to Idle or Walk animation whichever was active before pressing the Jump button. I’ve also added a randomizeBGColor function which will give our scene a random RGB color. You can also overwrite to have just one color. We’ll be adding code to them shortly.

(3.) Added the buttons UI to the screen.

With all the above if you run it then it should be able to see the horse rendered with the Idle animation wiggling his tail and you can rotate and/or zoom in and out (Check out the bonus section some more bits on the scene):

Now let’s add code to the buttons we have in our UI.

//.. All Other imports 
import { AnimationGroup, Nullable } from "@babylonjs/core"; // Just added these two types for this step
//.. All Other imports

//.. Other Code
const [currentAnimation, setCurrentAnimation] = useState<Nullable<AnimationGroup>>(null);
//.. Other Code

const startWalkAnimation = () => {
if (scene) {
const walkAnimation = scene.getAnimationGroupByName("Walk");
if (walkAnimation) {
if (currentAnimation) {
currentAnimation.stop();
}
walkAnimation.play(true);
setCurrentAnimation(walkAnimation);
} else {
console.warn("Animation not found:", walkAnimation);
}
}
};

const stopWalkAnimation = () => {
if (scene) {
const idleAnimation = scene.getAnimationGroupByName("Idle");
if (idleAnimation) {
if (currentAnimation) {
currentAnimation.stop();
}
idleAnimation.play(true);
setCurrentAnimation(idleAnimation);
} else {
console.warn("Animation not found:", idleAnimation);
}
}
};

const doJumpAnimation = () => {
if (scene) {
var jumpAnimation = scene.getAnimationGroupByName("Jump_toIdle");
if (jumpAnimation) {
jumpAnimation.play();
} else {
console.warn("Animation not found:", jumpAnimation);
}
}
};

const randomizeBGColor = () => {
if (scene) {
// Generate random color values (0-255)
const red = Math.floor(Math.random() * 256);
const green = Math.floor(Math.random() * 256);
const blue = Math.floor(Math.random() * 256);
const randomColor = new Color4(red / 255, green / 255, blue / 255, 1.0);

scene.clearColor = randomColor;
}
};

I added AnimationGroup andNullable just so that I can create a useState in typescript with the right type to store which animation is currently active. As I know the names of the animations I have with this Horse.gltf file I can pick which ones I want. So in the startWalkAnimation it’ll fetch the animation by name Walk. At first we stop() any currentAnimation and then play the new animation we want to i.e. walkAnimation. The main difference between play and start an animation as we did with Idle is we get some more parameters, but as we need to keep the default speed of the animation so I just used play with true on the param loop because I want the Walk animation to be on loop.

For stopWalkAnimation same thing as startWalkAnimation but instead the animation given is now Idle. This will loop as well.

For doJumpAnimation everything is the same but the animation I choose is Jump_toIdle. Notice how in play() there is no true which means just jump once and don’t loop. Meaning the animation will be run once and then it’ll be back to whatever animation there was last which in my case can be Walk or Idle.

Finally the randomizeBGColor is creating a random RGB color and then setting it as the scene color. scene is the loadedScene.

Let’s test it out!

So you can see how by just making animation loop I’ve created the above. This was just the way I’m handling animation for this particular example.

You may need more than just playing with loops as may be you need to run it frame-by-frame or other. Plus the horse animations are not very smooth, meaning if I’ve Walking animation and I press Jump it’ll not seems realistic as the horse takes a different position before jumping (Because I took horse model from internet so not a lot of room for me to fix that). It’ll really depend on your communication with the 3D designer who is putting together your whole 3D business in how to start and end animation so that each animation is kinds in a sync’d starting and ending-point. Like below are just some of the things you can do with animations from which we have only used start, play and stop.

🏆 Challenge: As I took the horse model from this free resource https://poly.pizza/bundle/Animated-Animal-Pack-ILAPXeUYiS where you can find other animals like Cow, Donkey, Bull, Wolf etc. Using the knowledge you have just obtained in the above example, create an app named MyPets that allows a user to select an animal and then create a virtual pet experience where they have to feed the pet, say my pet is Horse, and it’s time to feed the horse I can press the button and Eating animation of horse is played just like that add a many functions as you can for horse and if a user fails to do so play the Death animation available for horse.

Changing shirt and hairstyles of a 3D Model

Creating a sims like avatar creation experience can be done in many ways. The one I used is the very simple one (not ideal) but it works. I asked a 3D designer friend of mine to create a 3D model just for this example and give me the gltf file of it, so it’s not perfect but you’ll get the gist.

This is what I see when I use the gltf-viewer to view it:

This weird looking 3D model is because it is showing two hairstyles and two shirts that are overlapping each-other. Now we’ll use BabylonJS to hide and show one shirt and one hair at a time.

Let’s begin by importing the 3D model in our react-native as we did in the basic example:

//... Imports and other code

const simsCharacterGLTF = "https://raw.githubusercontent.com/thechaudharysab/babylonjspoc/main/src/assets/ibjects_test.gltf";

//.. Other code like engine, scene, camera states

const renderSimsGLTF = () => {
SceneLoader.LoadAsync(simsCharacterGLTF, undefined, engine).then((loadedScene) => {
if (loadedScene) {
setScene(loadedScene);

// Camera & Light
loadedScene.createDefaultCameraOrLight(true, undefined, true);
(loadedScene.activeCamera as ArcRotateCamera).alpha += Math.PI;
setCamera(loadedScene.activeCamera!);

// Scene Bg Color
const lightBlueColor = new Color4(208 / 255, 236 / 255, 255 / 255, 1.0);
loadedScene.clearColor = lightBlueColor;

// Adding more code here shortly...

} else {
console.error("Error loading loadedScene.");
}
}).catch((error) => {
console.error("Error loading scene: ", error);
});
}

useEffect(() => {
if (engine) {
renderSimsGLTF();
}
}, [engine]);

return (
<View style={styles.container}>
<EngineView
camera={camera}
displayFrameRate={false}
/>
</View>
);
}

//.. Remaining code

In the above code we haven’t done anything new but just simply loaded the model with a fixed background color. You can run and se the model being rendered:

Each shape be in a shirt, arm, shoe, hair, eye is all a Mesh. Just like how we did with animation, we can also see all the meshes that a .gltf ontains. So, let me extend logAnimationNames to logMeshesAndAnimationNames as below:

  const logMeshesAndAnimationNames = (meshes: AbstractMesh[], animationGroups: AnimationGroup[]) => {

console.log("-------------------MESHES--------------------");
meshes.forEach(mesh => {
console.log(mesh.name);
});

console.log("---------------ANIMATIONS--------------------");
animationGroups.forEach(animationGroup => {
console.log(animationGroup.name);
});
}

Now doing logMeshesAndAnimationNames(loadedScene.meshes, loadedScene.animationGroups); on the above gltf file we’ll get:

As you can see in this .gltf file there is only one animation but many meshes. I don’t care about all of them but as I know this information that Hair1, Hair2, Shirt1 and Shirt2 are the meshes I’ll be working with. I’m going to do these two things:

  • Set Hair1 and Shirt2 as default
  • Create a UI to change Hair or Shirt accordingly to what user presses

Where I added the comment // Adding more code here shortly… add the code below:

        loadedScene.meshes.forEach(mesh => {
if (mesh.name === 'Shirt1' || mesh.name === 'Hair2') {
mesh.isVisible = false; // Hide this mesh
}
});

I’ve looped through the meshes and where I could fine the name of mesh to be Shirt1 or Hair2 I’m hiding that mesh and the result will be a much cleaner 3D Model:

Now on to the UI. Same as I did with horse I’m going to add a few buttons and actions:

//... All code before

const onAvatarEditOptionSelect = (hair1: boolean, hair2: boolean, shirt1: boolean, shirt2: boolean) => {
if (scene) {
scene.meshes.forEach(mesh => {
if (mesh.name === 'Shirt1') {
mesh.isVisible = shirt1;
} else if (mesh.name === 'Shirt2') {
mesh.isVisible = shirt2;
} else if (mesh.name === 'Hair1') {
mesh.isVisible = hair1;
} else if (mesh.name === 'Hair2') {
mesh.isVisible = hair2;
}
});
}
};

const renderSimsUI = () => {
if (scene) {
return (
<View style={{ position: 'absolute', bottom: 0, backgroundColor: 'white', padding: 20 }}>
<Text style={{ fontWeight: 'bold' }}>Select Hair</Text>
<TouchableOpacity onPress={() => onAvatarEditOptionSelect(true, false, scene.getMeshByName('Shirt1')?.isVisible as boolean, scene.getMeshByName('Shirt2')?.isVisible as boolean)} style={{ marginVertical: 4 }}>
<Text>Hair 1</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => onAvatarEditOptionSelect(false, true, scene.getMeshByName('Shirt1')?.isVisible as boolean, scene.getMeshByName('Shirt2')?.isVisible as boolean)} style={{ marginVertical: 4 }}>
<Text>Hair 2</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => onAvatarEditOptionSelect(false, false, scene.getMeshByName('Shirt1')?.isVisible as boolean, scene.getMeshByName('Shirt2')?.isVisible as boolean)} style={{ marginVertical: 4 }}>
<Text>No Hair</Text>
</TouchableOpacity>
<Text style={{ fontWeight: 'bold' }}>Select Shirt</Text>
<TouchableOpacity onPress={() => onAvatarEditOptionSelect(scene.getMeshByName('Hair1')?.isVisible as boolean, scene.getMeshByName('Hair2')?.isVisible as boolean, true, false)} style={{ marginVertical: 4 }}>
<Text>Shirt 1</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => onAvatarEditOptionSelect(scene.getMeshByName('Hair1')?.isVisible as boolean, scene.getMeshByName('Hair2')?.isVisible as boolean, false, true)} style={{ marginVertical: 4 }}>
<Text>Shirt 2</Text>
</TouchableOpacity>
</View>
)
}
}

return (
<View style={styles.container}>
<EngineView
camera={camera}
displayFrameRate={false}
/>
{renderSimsUI()}

//... Remaining code

Same as before I added all the required buttons and created a function named onAvatarEditOptionSelect. Which takes four boolean values and this is how I’ll tell my code what to show or hide. Using a forEach loop we loop through the meshes and hide/show them accordingly.

Let’s test it…

This worked as expected. The avatar has a very smooth breathing Idle animation and it’s very easy to hide/show the meshes. So there you have it, this is a technique of how you can create a similar experience to what they do in avatar creation of the sims game.

Here is the github project that include code for all the three examples we did:

If you have any questions or are stuck on a step feel free to connect on LinkedIn and ask.

If you find this article useful do press 👏👏👏 so that others can also find this article.

Like to support? Scan the QR to go to my BuyMeACoffee profile:

https://www.buymeacoffee.com/chaudhrytalha
https://www.buymeacoffee.com/chaudhrytalha

What you can do next?

BabylonJS is a huge library and there is a lot of learning left. We’ve barely scratched the surface yet. Below are some of the ideas for you to practice:

  • Find the challenge I posted after the horse example, and create that app
  • Enhance the sims example, and move the camera when say a user is trying to change the hairstyles, zoom in on face if they are changing the shirt zoom out and show full body.

Extras

Something the model is not just simple one .gltf file, it can have things in separate folders like this walking man file I downloaded form the internet has these in the folder:

Make sure all the files are inside a folder as it is downloaded or given to you by the 3D designer. If you open the gltf file in a text editor you can see it reference to Scene.bin and the jpeg in the textures folder. Hence it is important to have the files and folder as it is.
Then you can load the gltf file as you have been doing i.e. SceneLoader.LoadAsync(walkingManGLTFURL, undefined, engine) and it’ll automatically know where to look for scene.bin and textures folder. Just to test, try to move say scene.bin to some other place and you’ll see the model will fail to load.

--

--