API

Merging Meshes

Native Babylon.js function

To easily merge a number of meshes into a single mesh, use the static MergeMeshes method of the Mesh class:

import { Mesh } from "@babylonjs/core/Meshes/mesh";
const newMesh = Mesh.MergeMeshes(arrayOfMeshes, disposeSource, allow32BitsIndices, meshSubclass, subdivideWithSubMeshes, multiMultiMaterials);
variabledescription
arrayOfMeshes
An array of Meshes. They should all be of the same material.
disposeSource (optional)
When true (default), the source meshes will be disposed upon completion.
allow32BitsIndices (optional)
When the sum of the vertices > 64k, this must be set to true.
meshSubclass (optional)
When set, vertices are inserted into this Mesh. Meshes can then be merged into a Mesh subclass.
subdivideWithSubMeshes (optional)
When true (false by default), subdivides the mesh into its subMesh array using the source meshes.
multiMultiMaterials (optional)
When true (false default), subdivide mesh and accept multiple multi materials, ignores subdivideWithSubMeshes.

Since multiMultiMaterials defaults to false, the resulting merged mesh will have only one material applied to it (taken from the first mesh):

Merged Meshes Example

Compare with the following example which sets multiMultiMaterials to true:

Merging Meshes With Multiple Materials

See this page for more details on usage of merged meshes.

Use your own merge function

If you want to merge meshes into a new one using a self-implemented function, you can use the following code as a basis and adapt it to your needs:

Note: Be careful. When you merge cloned meshes, you need to update the world matrix of each mesh with computeWorldMatrix before calling the function.

Note: This article covers the internal merging process. You can also use BABYLON.VertexData object and its merge() function for a simpler solution.

import { VertexBuffer } from "@babylonjs/core/Buffers/buffer";
import { Vector3 } from "@babylonjs/core/Maths/math.vector";
import { Mesh } from "@babylonjs/core/Meshes/mesh";
import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData";
function mergeMeshes(meshes, disposeSource) {
if (!meshes || meshes.length === 0) {
return null;
}
const validMeshes = [];
let totalVertices = 0;
let totalIndices = 0;
const has = {
normals: false,
uvs: false,
uv2s: false,
colors: false,
matricesIndices: false,
matricesWeights: false,
};
for (const mesh of meshes) {
const vd = mesh.getVertexBuffer(VertexBuffer.PositionKind);
if (!vd)
continue;
const vertCount = mesh.getTotalVertices();
if (!vertCount)
continue;
const indices = mesh.getIndices();
if (!indices)
continue;
validMeshes.push(mesh);
totalVertices += vertCount;
totalIndices += indices.length;
if (!has.normals && mesh.getVertexBuffer(VertexBuffer.NormalKind))
has.normals = true;
if (!has.uvs && mesh.getVertexBuffer(VertexBuffer.UVKind))
has.uvs = true;
if (!has.uv2s && mesh.getVertexBuffer(VertexBuffer.UV2Kind))
has.uv2s = true;
if (!has.colors && mesh.getVertexBuffer(VertexBuffer.ColorKind))
has.colors = true;
if (!has.matricesIndices && mesh.getVertexBuffer(VertexBuffer.MatricesIndicesKind))
has.matricesIndices = true;
if (!has.matricesWeights && mesh.getVertexBuffer(VertexBuffer.MatricesWeightsKind))
has.matricesWeights = true;
}
if (validMeshes.length === 0) {
return null;
}
const positions = new Float32Array(totalVertices * 3);
const normals = has.normals ? new Float32Array(totalVertices * 3) : null;
const uvs = has.uvs ? new Float32Array(totalVertices * 2) : null;
const uv2s = has.uv2s ? new Float32Array(totalVertices * 2) : null;
const colors = has.colors ? new Float32Array(totalVertices * 4) : null;
const matricesIndices = has.matricesIndices ? new Float32Array(totalVertices * 4) : null;
const matricesWeights = has.matricesWeights ? new Float32Array(totalVertices * 4) : null;
const indices = new Uint32Array(totalIndices);
let vertexOffset = 0;
let indexOffset = 0;
const vec3tmp1 = new Vector3();
const vec3tmp2 = new Vector3();
for (const mesh of validMeshes) {
const wm = mesh.computeWorldMatrix(true);
const vertCount = mesh.getTotalVertices();
const pos = mesh.getVerticesData(VertexBuffer.PositionKind);
const nor = has.normals ? mesh.getVerticesData(VertexBuffer.NormalKind) : null;
const uv = has.uvs ? mesh.getVerticesData(VertexBuffer.UVKind) : null;
const uv2 = has.uv2s ? mesh.getVerticesData(VertexBuffer.UV2Kind) : null;
const col = has.colors ? mesh.getVerticesData(VertexBuffer.ColorKind) : null;
const matInd = has.matricesIndices ? mesh.getVerticesData(VertexBuffer.MatricesIndicesKind) : null;
const matWgt = has.matricesWeights ? mesh.getVerticesData(VertexBuffer.MatricesWeightsKind) : null;
const ind = mesh.getIndices();
disposeSource && mesh.dispose();
if (pos) {
const posOffset = vertexOffset * 3;
for (let i = 0; i < vertCount; i++) {
const i3 = i * 3;
vec3tmp1.set(pos[i3], pos[i3 + 1], pos[i3 + 2]);
Vector3.TransformCoordinatesToRef(vec3tmp1, wm, vec3tmp2);
const targetIndex = posOffset + i3;
positions[targetIndex] = vec3tmp2.x;
positions[targetIndex + 1] = vec3tmp2.y;
positions[targetIndex + 2] = vec3tmp2.z;
}
}
if (nor && normals) {
const norOffset = vertexOffset * 3;
for (let i = 0; i < vertCount; i++) {
const i3 = i * 3;
vec3tmp1.set(nor[i3], nor[i3 + 1], nor[i3 + 2]);
Vector3.TransformNormalToRef(vec3tmp1, wm, vec3tmp2);
vec3tmp2.normalizeToRef(vec3tmp2);
const targetIndex = norOffset + i3;
normals[targetIndex] = vec3tmp2.x;
normals[targetIndex + 1] = vec3tmp2.y;
normals[targetIndex + 2] = vec3tmp2.z;
}
}
if (uv && uvs) {
uvs.set(uv, vertexOffset * 2);
}
if (uv2 && uv2s) {
uv2s.set(uv2, vertexOffset * 2);
}
if (col && colors) {
colors.set(col, vertexOffset * 4);
}
if (matInd && matricesIndices) {
matricesIndices.set(matInd, vertexOffset * 4);
}
if (matWgt && matricesWeights) {
matricesWeights.set(matWgt, vertexOffset * 4);
}
if (ind) {
for (let i = 0; i < ind.length; i++) {
indices[indexOffset + i] = ind[i] + vertexOffset;
}
indexOffset += ind.length;
}
vertexOffset += vertCount;
}
const vd = new VertexData();
vd.positions = positions;
if (normals)
vd.normals = normals;
if (uvs)
vd.uvs = uvs;
if (uv2s)
vd.uv2s = uv2s;
if (colors)
vd.colors = colors;
if (matricesIndices)
vd.matricesIndices = matricesIndices;
if (matricesWeights)
vd.matricesWeights = matricesWeights;
vd.indices = indices;
const merged = new Mesh("merged");
vd.applyToMesh(merged);
return merged;
}

