Three.js Morphing



Focus

This tutorial is about morphing. Given a model coming from, for instance, Blend Swap, any geometry in this model is rarely equipped with morphing. Morphing consists in computing a distorded geometry. Animations are then the possible (discrete) motions from the original geometry to the distorded one.

Motions are controlled through the morphTargetInfluences attribute (an array in fact, each element being devoted to a computed morphing). Values of morphTargetInfluences[0] for the first morphing may follow a mathematical function mapping to, for example, a spring motion. On purpose, the TWEENJS library is here reused because it provides such animation functions. However, Three.js possesses its own animation system.

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 Man_head.ts
npm i
npm run Man_head

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

Theme

A man bust in GLTF format (license “CC0 Attribution”, credit to ArtOfLight) downloaded from Blend Swap includes several geometries. While eyes, eye corneas... are excluded, the main geometry (man bust essentially being a head) is kept. A half sphere geometry named "FRONT" is created from scratch. The morphing is computed for/from this geometry. The distortion is such that each vertex in this half sphere geometry is projected onto the man bust front using the intersectObject function within the THREE.Raycaster class. Distortion is then the carved face.

Picture shows the (white) half sphere in motion (intermediate step): each half sphere vertex moves to the face's details playing the role of distortion in this app.

class Man_head {
    static readonly Man_head_name = "2_Head_sculpt_retopo_mesh";
    …
    constructor() {
        …
        new Promise(ready => (new GLTFLoader()).load('./models/Man_head.gltf', this._process_GLTF.bind(this, ready))).then(value => {
            window.console.assert(value === Man_head.Man_head_name);
            this._animate();
        });
    }
    …
    // Called within '_process_GLTF':
    private _morphing(man_head: THREE.Mesh) { // https://stackoverflow.com/questions/16361327/check-if-point-is-inside-a-custom-mesh-geometry
        const surrounding_bubble = new THREE.Group();
        this._scene.add(surrounding_bubble);
        surrounding_bubble.name = "Surrounding Bubble";
        man_head.geometry.computeBoundingSphere();
        const sphere_geometry = new THREE.SphereGeometry(man_head.geometry.boundingSphere!.radius, 68, 40, 0, Math.PI);
        // 'raycaster' requires tiny offset so that intersections are *ALL* not empty:
        sphere_geometry.rotateY(Number.EPSILON);

        // const back = new THREE.Mesh(sphere_geometry, new THREE.MeshBasicMaterial({
        //     side: THREE.DoubleSide,
        //     wireframe: true
        // }));
        // back.rotateY(Math.PI);
        // back.name = "BACK";
        // surrounding_bubble.add(back);

        const front = new THREE.Mesh(sphere_geometry, new THREE.MeshBasicMaterial({
            alphaTest: 0.5,
            // morphNormals: true, // No longer needed with ver. 144...
            // morphTargets: true, // No longer needed with ver. 144...
            opacity: 0.5,
            side: THREE.DoubleSide,
            transparent: true
        }));
        front.name = "FRONT";
        surrounding_bubble.add(front);
        // Note that 'front' has been designed such that vertex number is close to half of 'man_head', i.e., 5784:
        window.console.assert(front.geometry.attributes.position.count === 2829);

        // Set up 'front' morphing (morphing vertex number is the same that 'front' geometry vertex number):
        const morphing = new THREE.BufferAttribute(Float32Array.from(front.geometry.attributes.position.array),
                                                    front.geometry.attributes.position.itemSize);

        front.geometry.morphAttributes.position = []; // Array of each computed morphing...
        front.geometry.morphAttributes.position.push(morphing); // First one whose motion is ruled by 'front.morphTargetInfluences[0]'
        // Compute morphing (i.e., geometry of 'front' half sphere is distorted with regard to man head's face):
        const direction = new THREE.Vector3(0, 0, 0);
        const origin = new THREE.Vector3(0, 0, 0);
        const positions = front.geometry.getAttribute('position').array;

        const intersection = new Array();
        const raycaster = new THREE.Raycaster();
        for (let i = 0; i < front.geometry.attributes.position.count; i++) {
            direction.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
            direction.negate().normalize(); // To scene center...
            origin.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);

            raycaster.set(origin, direction); // From point of 'front' geometry towards scene center as direction...
            /** DEBUG */
            // this._scene.add(new THREE.ArrowHelper(direction, origin));
            /** End of DEBUG */
            intersection.length = 0; // Clear...
            raycaster.intersectObject(man_head, false, intersection);
            /* Returned by 'intersectObject':
             [ { distance, point, face, faceIndex, object }, ... ]

             distance – distance between the origin of the ray and the intersection
             point – point of intersection, in world coordinates
             face – intersected face
             faceIndex – index of the intersected face
             object – the intersected object
             */
            if (intersection.length > 0) // 'intersection[0].point' is the closest point:
                // Effective distortion:
                morphing.setXYZ(i, intersection[0].point.x, intersection[0].point.y, intersection[0].point.z);
        }
        front.updateMorphTargets(); // 'front.morphTargetInfluences' is reset to blank array:
        window.console.assert(front.morphTargetInfluences!.length === 1 && front.morphTargetInfluences![0] === 0);
        createjs.Tween.get(this, { // TWEENJS animation from offered functions...
            onChange: () => {
                front.morphTargetInfluences![0] = this._tween; // Morphing motion here...
            }, loop: true, paused: false
        })
            .to({_tween: 1}, 1500, createjs.Ease.linear) // Other mathematical functions are usable...
            .wait(1500)
            .to({_tween: 0}, 500, createjs.Ease.linear); // Other mathematical functions are usable...
    }

    private _process_GLTF(ready: Function, gltf: GLTF) {
        …
    }
}

Exercise

Compute some morphing for "Eyes" mesh, which is currently set to “invisble”.