Three.js Loaders-Simplifiers-Exporters



Focus

This tutorial is about the use of 3D formats (e.g., Material Template Library - MTL) in which 3D models are encoded. Three.js offers 3D object loaders, which enable the load of any model in any format coming, for instance, from Blender. Exporters are loaders counterparts while simplifiers allow the simplification of geometries (typically, the reduction of vertices).

Files with the .obj suffix rely on the well-known 3D “OBJECT” format (OBJ for short), which is relatively poor in terms of 3D property coding. Instead, the 3D Graphics Language Transmission Format (GLTF) format (.glb & .gltf suffixes) is the preferred format within Three.js. Without the possibility of being exhaustive, this tutorial also copes with the Filmbox (Fbx) format.

To deal with 3D formats, Three.js provides a dedicated Application Programming Interface -API- external to Three.js core.

import {FBXLoader} from "three/examples/jsm/loaders/FBXLoader";
import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader";
import {KTX2Loader} from "three/examples/jsm/loaders/KTX2Loader";
import {MeshoptDecoder} from "./js/meshopt_decoder.module.js"; // Requires '"allowJs": true,'
import {MTLLoader} from "three/examples/jsm/loaders/MTLLoader";
import {OBJLoader} from "three/examples/jsm/loaders/OBJLoader";
Application

Installation and use

For ease of installation and use, you may download the TypeScript application with all the necessary settings: .

Once unzipped, you can execute the following statements:

cd Exhibition.ts
npm i
npm run Exhibition

Be careful about running things locally, you need a local Web server to load Exhibition.html (further explanation here…). Typically, if you use Visual Studio Code then you benefit from installing Live Server and running Exhibition.html within it.

Theme

Human models (from left to right: Oscar, SophieR, and Franck) are designed from fun Web sites like AVATAR SDK or READY PLAYER ME. As shown, SophieR is wireframed while the vertex color mode is set to true. Face vertex are colored in green for SophieR.

GLTF format (Oscar) is straightforward. Sub-scene (glb.scene, line 11) is added to the main scene (this._scene) and 3D objects may be subject to scaling (line 12) and/or new positioning (lines 13-14) for custom visualization.

export interface GLTF { // Homemade for this tutorial...
    animations: Array<THREE.AnimationClip>,
    scene: THREE.Group,
    scenes: Array<THREE.Group>,
    cameras: Array<THREE.Camera>,
    asset: Object
}
…
(new GLTFLoader()).load('./models/Oscar.glb', (glb: GLTF) => {
    window.console.assert(glb.scene.isGroup);
    this._scene.add(glb.scene);
    glb.scene.scale.setScalar(2);
    glb.scene.translateX(-2);
    glb.scene.translateZ(2);
});

MTL format works in conjunction with the OBJ format. While Franck is loaded as a whole (lines 25-26,27,29), SophieR is analyzed (i.e., decomposed) so that single meshes are incorporated into the main scene piece by piece (lines 5-6,11,15).

class Exhibition {
    private static readonly _MTLLoader = new MTLLoader;
    …
        Exhibition._MTLLoader.setPath("models/SophieR/").setMaterialOptions({side: THREE.DoubleSide});
        Exhibition._MTLLoader.load("model.mtl", (materials: MTLLoader.MaterialCreator) => {
            materials.preload();
            /**
             * Loaders cannot be shared! If so, Franck becomes wireframed (unexpected!) while wireframing SophieR:
             */
            (new OBJLoader()).setMaterials(materials).setPath("models/SophieR/").load("model.obj", (SophieR: THREE.Group) => {
                // this._scene.add(SophieR); // Whole model *IS NOT* added to the detriment of each piece, see below...
                SophieR.traverse((piece: THREE.Object3D) => {
                    if ((piece as THREE.Mesh).isMesh) {
                        this._scene.add(piece); // Piece instead...
                        piece.scale.setScalar(5);
                        piece.translateY(-7.5);
                        // Set up wireframe mode and color vertex color
                        Utilities.Color_vertices(piece as THREE.Mesh);
                    }
                });
            });
        }); 

        Exhibition._MTLLoader.setPath("models/Franck/").setMaterialOptions({side: THREE.DoubleSide});
        Exhibition._MTLLoader.load("model.mtl", (materials: MTLLoader.MaterialCreator) => {
            materials.preload();
            (new OBJLoader).setMaterials(materials).setPath("models/Franck/").load("model.obj", (Franck: THREE.Group) => {
                Franck.name = "Franck"; // To be used by 'this._scene.getObjectByName'...
                this._scene.add(Franck);
                Franck.scale.setScalar(5); // Adapt original model dimensions to scene...
                Franck.translateX(2);
                Franck.translateY(-7.5);
                Franck.translateZ(2.5);
            });
        });

    …
}

Fbx format is a lesser open format, but, similar to GLTF, models may include enhanced 3D properties like animations. In the code below, two models (from left to right: Zig and Puce) coming from Face Cap are loaded. While Puce (Fbx) is wireframed, Zig (GLTF) is loadable with subtleties: intermediate decoders for compressed textures especially (lines 17-18) must be used (note that MeshoptDecoder is in "./js/meshopt_decoder.module.js", which itself is a copy of the original source code in the Three.js package).

The availability of animations in the two downloaded models invites us to activate these by means of the Three.js animation API (lines 24-27,36 for Zig).

class Exhibition {
    …
        (new FBXLoader()).load("models/FaceCap_ExampleRecording/FaceCap_ExampleExport.fbx", async (head: THREE.Group) => { // https://www.bannaflak.com/face-cap/
            this._scene.add(head);
            head.translateX(1);
            head.translateZ(0.5);
            head.scale.setScalar(0.05);
            this._animation_mixer1 = new THREE.AnimationMixer(head);
            window.console.assert(head.animations.length > 0);
            this._animation_mixer1.clipAction(head.animations[0]).play();
            head.traverse((child: THREE.Object3D) => {
                if (child.type === "Mesh")
                    ((child as THREE.Mesh).material as THREE.MeshBasicMaterial).wireframe = true;
            });
        });

        const ktx2Loader = new KTX2Loader().setTranscoderPath("node_modules/three/examples/js/libs/basis/").detectSupport(this._renderer);
        (new GLTFLoader()).setKTX2Loader(ktx2Loader).setMeshoptDecoder(MeshoptDecoder).load('models/facecap.glb', (glb: GLTF) => {
            window.console.assert(glb.scene.isGroup);
            this._scene.add(glb.scene);
            glb.scene.translateX(-2.5);
            glb.scene.translateZ(2.5);
// Caution, it depends upon the structure of the loaded GLTF model:
            const mesh = glb.scene.children[0];
            this._animation_mixer2 = new THREE.AnimationMixer(mesh);
            window.console.assert(glb.animations.length > 0);
            this._animation_mixer2.clipAction(glb.animations[0]).play();
        });

    …
    private _animate = () => {
        this._requestAnimationFrame_id = window.requestAnimationFrame(this._animate);
        const delta = this._clock.getDelta();

        this._animation_mixer1?.update(delta);
        this._animation_mixer2?.update(delta);
        …
    }
}

Exercise

Create your own avatar from AVATAR SDK or READY PLAYER ME and load it!

Model simplification (i.e., geometry) and export

Principle

Models come with a non-homogeneous hierarchical 3D structure designed in tools like Blender. Custom geometries in such models may be excessively “precise” (left hand side of picture) while application needs may be low in geometry precision. Simplification is thus the process of obtaining refined geometries (right hand side of picture) that fit needs.

Three.js supports off-library facilities (typically, THREE.BufferGeometryUtils for THREE.BufferGeometry) that allow the merging of several geometries into one. Moreover, custom geometries may benefit from being simplified in drastically reducing their number of vertices and thus faces.

Application

A woman bust in GLTF format (license “CC0”, credit to KungFuMasterPS) downloaded from Blendswap is based on a sizeable geometry (more than 300.000 vertices), which lowers application's performance in general. Before visualizing the woman bust, a new 3D object is built from a simplified geometry (around 30.000 vertices). Export in Woman_head_simplified.gltf file allows future usages in other applications…

