From Desktop 3d Apps to Web 3d Apps using Blender and React
Omher
Posted on April 18, 2022
In this tutorial, I will walk you thru the steps to create a 3d react application with some interactivity so in the final you will have something like this
- What is Blender? - Simply Explained
- Create React App
- Install dependencies
- Export blender asset
- Compress asset
- Convert asset to
JSX
component - Integrate new component
- Enhanced component and functionality
- Adding some style
- Install dependency
- Edit React Components
- Resources
- Appendix
Before you start
You will need to have the following installed or configured and know at least the basics of using them before proceeding.
- NodeJS installed (preferable > 12)
- Basic Knowledge in React
- Previous use of
create-react-app
- Not mandatory, but some basic knowledge of using blender 3d app to understand the concept of mesh and material
What is Blender? Simply Explained
This tutorial is not a blender tutorial, so that it will be a short explanation.
Blender is a free, open-source 3D creation suite. With a strong foundation of modeling capabilities, there's also robustly texturing, rigging, animation, lighting, and other tools for complete 3D creation.
Source: Spring - Blender Open Movie Blender, Animation Studio via YouTube
Create React App
npx create-react-app cra-fiber-threejs
npm run start
If everything works successfully, you can navigate to: http://localhost:3000/, and you will see a React App
Install dependencies
- Install
gltf-pipeline
; this will help you to optimize our glTF, meaning smaller for the web; this is installed globally
npm install -g gltf-pipeline
- Install @react-three dependencies for our project, navigate to
cra-fiber-threejs
folder and run
npm i @react-three/drei
npm i @react-three/fiber
Export blender asset
- Open blender program with you're created, 3d model
- if you have installed blender and created a 3d modeling, in case you didn't, take a look in the optional step
Optional
- If you have installed blender but didn't create any model, here you have the one I'm using in the tutorial
- If you didn't install blender and want the compressed
glb
file here, you can download it.
Compress asset
- The file we exported from the previous step, some times are significant, and they are not optimized for the web, so we need to compress it
- Navigate where you saved the
.glb
file (from the previous step) and run the following command:
gltf-pipeline -i <input file glb> -o <output glb> --draco.compressionLevel=10
e.g:
gltf-pipeline -i shoe.glb -o ShoeModelDraco.glb --draco.compressionLevel=10
Convert asset to JSX
component
To start interacting with our 3d model, we need to convert it to a JSX component using gltfjsx. You can read more here. gltfjsx
- Turns GLTFs into JSX components)
- Navigate where you saved the .glb file from the previous step and run the following command:
npx gltfjsx <outputed glb from previus step>
e.g. npx gltfjsx ShoeModelDraco.glb
- The output will be a
js
file with content similar to:
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/
import React, { useRef } from 'react'
import { useGLTF } from '@react-three/drei'
export default function Model({ ...props }) {
const group = useRef()
const { nodes, materials } = useGLTF('/ShoeModelDraco.glb')
return (
<group ref={group} {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.laces} />
<mesh geometry={nodes.shoe_1.geometry} material={materials.mesh} />
<mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
<mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
<mesh geometry={nodes.shoe_4.geometry} material={materials.sole} />
<mesh geometry={nodes.shoe_5.geometry} material={materials.stripes} />
<mesh geometry={nodes.shoe_6.geometry} material={materials.band} />
<mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
</group>
)
}
useGLTF.preload('/ShoeModelDraco.glb')
- The output It's a React component with all the meshed/materials mapped ready to work
- If you worked with blender, you can see that it has mapped all its meshes objects and all its materials
- This component can now be dropped into your scene. It is asynchronous and, therefore, must be wrapped into
<Suspense>
which gives you complete control over intermediary loading-fallbacks and error handling.
Integrate new component
- Go the project you created using
create-react-app
- Copy your new file created in step "Convert asset to
JSX
component" e.g. ShoeModelDraco.js tosrc/
folder - Create a new file for your new component and called it
BlenderScene.js
, this file will include for the simplicity also some logic and the Scene components, in a real application you will want to separate them in different files/components, copy the following code:
import React, { Suspense } from 'react';
import { Canvas } from "@react-three/fiber"
import { ContactShadows, Environment, OrbitControls } from "@react-three/drei"
import Model from './ShoeModelDraco'
function Scene() {
return (
<div className='scene'>
<Canvas shadows dpr={[1, 2]} camera={{ position: [0, 0, 4], fov: 50 }}>
<ambientLight intensity={0.3} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
<Suspense fallback={null}>
<Model />
<Environment preset="city" />
<ContactShadows rotateX={Math.PI / 2} position={[0, -0.8, 0]} opacity={0.25} width={10} />
</Suspense>
<OrbitControls minPolarAngle={Math.PI / 2} maxPolarAngle={Math.PI / 2} enableZoom={false} enablePan={false} />
</Canvas>
</div>
)
}
function BlenderScene() {
return (
<>
<Scene />
</>
);
}
export default BlenderScene;
Copy into the public folder the
.glb
output file from step "Export blender asset," in my case:ShoeModelDraco.glb
Use the
BlenderScene
component you just created, open theApp.js
file, and import it something like:
import './App.css';
import BlenderScene from './BlenderScene';
function App() {
return (
<BlenderScene />
);
}
export default App;
- If everything runs successfully, you should see your 3d model in the browser, something like this:
- The only interactivity that you have it's that you can spin the 3d model, and that's it,
- In the following steps, we will:
- Add more fun/complex interactivity
- Display nicer in the browser
- In the resources part, you can find a link for the branch with the code until this step
Enhanced component and functionality
If you are reading here, kudos 💪🏼.
You are almost done 🥵; you have your 3d model in the browser 🎉, but you saw, it's not very interesting and boring; let's start adding cool stuff 😎.
Disclaimer: The following code it's not production-ready, and I did some hacks and also not best practices when writing the components
Adding some style
- Open the
App.css
file and add in the end of it the following:
#root {
position: relative;
margin: 0;
padding: 0;
overflow: hidden;
outline: none;
width: 100vw;
height: 100vh;
}
.scene {
height: 500px;
padding: 100px;
}
Install dependency
- We will install
react-colorful
, a tiny color picker component for React and Preact apps. We will use it for choosing colors
npm i react-colorful
Edit React Components
- Open
ShoeModelDraco.js
file and copy the following code - We add functionality to work with the mouse when the user clicks on our model
- We add state to know which part of the model was selected
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/
import React, { useRef } from 'react'
import { useGLTF } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
export default function Model({ props, currentState, setCurrentState, setHover }) {
const group = useRef()
const { nodes, materials } = useGLTF('/ShoeModelDraco.glb');
// Animate model
useFrame(() => {
const t = performance.now() / 1000
group.current.rotation.z = -0.2 - (1 + Math.sin(t / 1.5)) / 20
group.current.rotation.x = Math.cos(t / 4) / 8
group.current.rotation.y = Math.sin(t / 4) / 8
group.current.position.y = (1 + Math.sin(t / 1.5)) / 10
})
return (
<>
<group
ref={group} {...props}
dispose={null}
onPointerOver={(e) => {
e.stopPropagation();
setHover(e.object.material.name);
}}
onPointerOut={(e) => {
e.intersections.length === 0 && setHover(null);
}}
onPointerMissed={() => {
setCurrentState(null);
}}
onClick={(e) => {
e.stopPropagation();
setCurrentState(e.object.material.name);
}}>
<mesh receiveShadow castShadow geometry={nodes.shoe.geometry} material={materials.laces} material-color={currentState.items.laces} />
<mesh receiveShadow castShadow geometry={nodes.shoe_1.geometry} material={materials.mesh} material-color={currentState.items.mesh} />
<mesh receiveShadow castShadow geometry={nodes.shoe_2.geometry} material={materials.caps} material-color={currentState.items.caps} />
<mesh receiveShadow castShadow geometry={nodes.shoe_3.geometry} material={materials.inner} material-color={currentState.items.inner} />
<mesh receiveShadow castShadow geometry={nodes.shoe_4.geometry} material={materials.sole} material-color={currentState.items.sole} />
<mesh receiveShadow castShadow geometry={nodes.shoe_5.geometry} material={materials.stripes} material-color={currentState.items.stripes} />
<mesh receiveShadow castShadow geometry={nodes.shoe_6.geometry} material={materials.band} material-color={currentState.items.band} />
<mesh receiveShadow castShadow geometry={nodes.shoe_7.geometry} material={materials.patch} material-color={currentState.items.patch} />
</group>
</>
)
}
useGLTF.preload('/ShoeModelDraco.glb')
- Open
BlenderScene.js
file and copy the following code - We add state in order to know which part of the model was selected
- Added work with the picker component
- Added animation to the model, floating illusion
import React, { useState, useEffect, Suspense } from 'react';
import { Canvas } from "@react-three/fiber"
import { ContactShadows, Environment, OrbitControls } from "@react-three/drei"
import { HexColorPicker } from 'react-colorful'
import Model from './ShoeModelDraco'
function Scene() {
// Cursor showing current color
const [state, setState] = useState({
current: null,
items: {
laces: "#ffffff",
mesh: "#ffffff",
caps: "#ffffff",
inner: "#ffffff",
sole: "#ffffff",
stripes: "#ffffff",
band: "#ffffff",
patch: "#ffffff",
},
});
const [hovered, setHover] = useState(null)
useEffect(() => {
const cursor = `<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path fill="rgba(255, 255, 255, 0.5)" d="M29.5 54C43.031 54 54 43.031 54 29.5S43.031 5 29.5 5 5 15.969 5 29.5 15.969 54 29.5 54z" stroke="#000"/><g filter="url(#filter0_d)"><path d="M29.5 47C39.165 47 47 39.165 47 29.5S39.165 12 29.5 12 12 19.835 12 29.5 19.835 47 29.5 47z" fill="${state.items[hovered]}"/></g><path d="M2 2l11 2.947L4.947 13 2 2z" fill="#000"/><text fill="#000" style="white-space:pre" font-family="Inter var, sans-serif" font-size="10" letter-spacing="-.01em"><tspan x="35" y="63">${hovered}</tspan></text></g><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h64v64H0z"/></clipPath><filter id="filter0_d" x="6" y="8" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="2"/><feGaussianBlur stdDeviation="3"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter></defs></svg>`
const auto = `<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, 0.5)" d="M29.5 54C43.031 54 54 43.031 54 29.5S43.031 5 29.5 5 5 15.969 5 29.5 15.969 54 29.5 54z" stroke="#000"/><path d="M2 2l11 2.947L4.947 13 2 2z" fill="#000"/></svg>`
if (hovered) {
document.body.style.cursor = `url('data:image/svg+xml;base64,${btoa(cursor)}'), auto`
return () => (document.body.style.cursor = `url('data:image/svg+xml;base64,${btoa(auto)}'), auto`)
}
}, [hovered])
function Picker() {
return (
<div style={
{
display: state.current ? "block" : "none",
position: "absolute",
top: "50px",
left: "50px",
}
}>
<HexColorPicker
className="picker"
color={state.items[state.current]}
onChange={(color) => {
let items = state.items;
items[state.current] = color
}}
/>
<h1>{state.current}</h1>
</div>
)
}
return (
<div className='scene'>
<Canvas shadows dpr={[1, 2]} camera={{ position: [0, 0, 4], fov: 50 }}>
<ambientLight intensity={0.3} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
<Suspense fallback={null}>
<Model
currentState={ state }
setCurrentState={(curState) => {
setState({
...state,
current: curState
})
}}
setHover={ setHover}
/>
<Environment preset="city" />
<ContactShadows rotateX={Math.PI / 2} position={[0, -0.8, 0]} opacity={0.25} width={10} />
</Suspense>
<OrbitControls minPolarAngle={Math.PI / 2} maxPolarAngle={Math.PI / 2} enableZoom={false} enablePan={false} />
</Canvas>
<Picker />
</div>
)
}
function BlenderScene() {
return (
<>
<Scene />
</>
);
}
export default BlenderScene;
If everything works successfully, you should see something like this:
In the resources part, you can find a link for the branch with the code until this step
Live Working example here
Resources
Appendix
- 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; more in here
- glTF files
- Graphics Language Transmission Format or GL Transmission Format, more here
- gltf-pipeline
- Content pipeline tools for optimizing glTF, more here
Posted on April 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.