DEV Community

Cover image for From Desktop 3d Apps to Web 3d Apps using Blender and React
Omher
Omher

Posted on

8 2

From Desktop 3d Apps to Web 3d Apps using Blender and React

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.

Blender Program

Spring - Blender Open Movie Blender
Source: Spring - Blender Open Movie Blender, Animation Studio via YouTube

Create React App

npx create-react-app cra-fiber-threejs
npm run start
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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 Exporting Menu Blender

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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')
Enter fullscreen mode Exit fullscreen mode
  • 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 to src/ 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;
Enter fullscreen mode Exit fullscreen mode
  • 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 the App.js file, and import it something like:

import './App.css';
import BlenderScene from './BlenderScene';

function App() {
  return (
    <BlenderScene /> 
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • If everything runs successfully, you should see your 3d model in the browser, something like this:

React component

  • 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;

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • If everything works successfully, you should see something like this:
    Final Result

  • 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

Billboard image

Deploy and scale your apps on AWS and GCP with a world class developer experience

Coherence makes it easy to set up and maintain cloud infrastructure. Harness the extensibility, compliance and cost efficiency of the cloud.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs