API

Dynamically Morph A Mesh

What is morphing?

This page explains how to morph certain kinds of meshes.
For now, it concerns only ribbons, tubes, extruded shapes and lines.

When talking about morphing, we mean changing the vertex positions of an existing mesh. Indices remain unchanged. This means the mesh keeps the same number of vertices and the same faces between vertices, and it remains the same object in your code. Only its vertices change their coordinates.

If you handle a box or another fixed basic shape, it's quite easy to access vertex positions because your mesh has an expected shape. For example, you can assume a box has four vertices per face.
But when handling parametric shapes like ribbons, tubes, and so on, it becomes very difficult to guess how and where vertices were positioned by the mesh constructor algorithm. For a tube, for instance, you only know the axis path you gave to build it, along with the radius and tessellation.

So you will learn here how to update the shape of an existing mesh and how to morph it in the render loop.

Ribbon

ribbon updatable parameter : pathArray

(reminder : only points positions can change in the path array, not the number of points. Please see the summary at the bottom of this page for more details)

Let's create a ribbon.
As explained in the Ribbon tutorial, a good way to create a ribbon is to fill many arrays with Vector3 values using two for loops: one for each path, and another for the array of paths, the pathArray.

Here we create a simple plane ribbon in the xOz plane.

import { Vector3 } from "@babylonjs/core/Maths/math.vector";
import { Mesh } from "@babylonjs/core/Meshes/mesh";
import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
// path function
const pathFunction = function (k) {
const path = [];
for (let i = 0; i < 60; i++) {
const x = i - 30;
const y = 0;
const z = k;
path.push(new Vector3(x, y, z));
}
return path;
};
// ribbon creation
const sideO = Mesh.BACKSIDE;
const pathArray = [];
for (let i = -20; i < 20; i++) {
pathArray.push(pathFunction(i * 2));
}
const mesh = MeshBuilder.CreateRibbon("ribbon", { pathArray: pathArray, closeArray: false, closePath: false, offset: 0, updatable: true, sideOrientation: sideO }, scene);

example : Dynamic Mesh Morph Example 1 (please rotate the cam to see it)

The important point to notice is that we set the updatable parameter to true in the CreateRibbon() method.

We can now imagine that we want to change this existing ribbon's y coordinates according to a sine function. So, for each path in the existing pathArray array, we simply change the Vector3 coordinates.
Note that we don't create new paths or a new pathArray array. We just access every element by index and change its values.

const updatePath = function (path) {
for (let i = 0; i < path.length; i++) {
const x = path[i].x;
const z = path[i].z;
const y = 20 * Math.sin(i / 10);
path[i].x = x;
path[i].y = y;
path[i].z = z;
}
};
// update pathArray
for (let p = 0; p < pathArray.length; p++) {
updatePath(pathArray[p]);
}

The way to update our existing mesh is quite simple: just reuse the CreateRibbon() method and pass this mesh as the last parameter along with our modified pathArray.

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
mesh = MeshBuilder.CreateRibbon(null, { pathArray: pathArray, instance: mesh });

example : Dynamic Mesh Morph Example 2

Well, we just updated our ribbon's shape once for now.

If we now want its shape to evolve dynamically, we just need to move the pathArray computation, adjusted with an extra k parameter, and the CreateRibbon() call inside the render loop.

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
const updatePath = function (path, k) {
for (let i = 0; i < path.length; i++) {
const x = path[i].x;
const z = path[i].z;
const y = 20 * Math.sin(i / 10) * Math.sin(k + z / 40);
path[i].x = x;
path[i].y = y;
path[i].z = z;
}
};
// path array population ...
const mesh = MeshBuilder.CreateRibbon("ribbon", { pathArray: pathArray, closeArray: false, closePath: false, offset: 0, updatable: true }, scene);
// morphing
const k = 0;
scene.registerBeforeRender(function () {
// path array update
for (let p = 0; p < pathArray.length; p++) {
updatePath(pathArray[p], k);
}
// ribbon update
mesh = MeshBuilder.CreateRibbon(null, { pathArray: pathArray, instance: mesh });
k += 0.05;
});

example : Dynamic Mesh Morph Example 3

Lines and DashedLines

lines and dashed lines updatable parameter : points

lines optional updatable parameter : colors

(reminder : only points positions can change, not the number of points. Please see the summary at the bottom of this page for more details)

