Customizing a 3D Model with React-Three-Fiber

Introduction

In search of opportunities for working with 3D in modern browsers, namely popular tools with good documentation and examples of solutions to popular problems, I came across “Three.js”. After studying the documentation, and witchcraft with the basic elements of the library, I came across an excellent tutorial “How to Build Color Customizer App” on codrops by Kyle Wetton.

Amazing Three.js tutorial by Kyle Wetton: here

To fully consolidate the material for beginners, I decided to implement Kyle’s application on the popular, modern, and just my favorite, the ReactJS library. With the addition of React-three-fiber, a library that combines ReactJS and ThreeJS.

Notice: This article omits the description of the development of interface elements in pure react, such as the choice of color and model element. Their implementation can be viewed in the result code.

TL;DR: Check FINAL result here.

Final Result

Few words about React Three Fiber

React Three Fiber projects examples

React Three Fiber (con. R3F) library helps developing dynamic scene graphs declaratively with re-usable components and add some structure and order to your codebase.These components react to state changes, are interactive out of the box and can tap into React’s infinite ecosystem. Also no limitation, everything that you can make with raw ThreeJS, will work in R3F. Rendering performance is up to threejs and the GPU, so R3F is not slower than raw ThreeJS.

React Three Fiber links:
-
Repo page
-
API page

Part 1: Setting all up

We will use create-react-app template for our purposes

npx create-react-app my-app
npm install three react-three-fiber

Create CanvasComponent which will render all 3D objects, all other HTML will be UI around this.

import {Canvas} from "react-three-fiber";<Canvas id="rtfCanvas" >
<Scene/>
</Canvas>

The Canvas object is your portal into Threejs. It renders Threejs elements, not DOM elements. We can configure default elements such as scene, raycaster, camera throught Canvas props, like

<Canvas id="rtfCanvas" camera={{fov: 50}}/>

But better create Scene component as child for cleaner project structure and development comfort. R3F`s Canvas has default embedded THREE.Scene object already, so in Scene we will just customize it for ourself. Set field of view value equal to 50 and let’s update the scene`s background color. Then add some fog of the same color off in the distance, this is going to help hide the edges of the floor once we add that in.

const Scene = ({newMaterialOpt}) => {
const {
scene, camera,
gl: {domElement}
} = useThree();

useEffect(() => {
scene.background = new THREE.Color(0xf1f1f1);
scene.fog = new THREE.Fog(0xf1f1f1, 20, 100);
camera.fov = 50;
}, [])

return (<> </>)
}

Add hemisphere and directional ligths to scene

<hemisphereLight
skycolor={new THREE.Color(0xffffff)}
groundColor={new THREE.Color(0xffffff)}
intensity={0.61}
position={[0, 50, 0]}
/>
<directionalLight
color={new THREE.Color(0xffffff)}
intensity={0.54}
position={[-8, 12, 8]}
castShadow
mapSize={new THREE.Vector2(1024, 1024)}
/>

Add controls by third-party OrbitControls.js. The extend function extends three-fiber's catalogue of JSX elements. Components added this way can then be referenced in the scene-graph using camel casing similar to other primitives.

import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'extend({OrbitControls})<orbitControls args={[camera, domElement]}/>

Started configuration of our Scene looks like this:

import {extend, useThree} from "react-three-fiber";
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'
import * as THREE from "three";
extend({OrbitControls})const Scene = ({newMaterialOpt}) => {
const {
scene, camera,
gl: {domElement}
} = useThree();

useEffect(() => {
scene.background = new THREE.Color(0xf1f1f1);
scene.fog = new THREE.Fog(0xf1f1f1, 20, 100);
camera.fov = 50;
}, [])

return (
<>
<orbitControls args={[camera, domElement]}/>
<hemisphereLight
skycolor={new THREE.Color(0xffffff)}
groundColor={new THREE.Color(0xffffff)}
intensity={0.61}
position={[0, 50, 0]}
/>
<directionalLight
color={new THREE.Color(0xffffff)}
intensity={0.54}
position={[-8, 12, 8]}
castShadow
/>
<Suspense fallback={null}>
<ChairMesh newMaterialOpt={newMaterialOpt}/>
<Floor/>
</Suspense>
</>
)
}

Part 2: Loading the model.

