SKIP TO

3D, Talks

How do we quickly create performant 3D web experiences, (mostly) on-time and under-budget?

In this talk, we will cover key takeaways from using tools like Blender, the storied Three.js, as well as newer players like React Three Fiber, and other helpful libraries that helped us achieve our goals.

From compressed textures and meshes to modern Three materials and lightning, we will take you thru our current process.

Talk

Post

In the ever-evolving world of web development, creating performant 3D web experiences has become increasingly possible and efficient. This post will walk you through our process at MonstarLab, where we leverage tools like Blender, Three.js, and React Three Fiber to deliver high-quality 3D experiences on the web. To demonstrate the power of these tools, let's walk through the process of building a 3D car configurator.

Demo

Car Configurator

Code

Expand App.jsx
App.jsxtsx
1
2import { EquirectangularReflectionMapping } from 'three'
3
4import { useState } from 'react'
5import { Canvas, useLoader } from '@react-three/fiber'
6import { useGLTF, AccumulativeShadows, RandomizedLight, Html, Environment, Center, PresentationControls } from '@react-three/drei'
7import { EffectComposer, Bloom } from '@react-three/postprocessing'
8import { RGBELoader } from 'three-stdlib'
9import { HexColorPicker } from 'react-colorful'
10
11function Car({ map, ...props }) {
12 const [color, setColor] = useState('white')
13 const { nodes, materials } = useGLTF('nissan.glb')
14
15 return (
16 <group {...props} dispose={null}>
17 <group {...props} dispose={null}>
18 <group rotation={[Math.PI / 2, 0, 0]} scale={0.5}>
19 <mesh castShadow receiveShadow geometry={nodes.Object_2.geometry} material={materials['Material.002']} />
20 <mesh castShadow receiveShadow geometry={nodes.Object_4.geometry} material={materials['Material.012']} />
21 <mesh castShadow receiveShadow geometry={nodes.Object_8.geometry} material={materials['Material.034']} />
22 <mesh castShadow receiveShadow geometry={nodes.Object_9.geometry} material={materials['Material.030']} />
23 <mesh castShadow receiveShadow geometry={nodes.Object_10.geometry} material={materials['Material.026']} />
24 <mesh castShadow receiveShadow geometry={nodes.Object_11.geometry} material={materials['Material.028']} />
25 <mesh castShadow receiveShadow geometry={nodes.Object_12.geometry} material={materials['Material.039']} />
26 <mesh castShadow receiveShadow geometry={nodes.Object_13.geometry} material={materials['Material.046']} />
27 <mesh castShadow receiveShadow geometry={nodes.Object_14.geometry} material={materials['Material.048']} />
28 <mesh castShadow receiveShadow geometry={nodes.Object_16.geometry} material={materials['Material.001']} />
29 <mesh castShadow receiveShadow geometry={nodes.Object_17.geometry} material={materials['Material.001']} material-color={color} />
30 <mesh castShadow receiveShadow geometry={nodes.Object_18.geometry} material={materials.Material} />
31 <mesh castShadow receiveShadow geometry={nodes.Object_19.geometry} material={materials['Material.003']} />
32 <mesh castShadow receiveShadow geometry={nodes.Object_20.geometry} material={materials['Material.003']} />
33 <mesh castShadow receiveShadow geometry={nodes.Object_22.geometry} material={materials['Material.007']} />
34 <mesh castShadow receiveShadow geometry={nodes.Object_23.geometry} material={materials['Material.005']} />
35 <mesh castShadow receiveShadow geometry={nodes.Object_24.geometry} material={materials['Material.011']} />
36 <mesh castShadow receiveShadow geometry={nodes.Object_25.geometry} material={materials['Material.011']} />
37 <mesh castShadow receiveShadow geometry={nodes.Object_26.geometry} material={materials['Material.011']} />
38 <mesh castShadow receiveShadow geometry={nodes.Object_27.geometry} material={materials['Material.011']} />
39 <mesh castShadow receiveShadow geometry={nodes.Object_29.geometry} material={materials['Material.018']} />
40 <mesh castShadow receiveShadow geometry={nodes.Object_30.geometry} material={materials['Material.023']} />
41 <mesh castShadow receiveShadow geometry={nodes.Object_31.geometry} material={materials['Material.023']} />
42 <mesh castShadow receiveShadow geometry={nodes.Object_32.geometry} material={materials['Material.023']} />
43 <mesh castShadow receiveShadow geometry={nodes.Object_33.geometry} material={materials['Material.023']} />
44 <mesh castShadow receiveShadow geometry={nodes.Object_34.geometry} material={materials['Material.021']} />
45 <mesh castShadow receiveShadow geometry={nodes.Object_35.geometry} material={materials['Material.021']} />
46 <mesh castShadow receiveShadow geometry={nodes.Object_36.geometry} material={materials['Material.025']} />
47 <mesh castShadow receiveShadow geometry={nodes.Object_37.geometry} material={materials['Material.035']} />
48 <mesh castShadow receiveShadow geometry={nodes.Object_38.geometry} material={materials['Material.035']} />
49 <mesh castShadow receiveShadow geometry={nodes.Object_41.geometry} material={materials['Material.037']} />
50 <mesh castShadow receiveShadow geometry={nodes.Object_42.geometry} material={materials['Material.043']} />
51 <mesh castShadow receiveShadow geometry={nodes.Object_43.geometry} material={materials['Material.043']} />
52 <mesh castShadow receiveShadow geometry={nodes.Object_44.geometry} material={materials['Material.042']} />
53 <mesh castShadow receiveShadow geometry={nodes.Object_45.geometry} material={materials['Material.044']} />
54 <mesh castShadow receiveShadow geometry={nodes.Object_7.geometry} material={materials['Material.013']} />
55 <mesh castShadow receiveShadow geometry={nodes.Object_40.geometry} material={materials['Material.024']} />
56 <mesh castShadow receiveShadow geometry={nodes.Object_39.geometry} material={materials['Material.029']} />
57 <mesh castShadow receiveShadow geometry={nodes.Object_15.geometry} material={materials['plate_d-dds']} />
58 <mesh castShadow receiveShadow geometry={nodes.Object_28.geometry} material={materials['Material.016']} />
59 <mesh castShadow receiveShadow geometry={nodes.Object_5.geometry} material={materials['Material.017']} />
60 <mesh castShadow receiveShadow geometry={nodes.Object_6.geometry} material={materials['Material.019']} />
61 <mesh castShadow receiveShadow geometry={nodes.Object_3.geometry} material={materials['Material.009']} />
62 <mesh castShadow receiveShadow geometry={nodes.Object_21.geometry} material={materials['Material.006']} />
63 </group>
64 </group>
65 <Html position={[0.5, 0.1, 2.75]} scale={0.2} rotation={[Math.PI / 2, 0, 0]} transform>
66 <HexColorPicker className="picker" color={color} onChange={setColor} />
67 </Html>
68 </group>
69 )
70}
71
72export default function App() {
73 const texture = useLoader(RGBELoader, 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/peppermint_powerplant_2_1k.hdr')
74 texture.mapping = EquirectangularReflectionMapping
75 return (
76 <Canvas shadows camera={{ position: [0, 0, 15], fov: 40, near: 1, far: 30 }}>
77 <color attach="background" args={['#f0f0f0']} />
78 <ambientLight />
79 <Environment map={texture} />
80 <PresentationControls
81 global
82 config={{ mass: 1, tension: 250, friction: 25 }}
83 snap={{ mass: 2, tension: 250, friction: 50 }}
84 zoom={1.25}
85 rotation={[0.2, 0.5, 0]}
86 polar={[-Math.PI / 5, Math.PI / 4]}
87 azimuth={[-Math.PI / 1.75, Math.PI / 4]}>
88 <group position={[0, -3, 0]}>
89 <Center top>
90 <Car map={texture} rotation={[-Math.PI / 2, 0, 0]} scale={2.5} />
91 </Center>
92 <AccumulativeShadows temporal frames={100} alphaTest={0.95} opacity={1} scale={20}>
93 <RandomizedLight amount={8} radius={10} ambient={0.5} position={[0, 10, -2.5]} bias={0.001} size={3} />
94 </AccumulativeShadows>
95 </group>
96 </PresentationControls>
97 <EffectComposer>
98 <Bloom luminanceThreshold={1} intensity={0.85} levels={9} mipmapBlur />
99 </EffectComposer>
100 </Canvas>
101 )
102}
103
104

In the code snippet above, we're using React Three Fiber to create a 3D scene within a React component. We add some ambient light, a 3D car model, and orbit controls to allow the user to rotate the view. One of the benefits of using React in this context is the ease of componentization. This allows us to break down the application into understandable and scalable components. Furthermore, we can easily share and prototype ideas using CodeSandbox to promote the sharing of ideas.

In-depth Breakdown

We've built this with TypeScript, Webpack 5, and React 18, aiming to access a new reconciler. Although we haven't yet tested the potential of the new React 18 reconciler and Three Fiber to parallelize a more expensive scene, we're excited about the possibilities this may enable in the future.

For state management, we've used Zustand, an excellent drop-in replacement for Redux. It's a refreshing change if you're used to navigating through Redux folders. We've also used React Three Fiber, which combines React and Three into a React-centered language.

On the CSS side, we prefer styled components. We've also used a group of helpers called Drei. These are React-compatible hooks and components that you can use in conjunction with React Three Fiber. They're incredibly useful, and I highly recommend checking them out on GitHub. For orchestrating our animations, we've used React Spring.

We've also been experimenting with GLTFs compressed with Draco mesh compression. This allows us to download beautiful 3D models quickly. Thanks to React Suspense, we've been able to create a perfect loader that keeps track of the loading status for all textures and models.

In terms of lighting, we've used newer Three materials that allow for simulated light bounce off. We've also used an HDR environment for nice reflections on our 3D models. To center our scene into the world axis of Three.js, we've used Drei's center component.

One of the exciting features we've implemented is a color picker for a 3D car model. This was achieved by using a GLTF to JSX converter to break down all the nodes on our GLTF into addressable objects. We then overwrote the existing material's color with our local color, creating a customizable car color feature.

We've also used Drei's presentation controls for gesture control and an effects composer for a subtle bloom effect on our 3D models. To improve performance, we've used Drei's accumulation shadows, which calculates shadows once and then bakes them in.

WebGL2, GLTFs, and WebAssembly

One of the key factors that have significantly enhanced the performance and capabilities of 3D experiences on the web is the advent of WebGL2. This new standard allows for improved performance levels and easy transition from WebGL1 to WebGL2.

Another crucial element is GLTFs (GL Transmission Format), a standard file format for 3D scenes and models. GLTFs are often referred to as the "JPEG of 3D" due to their efficiency and wide support.

Lastly, WebAssembly (Wasm) has been a game-changer. It allows us to run compiled code in the browser, which significantly improves performance. Both GLTFs and WebGL2 leverage WebAssembly, enabling us to deliver high-performance 3D web experiences.

Conclusion

In conclusion, the combination of TypeScript, Webpack 5, React 18, and other tools has allowed us to create a highly interactive and customizable 3D experience. We've also found that using CodeSandbox to share and iterate on ideas quickly has been invaluable in our development process.

I'd like to extend my gratitude to Mr. Doob, the maintainer of Three.js, the Facebook React team, and Paul Henshell, the maintainer of React Three Fiber. Their contributions to open source have made our work possible. I'd also like to thank Johan Medina for his help with our environments.