3D Optimization for Web—How I Got a Model From 26MB Down to 560KB
3D Optimization for Web—How I Got a Model From 26MB Down to 560KB
With my investment time at Echobind, as well as my own free time, I’ve been slowly learning webGL libraries such as three.js, Mozilla’s/Super Medium’s A-Frame, Google’s model-viewer and, more recently, React-three-fiber(R3F).
For Q4 investment time, I worked on creating a scrollable, 3D website with a coworker using R3F. From this project, I’d like to highlight my process for optimizing 3D models for web. From that process, I was able to reduce the main 3D model from 26MB down to 560KB.
What is React-three-fiber?
Frameworks such as three.js make programming with webGL easier. Furthermore, libraries such as R3F—which abstracts three.js more—make working with 3D experiences on the web even easier.
Load time issues
One of the challenges that we faced while working on this scene was how to minimize load time on the client side. Our first approach was to create a load screen. However, since our 3D models files size was too large (26MB), it was taking too long to load. When it finally loaded, the interactive animations were slow and choppy. To correct these issues, I wanted to find a way to minimize the file size.
My approach to the problem
The primary contribution to the large file size was the complexity in our 3D model as well as its associated textures. So, I took on the responsibility of exploring how to minimize our loading time by optimizing our 3D model. I also discovered that there were too many materials and meshes in our scene. By minimizing both of these components, it would reduce the file size of the exported model. Here are the steps that I took:
Step 1 - Optimizing meshes, deleting polygons and combining objects
Optimization
Since we were concerned about this scene not running well on the web, I wanted to remove any potentially unnecessary polygons on the meshes in the blender project. This was done using the decimation modifier. I typically start with reducing the mesh ratio to 0.5 and reduce lower if it doesn’t cause any unwanted artifacts. This step was repeated for most of the meshes.
Deleting Polygons
In an effort to help with unwrapping, as well as removing anything that won’t be showing up in the final view, I removed all polygons that were at the bottom of all meshes, such as the bottom portion of the snow mesh. This was done by going into edit mode > face selection using the select lasso. This step was repeated for most of the meshes.
Combining meshes
In order to get started with combining meshes, I needed to determine which meshes make sense to combine. In the end, I combined all objects in the scene that are on top of the snow on the ground and floor meshes—minus the picture frame. I decided to do this because they would all fit into the same UV space well and wanted them all to have the same size materials. The snow took up a considerable amount of space and adding it to the same UV space would reduce the scale of all of the objects; which would lower the quality of the textures for the objects as well. From there, I wanted to combine all meshes associated with the picture frame—minus the glass and photo—since I wanted to be able to add as much quality into the photo image. The glass needed its own material to prevent any transparency issues will other parts of the scene that didn’t need transparency. I renamed the meshes appropriately to their contents: objects, frame, glass, photo, floor, snow
as well as their associated materials.
This resulted in reducing the scene from 65 individual meshes to 6 objects of related meshes. Combining meshes actually increases the blender file size, but helped me to remove any potential duplicates in the scene and quickly select a large amount of polygons in the scene in order to manipulate them easier for the following steps.
Step 2 - UV Packing
Before I was able to unwrap and pack the UVs together, I needed to associate all of the polygons of each mesh into one material. At this point there were a lot of materials within the project. So, first what needed to be done was to remove all unused orphaned
materials. By deleting all orphaned
materials, I reduced the materials in the scene from 71 materials down to 6 materials.
Here’s the results of the unwrapped objects
:
Here’s the results of the unwrapped picture frame
:
All of the remaining meshes were unwrapped as well.
Step 3 - Material IDs
Material ID grants you the ability to quickly select a section of a mesh that you have determined will have a specific material added to that section. Instead of adding several materials to one mesh, which can add more to the files size of the model, we can take advantage of material IDs and translate these selections of a mesh into one single texture that can be used in step 5.
To do this, I created various materials with high contrasting colors. After all of the polygons of the objects
mesh and picture frame
mesh had various colored materials added to it, I then baked the colors out so they were added to the UV coordinates of the meshes.
Step 4 - Baking Material IDs
The process of baking, for this purpose, is to take all of the color information, from the various colored materials I had just added, and project those colors onto a texture.
This is the resulting material ID maps that were baked out, as shown above. Some things to note before baking materials in Blender:
- Change the render mode to
cycles
- Turn off any
direct
andindirect
lights - Ensure you have selected the diffuse map in shaders viewport
- Select the duplicate mesh that has the colors added
- Have the mesh with the unwrapped UVs be your active selection
- Ensure that
Bake Type
isDiffuse
- Press
Bake
The final step in Blender was exporting everything as .fbx as well as export out all material ID textures.
Step 5 - Material Creation in Substance Painter
Substance Painter is one of my favorite 3D tools. It can make the texturing process so much fun and contextual. It’s basically photoshop for 3D models.
Set up
While importing the .fbx, I also imported in the material ID textures for objects
mesh and picture frame
mesh.
Masking and selecting material IDs
After baking the mesh maps (normals, world space normals, ambient occlusion, curves, etc.) I then created folders for each color of the objects
mesh and picture frame
mesh and used a mask to select each material id to attribute to the mask. This is how we’re able to add different colors and textures to part of a mesh.
Exporting and Combining baseColor and ambient occlusion
Since the desired result we’re going for is not so realistic, I decided to only export out the baseColor
and ambient occlusion
but since I wanted to add the shadow/lighting from ambient occlusion to the baseColor I needed to combine the two into one texture. In order to do this, I needed to add a new preset in the export textures
viewport.
Step 6 - Adding baseColor in Blender
This step is really quick. I just needed to add the baked texture results from Substance Painter and add them to each material’s baseColor
. After that, I exported everything as a .glb
. At this point, the exported file size was 5.1MB.
Step 7 - glTF Compression
gltf.report tool
Now that the model is textured and exported from Blender, I uploaded the model to gltf.report to compress the .glb
more.
After uploading the model in to gltf.report, I then clicked on the script
tab. From there I reduced the texture size from 1024 x 1024 down to 128 x 128 and clicked Run
then Export
afterward.
The results got the file from 5.1MB down to 3MB. The resulting reduced mesh was then added to the project under /public/meshes/
.
gltfjsx command-line tool
Before running the next step, it’s important to install nvm
and install an earlier version of node. Otherwise, you’ll probably get error when running gltfjsx
.
NVM ls NVM use 14.20.0
Before I could run the command-line tool, I needed to change directories from the project folder to where the mesh is located. Otherwise the results would be added to the project folder.
cd /public/meshes/
Now, I was able to run gltfjsx
npx gltfjsx main_scene.glb -T
The result of running gltfjsx
provides a smaller .glb
file as well as a react.js
component of the 3D model. The results reduced the file from 3MB down to 581KB.
If you don’t want to mess with installing gltfjsx, and earlier node versions, you could also go to https://gltf.pmnd.rs/ but results may vary.
And here’s the final result at 560KB
Here are the results from gltfjsx
for main_scene.js
:
/* Auto-generated by: https://github.com/pmndrs/gltfjsx */ import * as THREE from 'three' import React, { useRef } from 'react' import { CycleRaycast, useGLTF } from '@react-three/drei' import { GLTF } from 'three-stdlib' export function Main(props) { const { nodes, materials } = useGLTF('/meshes/main_scene.glb') return ( <group {...props} dispose={null}> <mesh geometry={nodes.floor.geometry} material={materials.floor} position={[-0.01, 0, -0.61]} /> <mesh geometry={nodes.Snow.geometry} material={materials.Snow} position={[0, 0, -0.61]} /> <mesh geometry={nodes.objects.geometry} material={materials.objects} position={[1.13, 0.28, 0.53]} rotation={[-Math.PI, -0.09, -Math.PI]} /> <mesh // onClick={() => {console.log('frame clicked')}} geometry={nodes.frame.geometry} material={materials.frame} position={[-0.02, 1.27, -1.89]} rotation={[Math.PI, -0.02, Math.PI]} /> <mesh onClick={() => props.setModalIsOpen(true)} geometry={nodes.photo.geometry} material={materials.photo} position={[-0.02, 1.24, -1.89]} rotation={[Math.PI, -0.02, Math.PI]} scale={0.09} > {/* testing out how to add cursor pointer based on mouse over the photo */} {/* docs here https://github.com/pmndrs/drei#cycleraycast */} {/* example use of useCursor https://codesandbox.io/s/ny3p4?file=/src/App.js:977-983 */} <CycleRaycast preventDefault={true} // Call event.preventDefault() (default: true) scroll={true} // Wheel events (default: true) keyCode={9} // Keyboard events (default: 9 [Tab]) onChanged={(objects, cycle) => console.log(objects)} // Optional onChanged event onPointerOver={() => {console.log('hovered')}} /> </mesh> <mesh geometry={nodes.glass.geometry} material={materials.glass} position={[-0.02, 1.27, -1.89]} rotation={[0, 0.02, 0]} /> </group> ) } useGLTF.preload('/meshes/main_scene.glb')
Here’s the full 3D website project
Thanks
We wouldn’t have been able to learn this process—among everything R3F and three.js—if it wasn’t for Bruno Simon’s new R3F lessons, and Paul Henschel’s Gumroad course. Thank you both for sharing your knowledge!
Resources
three.js - JavaScript 3D library
React Three Fiber Documentation
Blender how to Reduce Poly Count and Bake Textures
What are colour IDs and how do we use them?
I wish I knew this before using React Three Fiber