Once we understand ribbons, we can apply the same idea to Lines or DashedLines. It's even easier because Lines only require a path of points as a parameter.

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
const points1 = [v1, v2, ..., vN]; // vector3 array
let lines = MeshBuilder.CreateLines("lines", {points: points1, updatable: true}, scene, true);
let dashedLines = MeshBuilder.CreateDashedLines("lines", { points: points1, dashSize: dashSize, gapSize: gapSize, dashNb: nb, updatable: true }, scene);
lines = MeshBuilder.CreateLines(null, {points: points2, instance: lines});
dashedLines = MeshBuilder.CreateDashedLines(null, {points: points2, instance: dashedLines});

We can, of course, set the update method within the render loop.

example lines: Lines Example dashed lines: Dashed Lines Example

Tube

tube updatable parameters : path, radius, radiusFunction

(reminder : only points positions can change in the path, not the number of points. Please see the summary at the bottom of this page for more details)

Nothing changes for tubes. Let's create a tube and then update it according to new path, radius, or radiusFunction values:

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
const path1 = [v1, ..., vN]; //vector3 array : tube axis1
const radius1 = 5;
const path2 = [u1, ..., uN]; // another vector3 array : tube axis2
const radius2 = 8;
const tube = MeshBuilder.CreateTube("tube", { path: path1, radius: radius1, tessellation: 12, cap: cap, updatable: true }, scene);
tube = MeshBuilder.CreateTube(null, { path: path2, radius: radius2, instance: tube });

Of course, it also works with the radiusFunction parameter :

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
const radiusFunction1 = function(i, distance) { ... };
const radiusFunction2 = function(i, distance) { ... };
let tube = MeshBuilder.CreateTube("tube", { path: path1, tessellation: 12, radiusFunction: radiusFunction1, cap, updatable: true }, scene);
tube = MeshBuilder.CreateTube(null, {path: path2, radiusFunction: radiusFunction2, instance: tube});

Example: Tube Example

As you can read at line 53, the radiusFunction is redefined here at each iteration in the registerBeforeRender loop because it uses the value of the incrementing parameter k: the radius changes according to each path point position and as k changes over time.

Extruded shape

extrusion updatable parameters for ExtrudeShape(): shape, path, scale, rotation

extrusion updatable parameters for ExtrudeShapeCustom(): shape, path, scaleFunction, rotateFunction

(reminder : only points positions can change in the path, not the number of points. Please see the summary at the bottom of this page for more details)

We can think of extrusion as a generalization of a tube: a tube would be a circular shape extruded along a path.
So we have the same update capabilities as for a tube (path or shape scale, which would be equivalent to the tube radius) and some extra parameters: the rotation step can be updated, and the shape itself can be updated too.
The new shape array must have the same number of Vector3 values as the shape used to build the original instance. A good way to ensure this is simply to keep the original shape instance and modify it instead of creating a new one, or to modify a copy of it.

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
const shape1 = [s1, s2, ...sN]; // Vector3 array
const shape2 = [t1, t2, ...tN]; // another Vector3 array
const path1 = [p1, p2, ...pN]; // Vector3 array
const path2 = [q1, q2, ...qN]; // another Vector3 array
const scale1 = 1;
const scale2 = 3;
const rotation1 = 0;
const rotation2 = 0.2;
// extrusion
let extruded = MeshBuilder.ExtrudeShape("ext", { shape: shape1, path: path1, scale: scale1, rotation: rotation1, cap, updatable: true }, scene);
// mesh update
extruded = MeshBuilder.ExtrudeShape(null, { shape: shape2, path: path2, scale: scale2, rotation: rotation2, instance: extruded });

The same applies to ExtrudeShapeCustom(), which accepts scaleFunction and rotationFunction parameters:

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
// path and shape const declared before ...
const myScale1 = function(i, distance) { ... };
const myScale2 = function(i, distance) { ... };
const myRotation1 = function(i, distance) { ... };
const myRotation2 = function(i, distance) { ... };
// extrusion
let ext = MeshBuilder.ExtrudeShapeCustom("ext", { shape: shape1, path: path1, scaleFunction: myScale1, rotationFunction: myRotation1, updatable: true, cap: cap }, scene);
// mesh update
ext = MeshBuilder.ExtrudeShapeCustom(null,{shape: shape2, path: path2, scaleFunction: myScale2, rotationFunction: myRotation2, instance: ext});

