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 ofmorphTargetInfluences[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.
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 runningMan_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 theintersectObject
function within theTHREE.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”.