Merging Meshes with Constructive Solid Geometry

You can construct complex meshes by using subtract, intersect, and add methods of the CSG2 class.

Complete example:

Complete CSG2 example

Initializing CSG2

Before you can use CSG2, you must initialize the Manifold library.

To do so you need to call the InitializeCSG2Async function:

import { InitializeCSG2Async } from "@babylonjs/core/Meshes/csg2";
await InitializeCSG2Async();

Please note that if you want to import the library yourself, you must call InitializeCSG2Async with the following information:

import { InitializeCSG2Async } from "@babylonjs/core/Meshes/csg2";
await InitializeCSG2Async({
manifoldInstance: ...
manifoldMeshInstance: ...
});

You can alternatively provide a link to the library itself:

import { InitializeCSG2Async } from "@babylonjs/core/Meshes/csg2";
await InitializeCSG2Async({
manifoldUrl: "https://unpkg.com/manifold-3d@2.5.1"
});

Using CSG2

Let's say you want to create a "pipe" shape with an inner and outer diameter (i.e., not just a "tube" mesh, which is a curved plane with no "thickness"). This can be constructed by first creating a "cylinder" mesh, and then subtracting a "tube" mesh from the inside of it.

import { CSG2 } from "@babylonjs/core/Meshes/csg2";
import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
import type { Mesh } from "@babylonjs/core/Meshes/mesh";
import type { Scene } from "@babylonjs/core/scene";
function createPipe(diamInner: number, diamOuter: number, height: number, scene: Scene): Mesh {
// Create the outer wall using a Cylinder mesh
const mOuter = MeshBuilder.CreateCylinder("mOuter", {
diameter: diamOuter,
height: height,
}, scene);
// Create the inner wall using a Cylinder mesh
const mInner = MeshBuilder.CreateCylinder("mOuter", {
diameter: diamInner,
height: height,
}, scene);
// Create CSG objects from each mesh
const outerCSG = CSG2.FromMesh(mOuter);
const innerCSG = CSG2.FromMesh(mInner);
// Create a new CSG object by subtracting the inner tube from the outer cylinder
const pipeCSG = outerCSG.subtract(innerCSG);
// Create the resulting mesh from the new CSG object
const mPipe = pipeCSG.toMesh("mPipe", scene);
// Dispose of the meshes, no longer needed
mInner.dispose();
mOuter.dispose();
outerCSG.dispose();
innerCSG.dispose();
// Return the result
return mPipe;
}

Playground example:

Pipe CSG Example

The FromMesh function accepts a second parameter that can be set to true if you do not want to work in world space. The toMesh function can take an option as the third parameter:

{
/**
* Rebuild normals (false by default)
*/
rebuildNormals: boolean;
/**
* True to center the mesh on 0,0,0. True by default
*/
centerMesh?: boolean;
}

Please note that you can also work directly with VertexData with the FromVertexData and toVertexData functions.

Subtract example:

CSG Subtract Example