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);| variable | description |
|---|---|
| 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):
Compare with the following example which sets multiMultiMaterials to true:
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 exampleInitializing 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 ExampleThe 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