DEV Community

Cover image for How to Use Three.js And React to Render a 3D Model of Your Self
Nourdine Labib
Nourdine Labib

Posted on • Edited on • Originally published at blog.nourdinedev.com

How to Use Three.js And React to Render a 3D Model of Your Self

In this article, we’ll cover how to render and configure 3D assets created in a 3D software program like Blender or Maya in a React project using react-three-fiber. By the end of this article, you’ll be able to render 3D models (gltf / glb) on your website.

Get a 3D model of yourself

To get a customized 3D model, We well use Ready Player Me, a free-to-use 3D avatar creator from Wolf3D that allows anyone to create their own digital representation in a matter of minutes, no 3D modeling experience required. All you need to do is take a quick selfie and wait as the program automatically generates a custom 3D avatar based on your likeness.

You’re then free to make your own adjustments to the character using an okay range of hairstyles, skin tones, facial features, clothing options, and other customizable attributes.

After signing in to Ready Player Me, You need to follow the steps below and you good to go.

Choose a body type

Choose body type

Upload a photo of yourself

Upload a photo of yourself

Customize your look

Customize your look

Download your model

Download your model

Render the model in React

To render the model in our React app, We will use react-three-fiber a React renderer for Threejs.

Setting up the project

First, let’s create a new React project with Create React App:

npx create-react-app my-3d-model
#or
yarn create react-app my-3d-model
Enter fullscreen mode Exit fullscreen mode

Afterwards, install @react-three/fiber and @react-three/drei with the command below:

npm install three @react-three/fiber @react-three/drei
#or
yarn add three @react-three/fiber @react-three/drei
Enter fullscreen mode Exit fullscreen mode

Converting the model into a reusable React component

Once you’re done, go ahead and run the command below to create a javascript file using gltfjsx that plots out all of the assets contents in the format of a React functional component.

npx gltfjsx model.glb
Enter fullscreen mode Exit fullscreen mode

The file’s content will look similar to the following code:

import React, { useRef } from "react";
import { useGLTF } from "@react-three/drei";

export default function Model({ ...props }) {
  const group = useRef();
  const { nodes, materials } = useGLTF("/model.glb");
  return (
    <group ref={group} {...props} dispose={null}>
      <primitive object={nodes.Hips} />
      <skinnedMesh
        geometry={nodes.Wolf3D_Body.geometry}
        material={materials.Wolf3D_Body}
        skeleton={nodes.Wolf3D_Body.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Glasses.geometry}
        material={materials.Wolf3D_Glasses}
        skeleton={nodes.Wolf3D_Glasses.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Hair.geometry}
        material={materials.Wolf3D_Hair}
        skeleton={nodes.Wolf3D_Hair.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Outfit_Bottom.geometry}
        material={materials.Wolf3D_Outfit_Bottom}
        skeleton={nodes.Wolf3D_Outfit_Bottom.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Outfit_Footwear.geometry}
        material={materials.Wolf3D_Outfit_Footwear}
        skeleton={nodes.Wolf3D_Outfit_Footwear.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Outfit_Top.geometry}
        material={materials.Wolf3D_Outfit_Top}
        skeleton={nodes.Wolf3D_Outfit_Top.skeleton}
      />
      <skinnedMesh
        name="EyeLeft"
        geometry={nodes.EyeLeft.geometry}
        material={nodes.EyeLeft.material}
        skeleton={nodes.EyeLeft.skeleton}
        morphTargetDictionary={nodes.EyeLeft.morphTargetDictionary}
        morphTargetInfluences={nodes.EyeLeft.morphTargetInfluences}
      />
      <skinnedMesh
        name="EyeRight"
        geometry={nodes.EyeRight.geometry}
        material={nodes.EyeRight.material}
        skeleton={nodes.EyeRight.skeleton}
        morphTargetDictionary={nodes.EyeRight.morphTargetDictionary}
        morphTargetInfluences={nodes.EyeRight.morphTargetInfluences}
      />
      <skinnedMesh
        name="Wolf3D_Head"
        geometry={nodes.Wolf3D_Head.geometry}
        material={materials.Wolf3D_Skin}
        skeleton={nodes.Wolf3D_Head.skeleton}
        morphTargetDictionary={nodes.Wolf3D_Head.morphTargetDictionary}
        morphTargetInfluences={nodes.Wolf3D_Head.morphTargetInfluences}
      />
      <skinnedMesh
        name="Wolf3D_Teeth"
        geometry={nodes.Wolf3D_Teeth.geometry}
        material={materials.Wolf3D_Teeth}
        skeleton={nodes.Wolf3D_Teeth.skeleton}
        morphTargetDictionary={nodes.Wolf3D_Teeth.morphTargetDictionary}
        morphTargetInfluences={nodes.Wolf3D_Teeth.morphTargetInfluences}
      />
    </group>
  );
}

