We're at Stripe

Sessions this week!

Fresh off being awarded Stripe's Services Implementation Specialization, we're in San Francisco to spend time with everyone. Make sure to say hi👋

Back
Blog Post|#3d#react

3D Optimization for Web—How I Got a Model From 26MB Down to 560KB

Jacob Galito
Jacob GalitoThursday, January 5, 2023
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.

Three.js example

R3F example

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 and indirect 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 is Diffuse
  • 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 objectsmesh 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

A-Frame - Make WebVR

3D Model-Viewer Embed

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

gITF Report

GitHub - pmndrs/gltfjsx: Turns GLTFs into JSX components

GLTF → React Three Fiber

Bruno Simon’s course

Bruno’s Twitter

Paul Henschel’s course

Paul Henschel’s twitter

Share this post

twitterfacebooklinkedin

Interested in working with us?

Give us some details about your project, and our team will be in touch with how we can help.

Get in Touch