We’re going to add the component ChairMesh which will render chair model. Extension of model is .gltf , is an open format specification for efficient delivery and loading of 3D content. Assets may be provided either in JSON (.gltf) or binary (.glb) format.

Using GLTFLoader from threejs package we loading out model:

const ChairMesh = ({newMaterialOpt}) => {
const {scene: theModel} = useLoader(GLTFLoader, "chair.gltf");
const chair = useRef(theModel)

return <primitive
ref={chair}
object={theModel}
scale={[2, 2, 2]}
rotation={[0, Math.PI, 0]}
position={[0, -1, 0]}
receiveShadow
castShadow
>
</primitive>

}

After loading all information about model in theModel variable, use the <primitive/> to provide already created objects to scene. You can get current state of model object by ref of primitive. Our model looks like this now:

Now add some floor to cast shadow on it by the chair.

const Floor = () => {
return(
<mesh
position={[0,-1,0]}
receiveShadow
rotation={[-0.5 * Math.PI, 0, 0]}
>
<planeGeometry args={[5000,5000,1,1]} />
<meshPhongMaterial
color={new THREE.Color(0xeeeeee)}
shininess={0}
/>
</mesh>
)
}

Let’s take a look at what`s happening here.

The process of creating 3D primitive in R3F with jsx is slightly different, but the essence remains the same as creating with pure ThreeJs. Firstly, we create a mesh — representing triangular polygon mesh based objects, mainly composed of geometry and material. Secondly we create planeGeometry, which represents square, and make a new meshPhongMaterial and set a couple options. It’s color, and it’s shininess. Phong is great because you can adjust its reflectiveness and specular highlights.

We should now be looking at this:

Now for the shadows

Staring with renderer additional configs we create shadows on chair and floor. Enable shadowMap in GL renderer in our scene configuration section. Also manually add shadow mapSize to directionalLight throught scene children because light shadow settings as props do not work at the moment of article creation.

const directionalLight = scene.children[1];
directionalLight.shadow.mapSize = new THREE.Vector2(1024, 1024)
shadowMap.enabled = true;

Our loader function includes the ability to traverse the 3D model. So, head to our loader function and add this in below the theModel = gltf.scene; line. For each object in our 3D model (legs, cushions, etc), we’re going to enable the option to cast shadows, and to receive shadows. This traverse method will be used again later on.

theModel.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});

Our model materials still look incorrect, because for now we have materials brought in from design app (Blender in my case). But at least it has shadows :

Let`s fix materials problem by setting Phong material as default for model.

const INITIAL_MTL = new THREE.MeshPhongMaterial({
color: new THREE.Color(0xf1f1f1),
shininess: 10
});

We could just add this to our chair and be done with it, but some objects may need a specific color or texture on load, and we can’t just blanket the whole thing with the same base color, the way we’re going to do this is to add this array of objects under our initial material.

const INITIAL_MAP = [
{childID: "back", mtl: INITIAL_MTL},
{childID: "base", mtl: INITIAL_MTL},
{childID: "cushions", mtl: INITIAL_MTL},
{childID: "legs", mtl: INITIAL_MTL},
{childID: "supports", mtl: INITIAL_MTL}
];

We’re going to traverse through our 3D model again and use the childID to find different parts of the chair, and apply the material to it (set in the mtl property). These childID’s match the names we gave each object in Blender, if you read that section, consider yourself informed!

Below our loader function, let’s add a function that takes the model, the part of the object (type), and the material, and sets the material. We’re also going to add a new property to this part called nameID so that we can reference it later.

const initColor = (parent, type, mtl) => {
parent.traverse(o => {
if (o.isMesh && o.name.includes(type)) {
o.castShadow = true;
o.receiveShadow = true;
o.material = mtl;
o.nameID = type;
}
});
}

Let`s run this function after loading of model is complete, in useEffect hook choosing theModel variable as dependancy to update

useEffect(() => {
if (theModel) {
for (let object of INITIAL_MAP) {
initColor(theModel, object.childID, object.mtl);
}
}
}, [theModel])

Also change floor color from red to 0xeeeeee . We’re now looking at this:

And there you go: you have scene which includes lights, mouse controls and floor with a chair on it.

Final result of app with complete UI you can fing here

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store