Morph Targets

Morph Targets for Shape Blending

Faces, cloth and paper deform better with morph targets (blend shapes): alternative vertex positions (and normals, tangents, UVs) blended in the vertex shader by an influence from 0 to 1. A MorphTargetManager holds them, and MorphTarget.FromMesh copies a mesh of the same topology. A curling page makes one target:

A page-curl morph target, blended by its influenceJavaScript
const options = { width: 0.2, height: 0.3, subdivisions: 8 };
const page = BABYLON.MeshBuilder.CreateGround('page', options, scene);
const curled = BABYLON.MeshBuilder.CreateGround('curled', options, scene);
const positions = curled.getVerticesData('position');
for (let i = 0; i < positions.length; i += 3) {
  positions[i + 1] = 4 * (positions[i] + 0.1) ** 2;    // 0 at the spine, 0.16 m at the edge
}
curled.setVerticesData('position', positions);
page.morphTargetManager = new BABYLON.MorphTargetManager(scene);
const curl = BABYLON.MorphTarget.FromMesh(curled, 'curl', 0);
page.morphTargetManager.addTarget(curl);
curled.dispose();                                       // the target keeps its own copy
for (const influence of [0, 0.5, 1]) {
  curl.influence = influence;                           // animatable, like any property
  const y = page.getPositionData(false, true).filter((_, i) => i % 3 === 1);   // morphed
  console.log(`influence ${influence}: edge at ${Math.max(...y).toFixed(2)} m`);
}
Output
influence 0: edge at 0.00 m
influence 0.5: edge at 0.08 m
influence 1: edge at 0.16 m

Each vertex moves by influence × (target - base), summed over targets; an Animation of 'influence' or a glTF file's weight channels drives it. Vertex-attribute mode blends at most 8 targets; the default texture mode (useTextureToStoreTargets) lifts that limit.

A book page curling through a morph target whose influence is animated from 0 to 1HTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 300px; display: block; touch-action: none; outline: none; }
  #info { max-width: 600px; padding: 4px 8px; font-family: monospace; }
</style>
<canvas id="view"></canvas>
<div id="info"></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.5, 1.05, 0.75, new BABYLON.Vector3(0, 0.03, 0), scene);
camera.minZ = 0.01;
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene);

// The open book underneath: two page blocks on a cover
const paper = new BABYLON.StandardMaterial('paper', scene);
paper.diffuseColor = BABYLON.Color3.FromHexString('#fff3e0');
paper.backFaceCulling = false;
const cover = BABYLON.MeshBuilder.CreateBox('cover', { width: 0.44, height: 0.01, depth: 0.32 }, scene);
cover.position.y = -0.012;
cover.material = new BABYLON.StandardMaterial('leather', scene);
cover.material.diffuseColor = BABYLON.Color3.FromHexString('#1f5f8b');
for (const x of [-0.105, 0.105]) {
  const block = BABYLON.MeshBuilder.CreateBox('block', { width: 0.2, height: 0.012, depth: 0.3 }, scene);
  block.position.set(x, -0.004, 0);
  block.material = paper;
}

// The base page and a curled copy with the same topology
const options = { width: 0.2, height: 0.3, subdivisions: 8 };
const page = BABYLON.MeshBuilder.CreateGround('page', options, scene);
page.position.x = 0.1;                                  // the spine at x = 0
page.material = paper;
const curled = BABYLON.MeshBuilder.CreateGround('curled', options, scene);
const positions = curled.getVerticesData('position');
for (let i = 0; i < positions.length; i += 3) {
  positions[i + 1] = 4 * (positions[i] + 0.1) ** 2;    // 0 at the spine, 0.16 m at the edge
}
curled.setVerticesData('position', positions);
page.morphTargetManager = new BABYLON.MorphTargetManager(scene);
const curl = BABYLON.MorphTarget.FromMesh(curled, 'curl', 0);
page.morphTargetManager.addTarget(curl);
curled.dispose();                                       // the target keeps its own copy

// influence is animatable like any property
const flip = new BABYLON.Animation('flip', 'influence', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT,
  BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
flip.setKeys([{ frame: 0, value: 0 }, { frame: 60, value: 1 }, { frame: 90, value: 1 }, { frame: 150, value: 0 }, { frame: 180, value: 0 }]);
const ease = new BABYLON.SineEase();
ease.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
flip.setEasingFunction(ease);
scene.beginDirectAnimation(curl, [flip], 0, 180, true);

const info = document.getElementById('info');
scene.onAfterRenderObservable.add(() => {
  const y = page.getPositionData(false, true).filter((_, i) => i % 3 === 1);   // morphed positions
  info.textContent = `curl.influence ${curl.influence.toFixed(2)}: edge at ${Math.max(...y).toFixed(3)} m`;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>