Both new functions can be used in the render loop.

The funny part is, as ExtrudeShape() and ExtrudedShapeCustom() build the same mesh (only parameters change), you can create a simple extruded shape with ExtrudeShape() and then morph it with ExtrudeShapeCustom() if you need more complexity.

import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
let ext = MeshBuilder.ExtrudeShape("ext", { shape: shape1, path: path1, scale: scale1, rotation: rotation1, cap: cap, updatable: true }, scene);
// mesh update
ext = MeshBuilder.ExtrudeShapeCustom(null, { shape: shape2, path: path2, scaleFunction: myScale2, rotationFunction: myRotation2, instance: ext });

Example: Extruded Shape Example

Other shapes : updateMeshPositions

So far, we have only talked about parametric shapes, which can be updated with their own initial CreateXXX() method. But what about other mesh types such as boxes, spheres, cylinders, and so on?

There is no way to update them with their initial CreateXXX() method, because a box remains a box and a sphere remains a sphere even if you change its size, radius, and so on. So usually, the right way to change these basic shapes is to play with their mesh.scale property.

Nevertheless, if you create your basic shape with its updatable parameter set to true, you can use another way to morph or change the shape afterward: the updateMeshPositions() method.

This method needs two parameters :

  • a positionFunction, which is a JavaScript function that modifies the mesh positions array,
  • a computeNormals boolean (default = true) to skip/unskip the normals re-computation after the mesh update .
import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
const box = MeshBuilder.CreateBox("box", { size: 5.0, updatable: true }, scene);
const positionFunction = function (positions) {
// modify positions array values here
};
box.updateMeshPositions(positionFunction, true);

Example: Custom Shape Example

More speed : freezeNormals

The former CreateXXX() update functions try to be as much optimized as possible to run fast in the render loop.
However, you may need more speed for any reason, for instance with a huge mesh containing tens of thousands of vertices.
So, if your mesh doesn't need to reflect light, for example if it uses only an emissive color, you can skip normal recomputation, which is a CPU-intensive process.
In that case, use the freezeNormals() method just after your mesh is created:

import { Mesh } from "@babylonjs/core/Meshes/mesh";
import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
const tube = MeshBuilder.CreateTube("tube", { path: path, radius: 3, tessellation: 12, cap: Mesh.NO_CAP, updatable: true }, scene);
tube.freezeNormals();
// path update here ...
tube = MeshBuilder.CreateTube(null, { path: path, instance: tube });

If you need to turn normal computation back on, simply use the unfreezeNormals() method.

tube.unfreezeNormals();

The normals will then be recomputed and re-applied on the next CreateXXX() update call.

Summary

  • To create an updatable mesh, you must set its updatable parameter to true when calling the CreateXXX() method.
  • To update an existing parametric shape, you just have to use the same CreateXXX method that you used to construct it.
  • Only the existing mesh and the data relative to new positions (path, pathArray, array of points) must be passed to this method, the other parameters are ignored.
  • If we want to morph the mesh, we then use the CreateXXX() method within the render loop.
    In this case, it is important not to allocate new memory each frame: we access our arrays by index and just change values instead of creating new arrays, and we access existing objects instead of instantiating new ones. We also pay attention to the complexity of each object, such as the number of sides and vertices.

Example: if we need to update a Lines mesh in the render loop, that is, update the points array each frame, it is better to change each array element's values (points[i].x = newXValue; points[i].y = newYValue; points[i].z = newZValue;) in a for loop instead of creating a new points array.

Use case with a path parameter, as all parametric shapes have one:

const path = [v1, v2, ..., vN]; // your own array, created once
const mesh = CreateXXX("mesh", path, etc, ..., scene, true); // updatable = true : creates your initial mesh
const computeValue = function(val1, val2) { ... }; // your update logic : returns a value in function of val1 and val2
const getParam = function() { ... }; // returns a param value evolving in the render loop
const updatePath = function(path, k) { // updates the existing path array elements
for (let i = 0; i < path.length; i++) {
const x = computeValue(path[i].x, k);
const y = computeValue(path[i].y, k);
const z = computeValue(path[i].z, k);
path[i].x = x;
path[i].y = y;
path[i].z = z;
}
};
scene.registerBeforeRender(function() {
const k = getParam();
updatePath(path, k);
mesh = CreateXXX(null, path, null, ..., null, mesh);
});