useGLTF.preload("/model.glb");
Enter fullscreen mode Exit fullscreen mode

creating the scene

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

export default function App() {
  return (
    <Canvas
      camera={{ position: [2, 0, 12.25], fov: 15 }}
      style={{
        backgroundColor: "#111a21",
        width: "100vw",
        height: "100vh",
      }}
    >
      <ambientLight intensity={1.25} />
      <ambientLight intensity={0.1} />
      <directionalLight intensity={0.4} />
      <Suspense fallback={null}>// your model here</Suspense>
      <OrbitControls />
    </Canvas>
  );
}
Enter fullscreen mode Exit fullscreen mode

Adding the model to the scene

First add the model (glb file) to the public folder, For the generated javascript file by gltfjsx you can add it either to the src folder or to the components folder.

import React, { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import Model from "./Model"; /* highlight-line */

export default function App() {
  return (
    <Canvas
      camera={{ position: [2, 0, 12.25], fov: 15 }}
      style={{
        backgroundColor: "#111a21",
        width: "100vw",
        height: "100vh",
      }}
    >
      <ambientLight intensity={1.25} />
      <ambientLight intensity={0.1} />
      <directionalLight intensity={0.4} />
      <Suspense fallback={null}>
        <Model position={[0.025, -0.9, 0]} /> /* highlight-line */
      </Suspense>
      <OrbitControls />
    </Canvas>
  );
}
Enter fullscreen mode Exit fullscreen mode
body {
   margin: 0;
   display: flex;
   align-items: center;
   justify-content: center;
   height: 100vh;
}
Enter fullscreen mode Exit fullscreen mode

result:

https://codesandbox.io/s/bold-wing-9w9n3i?file=/src/Model.js

Add animations to the model

To be able to add animations to your 3D model, You need to have blender installed in your machine.

Import the model to blender

Blender is the free and open source 3D creation suite. It supports the entirety of the 3D pipeline modeling, rigging, animation, simulation, rendering, compositing and motion tracking, even video editing and game creation.
learn more

Create a new blender project

new blender project

Clear the scene from all the objects

blank blender project

Import the glb file to blender

blender import glb file

blender import glb file

Select your model and click Import glTF 2.0

blender import glb file

Convert the model to fbx format

Before adding any animations to our model we need first to convert it into a FBX format.

Select the model

To select your 3D model in blender you only need to click on the letter a or you can use the mouse to do so.

blender model selection

Export the model as FBX

blender model export

blender model export config

Make sure to set Path Mode to Copy, and check the Embed textures option.

Adding animations with mixamo

Mixamo is a free online service for automatically rigging and animating 3-D characters. It was developed by Mixamo Incorporated, which was purchased by Adobe in 2015. Mixamo allows users to upload FBX, OBJ, or Zip files, and then the website attempts to automatically rig the character in under two minutes. The rigging process works best with humanoid characters.

Upload the model to mixamo

mixamo model upload

mixamo model upload

Select an animation and download the animated model

mixamo model animations

mixamo model download

Convert the animated model back to glb format

To use the model in our React app we need to change it back to glb format.

Import the animated model to blender

Import the animated model to blender

Export the animated model as glb

Export the animated model as glb

Rendering the animated model in React

In the public folder replace the model.glb file with the animated model, and add the changes below to src/Model.js file.

import React, { useRef, useEffect } from "react"; /* highlight-line */
import { useGLTF, useAnimations } from "@react-three/drei"; /* highlight-line */

export default function Model({ ...props }) {
  const group = useRef();
  const { nodes, materials, animations } = useGLTF("/model.glb");

  const { actions } = useAnimations(animations, group); /* highlight-line */

  // 'Armature|mixamo.com|Layer0' is the name of the animation we need to run.
  // console.log(actions);

  useEffect(() => {
    /* highlight-line */
    actions["Armature|mixamo.com|Layer0"].play(); /* highlight-line */
  }); /* highlight-line */

  return (
    <group ref={group} {...props} dispose={null}>
      <primitive object={nodes.Hips} />
      <skinnedMesh
        geometry={nodes.Wolf3D_Body.geometry}
        material={materials.Wolf3D_Body}
        skeleton={nodes.Wolf3D_Body.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Glasses.geometry}
        material={materials.Wolf3D_Glasses}
        skeleton={nodes.Wolf3D_Glasses.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Hair.geometry}
        material={materials.Wolf3D_Hair}
        skeleton={nodes.Wolf3D_Hair.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Outfit_Bottom.geometry}
        material={materials.Wolf3D_Outfit_Bottom}
        skeleton={nodes.Wolf3D_Outfit_Bottom.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Outfit_Footwear.geometry}
        material={materials.Wolf3D_Outfit_Footwear}
        skeleton={nodes.Wolf3D_Outfit_Footwear.skeleton}
      />
      <skinnedMesh
        geometry={nodes.Wolf3D_Outfit_Top.geometry}
        material={materials.Wolf3D_Outfit_Top}
        skeleton={nodes.Wolf3D_Outfit_Top.skeleton}
      />
      <skinnedMesh
        name="EyeLeft"
        geometry={nodes.EyeLeft.geometry}
        material={nodes.EyeLeft.material}
        skeleton={nodes.EyeLeft.skeleton}
        morphTargetDictionary={nodes.EyeLeft.morphTargetDictionary}
        morphTargetInfluences={nodes.EyeLeft.morphTargetInfluences}
      />
      <skinnedMesh
        name="EyeRight"
        geometry={nodes.EyeRight.geometry}
        material={nodes.EyeRight.material}
        skeleton={nodes.EyeRight.skeleton}
        morphTargetDictionary={nodes.EyeRight.morphTargetDictionary}
        morphTargetInfluences={nodes.EyeRight.morphTargetInfluences}
      />
      <skinnedMesh
        name="Wolf3D_Head"
        geometry={nodes.Wolf3D_Head.geometry}
        material={materials.Wolf3D_Skin}
        skeleton={nodes.Wolf3D_Head.skeleton}
        morphTargetDictionary={nodes.Wolf3D_Head.morphTargetDictionary}
        morphTargetInfluences={nodes.Wolf3D_Head.morphTargetInfluences}
      />
      <skinnedMesh
        name="Wolf3D_Teeth"
        geometry={nodes.Wolf3D_Teeth.geometry}
        material={materials.Wolf3D_Teeth}
        skeleton={nodes.Wolf3D_Teeth.skeleton}
        morphTargetDictionary={nodes.Wolf3D_Teeth.morphTargetDictionary}
        morphTargetInfluences={nodes.Wolf3D_Teeth.morphTargetInfluences}
      />
    </group>
  );
}

useGLTF.preload("/model.glb");
Enter fullscreen mode Exit fullscreen mode

result:

https://codesandbox.io/s/3d-model-animation-d41e9u?file=/src/Model.js:271-281


Also read:
React Best Practices - Ways To Write Better Code in 2022
10 JavaScript One-Liners - Ways To Boost Your Productivity


My blog
My website
Find me on Upwork
Find me on twitter
Find me on linkedin
Find me on github

Top comments (52)

Collapse
 
martinlundqvist profile image
MartinLundqvist • Edited

Nice one! Only issue I have is that I lose the textures when exporting the fbx animation to glb from Blender in the final step.

  • I can see the textures in Mixamo and in Blender, so they are "there" so to speak.
  • Also, if I just use the ReadyPlayerMe model without adding any animations to it, then the textures also show.

Any advice?

Collapse
 
nourdinedev profile image
Nourdine Labib

When you export the model as glb make sure you use this configurations.


I hope this fixes the problem.

Collapse
 
martinlundqvist profile image
MartinLundqvist

Now only one problem remains.. the "position" property of the Model component doesn't seem to affect the position once the model is animating. Whenever I reload the app, the position will go back to the bottom of the model being in the center of the canvas.

Thread Thread
 
nourdinedev profile image
Nourdine Labib

I'm sorry I didn't understand what problem you are having with the Model position, If you still didn't figure it out, provide me with some more feedback, and I will try my best to help.

Thread Thread
 
martinlundqvist profile image
MartinLundqvist • Edited

The "Model position={[0.2, -0.9, 0]}" positions the model as it should, when the model is not animating. However, as soon as I replace with the animated model, the position is somehow overridden and becomes [0,0,0]. I can see this behavior when I console.log(group.current.position).

Thread Thread
 
martinlundqvist profile image
MartinLundqvist

Problem solved. By wrapping the group in a new parent group, and setting the position on the parent - I could solve the problem. The animation clearly reset the position of its group.

Thread Thread
 
nourdinedev profile image
Nourdine Labib

I never had a problem like that before, glad you solved it.

Collapse
 
martinlundqvist profile image
MartinLundqvist

Indeed it did. The only config that differed was "Selected Objects". When I checked this option, it started working.

Thanks!

Collapse
 
jhonrios85 profile image
jhonrios85 • Edited

I had the same problem, when I exported from Blender to FBX I lost the textures, what I did is:

  • Export the to FBX using blender
  • Add the animation in Mixamo and export the "Animated Version FBX" (Doesn't matter that the FBX have not textures, this is only to generate the animation)
  • Import the "Animated Version FBX" and the original GLB (with textures) to Blender
  • And follow these instructions to copy the animation from "Animated Version FBX" to "Original GLB with textures" gachokistudios.com/how-to-copy-ani...
  • Once you copy the animation, you can delete the "Animated Version FBX" from blender and export the original one (animated) to GLB and continue with the tutorial to include it to the project
Collapse
 
irennalumbuun profile image
Irenna Nicole Lumbuun

Hope this is not too late; I got a similar error earlier:
You wanna uncomment the line console.log(action) and check the logs. It should show an object, mine looks something like this:

Armature.001|mixamo.com|Layer0: (...)
Armature.001|mixamo.com|Layer0.001: (...)
Enter fullscreen mode Exit fullscreen mode

Then you wanna change this line of code to match it

actions['Armature.001|mixamo.com|Layer0'].play(); 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
itzcookiie profile image
Mayowa

Awesome awesome awesome! Ty for making this post was really helpful for me as I plan on making a personal portfolio and this will go a long way.

Just in case this helps anyone, I also encountered issues with the textures for the head not showing up in the final model export. This is how I fixed:

  • Downgrade blender to version 2.93.0
  • Follow all the steps up until where you import the exported .fbx asset file from mixamo (so up until the Import the animated model to blender step under Convert the animated model back to glb format section)
  • Click on the head of the exported .fbx asset file from mixamo, then click on the Material Properties tab in the bottom right. It should have this icon: Image description
  • A UI like so should open up Image description
  • Click on the following dropdown to select a material for your head Image description
  • Search for Wolf3D_Skin
  • Select that and you should see the correct head texture
  • Follow the remaining steps from Rendering the animated model in React
Collapse
 
tythos profile image
Brian Kirkpatrick

Cool stuff! I'll bet you could cut React out of this entirely and just use (say) ColladaLoader with THREE.js to port the same content.

Collapse
 
nourdinedev profile image
Nourdine Labib • Edited

Thanks!, I had used GLTFLoader outside of React but I never used the ColladaLoader, Over all if you have a collada model (.dae) you need to use the ColladaLoader, or if you have a model with other format you need to use a loader that supports the file type/format.

You are very much right, you can load any model with the power of Three.js only, thank you for pointing that out.

Collapse
 
leminhung profile image
Lê Minh Hưng • Edited

i just follow your steps. When i compile "yarn start" then i receive this animation (character has no color). I don't know why. Can you please help fix this!

Collapse
 
nourdinedev profile image
Nourdine Labib • Edited

can you provide me with a link of a public Github repo that have your Code, and I will see if I can help.

Collapse
 
leminhung profile image
Lê Minh Hưng • Edited

Repo: github.com/leminhung/my-3d-model
This is my public github repo, Can you please clone repo and run this app to see the problems with color of animations.
Thank you so much!

Thread Thread
 
nourdinedev profile image
Nourdine Labib • Edited

Apparently when you added the animation to the model, the names of the materials changed, you need to generate a new Model.js file using gltfjsx.

npx gltfjsx model-with-animations.glb
Enter fullscreen mode Exit fullscreen mode

or you can use the code I did generate using gltfjsx.

import React, { useRef, useEffect } from 'react';
import { useGLTF, useAnimations } from '@react-three/drei';

export default function Model({ ...props }) {
   const group = useRef();
   const { nodes, materials, animations } = useGLTF(
      '/model-with-animations.glb'
   );
   const { actions } = useAnimations(animations, group);

   useEffect(() => {
      actions['Armature|mixamo.com|Layer0'].play();
   });
   return (
      <group ref={group} {...props} dispose={null}>
         <primitive object={nodes.Hips} />
         <skinnedMesh
            geometry={nodes.Wolf3D_Body.geometry}
            material={materials['Wolf3D_Body.001']}
            skeleton={nodes.Wolf3D_Body.skeleton}
         />
         <skinnedMesh
            geometry={nodes.Wolf3D_Hair.geometry}
            material={materials['Wolf3D_Hair.001']}
            skeleton={nodes.Wolf3D_Hair.skeleton}
         />
         <skinnedMesh
            geometry={nodes.Wolf3D_Outfit_Bottom.geometry}
            material={materials['Wolf3D_Outfit_Bottom.001']}
            skeleton={nodes.Wolf3D_Outfit_Bottom.skeleton}
         />
         <skinnedMesh
            geometry={nodes.Wolf3D_Outfit_Footwear.geometry}
            material={materials['Wolf3D_Outfit_Footwear.001']}
            skeleton={nodes.Wolf3D_Outfit_Footwear.skeleton}
         />
         <skinnedMesh
            geometry={nodes.Wolf3D_Outfit_Top.geometry}
            material={materials['Wolf3D_Outfit_Top.001']}
            skeleton={nodes.Wolf3D_Outfit_Top.skeleton}
         />
         <skinnedMesh
            name="EyeLeft"
            geometry={nodes.EyeLeft.geometry}
            material={nodes.EyeLeft.material}
            skeleton={nodes.EyeLeft.skeleton}
            morphTargetDictionary={nodes.EyeLeft.morphTargetDictionary}
            morphTargetInfluences={nodes.EyeLeft.morphTargetInfluences}
         />
         <skinnedMesh
            name="EyeRight"
            geometry={nodes.EyeRight.geometry}
            material={nodes.EyeRight.material}
            skeleton={nodes.EyeRight.skeleton}
            morphTargetDictionary={nodes.EyeRight.morphTargetDictionary}
            morphTargetInfluences={nodes.EyeRight.morphTargetInfluences}
         />
         <skinnedMesh
            name="Wolf3D_Head"
            geometry={nodes.Wolf3D_Head.geometry}
            material={materials['Wolf3D_Skin.001']}
            skeleton={nodes.Wolf3D_Head.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Head.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Head.morphTargetInfluences}
         />
         <skinnedMesh
            name="Wolf3D_Teeth"
            geometry={nodes.Wolf3D_Teeth.geometry}
            material={materials['Wolf3D_Teeth.001']}
            skeleton={nodes.Wolf3D_Teeth.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Teeth.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Teeth.morphTargetInfluences}
         />
      </group>
   );
}

useGLTF.preload('/model-with-animations.glb');
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
leminhung profile image
Lê Minh Hưng

Ohh, problem was resolved. Thank you!

Collapse
 
nihaltuda profile image
nihal-tuda

Uncaught Could not load ../public/breakDance.glb: Unexpected token < in JSON at position 0.
I'm getting this error and can't find a way to parse it as html. Need help!
I guess there is some issue with useGLTF function cuz when i inserted my avatar link it worked but it wasn't working when i used the path of .glb file in the useGLTF().

Collapse
 
nourdinedev profile image
Nourdine Labib

can you upload it into a public git repo and give me the link so I can see what the problem is, or you can see my shared code on codesandbox at codesandbox.io/s/3d-model-animatio... and try to find if you missed any thing.

Collapse
 
maqi1520 profile image
Alex Ma

Thanks for sharing, May I translate this article into Chinese?

Collapse
 
nourdinedev profile image
Nourdine Labib

Yeah no problem, you can translate the article.

Collapse
 
maqi1520 profile image
Alex Ma
Collapse
 
robsonmuniz16 profile image
Robson Muniz

Nice content, thanks for sharing it!

Collapse
 
jzombie profile image
jzombie

This is radical! Thanks for sharing.

Collapse
 
nourdinedev profile image
Nourdine Labib

Thank you, I appreciate that.

Collapse
 
luisnomad profile image
Luis Serrano 🇪🇺

Genius. Thank you for this! It's simply awesome.

Collapse
 
nourdinedev profile image
Nourdine Labib

Thank you, I appreciate that.