Installation and use

For ease of installation and use, you may download the TypeScript application with all the necessary settings: .

Once unzipped, you can execute the following statements:

cd Woman_head.ts
npm i
npm run Woman_head

Be careful about running things locally, you need a local Web server to load Woman_head.html (further explanation here…). Typically, if you use Visual Studio Code then you benefit from installing Live Server and running Woman_head.html within it.

Once simplified (lines 30-40: more than a couple of minutes, please wait!), revised 3D object is exported (from lines 54-55) in GLTF format

class Woman_head {
    static readonly Woman_head_name = "WOMAN HEAD";
    …
    constructor() {
        …
        new Promise(ready => (new GLTFLoader()).load('./models/Woman_head.gltf', this._process_GLTF.bind(this, ready))).then(value => {
            window.console.assert(value === Woman_head.Woman_head_name);
            this._save_as_GLTF();
            this._animate();
        });    
    }
    …
    private _process_GLTF(ready: Function, gltf: GLTF, rate: number = 0.75) { // Blender model from https://www.blendswap.com/blend/6855
        // Original Blender scene is ignored:
        // this._scene.add(gltf.scene);
        const geometries: Array<THREE.BufferGeometry> = new Array();
        const meshes: Array<THREE.Mesh> = new Array();
        gltf.scene.traverse((child: THREE.Object3D) => {
            if (child.type === "Mesh") {
                geometries.push((child as THREE.Mesh).geometry);
                meshes.push((child as THREE.Mesh));
            }
            if (child.type === "Scene")
                child.children.forEach(object => window.console.info(object.name + ' with type: ' + object.type)); // 'Camera with type: Object3D' 'Cube with type: Group'
        });
        meshes.forEach(mesh => {
            window.console.info(mesh.name + ' with type: ' + mesh.type); // 4 elements as part of 'Cube'
            (mesh.material as THREE.Material).dispose();
        });
// Create unified geometry ('import {mergeBufferGeometries, mergeVertices} from "three/examples/jsm/utils/BufferGeometryUtils"');
        let geometry = mergeBufferGeometries(geometries);
        window.console.assert(geometry.attributes.position.count === 139216); // Heavyweight original model...
// Close vertices are merged (default distance is 0.0001, greater value, e.g., '0.001' increases vertex reduction):
        geometry = mergeVertices(geometry);
        window.console.assert(geometry.attributes.position.count === 136616); // First simplification result...
// Discard source geometries:
        geometries.forEach(geometry => geometry.dispose());
// Simplify geometry (have to wait a couple of minutes!):
        geometry = (new SimplifyModifier()).modify(geometry, Math.floor(geometry.attributes.position.count * rate));
        window.console.assert(geometry.attributes.position.count === 34154); // Second simplification result...
// Set up colors:
        geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(geometry.attributes.position.array.length), geometry.attributes.position.itemSize));
        geometry.center(); // Positioning...
        this._coloring(geometry, geometry.attributes.position.count, geometry.attributes.position.count / 2)
        const woman_head = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
            vertexColors: true,
            wireframe: true
        }));
        woman_head.name = Woman_head.Woman_head_name;
        woman_head.scale.set(1.5, 1.5, 1.5);
        this._scene.add(woman_head);
        ready(Woman_head.Woman_head_name);
    }
    private _save_as_GLTF(file_name = "Woman_head_simplified.gltf") { // May be found in browser settings: defined directory for downloads...
        (new GLTFExporter()).parse(this._scene.getObjectByName(Woman_head.Woman_head_name)!, woman_head => {
            window.console.log(woman_head);
            const download = window.document.createElement('a'); // 'Anchor'
            window.document.body.appendChild(download); // Mandatory for Firefox
            download.href = URL.createObjectURL(new Blob([JSON.stringify(woman_head)], {type: 'application/json'}));
            download.download = file_name;
            download.click();
        }, error => window.console.error(error));
    }
}

Exercise

Export a simplified version of Man head to GLTF. Note that the availability of the model in Blender format only, imposes the export of the model to GLTF within Blender. Otherwise, the model in GLTF may be found in the Man head application .