Skip to content

Mesh, Clones, and (Thin) Instances

Babylon.js supports several ways to render meshes, each with their advantages and disadvantages. Anu's scene graph APIs currently support creating Meshes, Clones, Instances, and Thin Instances. Since each of these work slightly differently from each other, not all of the methods in Anu's Selection class will work with all of them. This page will dive deeper into these four approaches, when to use them, and how to use them with Anu.

This page contains several live examples meant to demonstrate the performance of each rendering approach. Since they all run on the same page, their performance will be influenced by each other. Remember to reset the sliders to mitigate this effect.

Mesh

The standard method for Mesh rendering used internally by create() and bind() is to call Babylon.js's MeshBuilder methods. In this approach, each Mesh with be created with its own geometry and draw call. While this gives us the most control and flexibility over how we create and manipulate Meshes, it is also the most resource intensive for both the CPU and GPU. In typical usage, this approach will start to slow down Babylon.js after around 2000 draw calls.

Source
js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.

import { HemisphericLight, ArcRotateCamera, Vector3, Scene} from '@babylonjs/core';
import { AdvancedDynamicTexture, Control, SelectionPanel, SliderGroup} from '@babylonjs/gui';
import * as anu from '@jpmorganchase/anu' //import anu, this project is using a local import of babylon js located at ../babylonjs-anu this may not be the latest version and is used for simplicity.


//create and export a function that takes a babylon engine and returns a scene
export const meshBench = function(engine){

  const scene = new Scene(engine);

  new HemisphericLight('light1', new Vector3(0, 10, 0), scene);

  const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
  camera.position = new Vector3(-25, 10, -50);
  camera.attachControl(true);


  let n = 100

  let box = anu.bind('box', {}, [...Array(n).keys()], scene)
                .position(() => Vector3.Random(-20,20));

  let createBoxes = (num) => {
    box.dispose();
    box = anu.bind('box', {}, [...Array(num).keys()], scene)
    .position(() => Vector3.Random(-20,20));

  }
  
	var advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI("mesh");

  var selectBox = new SelectionPanel("mesh");
  selectBox.width = 0.25;
  selectBox.height = 0.25;

  selectBox.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
  selectBox.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
   
  advancedTexture.addControl(selectBox);

  var numGroup = new SliderGroup("1");
  numGroup.addSlider("Boxes", (value) => createBoxes(Math.round(value)), "", 100, 10000, 100, (value) => Math.round(value)) 

  
  selectBox.addGroup(numGroup);

  scene.onAfterRenderObservable.add(() => {
    numGroup.header = "FPS: " + scene.getEngine().getFps().toFixed();
  })


  return scene;

};

Clone

If we are drawing many Meshes with the same geometry but we still want them to be fully independent from each other, we can use Clones to reuse geometry and save a little bit of performance. We can do this with bindClone(), which accepts an existing Mesh whose geometry will be cloned.

js
anu.bindClone(mesh: Mesh, data: [], scene: Scene)
Source
js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.

import { HemisphericLight, ArcRotateCamera, Vector3, Scene} from '@babylonjs/core';
import { AdvancedDynamicTexture, Control, SelectionPanel, SliderGroup} from '@babylonjs/gui';
import * as anu from '@jpmorganchase/anu' //import anu, this project is using a local import of babylon js located at ../babylonjs-anu this may not be the latest version and is used for simplicity.


//create and export a function that takes a babylon engine and returns a scene
export const cloneBench = async function(engine){

  const scene = new Scene(engine);

  new HemisphericLight('light1', new Vector3(0, 10, 0), scene);

  const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
  camera.position = new Vector3(-25, 10, -50);
  camera.attachControl(true);

  let n = 100

  let root_box = anu.create('box', 'root_box')

  let box = anu.bindClone(root_box, [...Array(n).keys()], scene)
                .position(() => Vector3.Random(-20,20));

  let createBoxes = (num) => {
    box.dispose();
    box = anu.bindClone(root_box, [...Array(num).keys()], scene)
                .position(() => Vector3.Random(-20,20));

  }
  
	var advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI("mesh");

  var selectBox = new SelectionPanel("mesh");
  selectBox.width = 0.25;
  selectBox.height = 0.25;

  selectBox.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
  selectBox.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
   
  advancedTexture.addControl(selectBox);

  var numGroup = new SliderGroup("1");
  numGroup.addSlider("Boxes", (value) => createBoxes(Math.round(value)), "", 100, 10000, 100, (value) => Math.round(value)) 

  selectBox.addGroup(numGroup);

  scene.onAfterRenderObservable.add(() => {
    numGroup.header = "FPS: " + scene.getEngine().getFps().toFixed();
  })


  return scene;

};

Instance

If we are still drawing many Meshes with the same geometry, but we don't mind sacrificing some flexibility for the sake of performance, we can use Instances. InstancedMeshes all share geometry and are rendered in a single draw call, leading to much better GPU performance. They are also still represented as individual Nodes in the Babylon.js scene graph, and thus can still retain individual properties such as name, metadata, and transforms. However, setting properties such as color now need to use InstancedBuffers to be able to set unique values per each InstancedMesh.

Using Anu, the bindInstance() method can be used to easily create InstancedMeshes from data, which also accepts an existing Mesh whose geometry will be instanced.

js
anu.bindInstance(mesh: Mesh, data: [], scene: Scene)

To register or set InstancedBuffers we can use registerInstancedBuffer() and setInstancedBuffer() from a Selection object.

js

//Create a sphere to be used in our instance and register a color buffer
let rootSphere = anu.create('sphere', 'mySphere', {diameter: 0.003});
rootSphere.isVisible = false;
rootSphere.registerInstancedBuffer("color", 4);
rootSphere.instancedBuffers.color = new Color4(1,1,1,1);

let spheres =  anu.bindInstance(rootSphere, data)
                  .setInstancedBuffer("color", (d) => new Color4(0, 0, 0, 1));
Source
js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.

import { HemisphericLight, ArcRotateCamera, Vector3, Scene} from '@babylonjs/core';
import { AdvancedDynamicTexture, Control, SelectionPanel, SliderGroup} from '@babylonjs/gui';
import * as anu from '@jpmorganchase/anu' //import anu, this project is using a local import of babylon js located at ../babylonjs-anu this may not be the latest version and is used for simplicity.


//create and export a function that takes a babylon engine and returns a scene
export const instanceBench = function(engine){

  const scene = new Scene(engine);

  new HemisphericLight('light1', new Vector3(0, 10, 0), scene);

  const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
  camera.position = new Vector3(-25, 10, -50);
  camera.attachControl(true);


  let n = 100

  let box = anu.create('box', 'box');

  let boxes = anu.bindInstance(box, [...Array(n).keys()], scene)
                .position(() => Vector3.Random(-20,20));

  let createBoxes = (num) => {
    boxes.dispose();
    boxes = anu.bindInstance(box, [...Array(num).keys()], scene)
    .position(() => Vector3.Random(-20,20));

  }
  
	var advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI("instance");

  var selectBox = new SelectionPanel("instance");
  selectBox.width = 0.25;
  selectBox.height = 0.25;

  selectBox.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
  selectBox.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
   
  advancedTexture.addControl(selectBox);

  var numGroup = new SliderGroup("2",);
  numGroup.addSlider("Boxes", (value) => createBoxes(Math.round(value)), "", 100, 10000, 100, (value) => Math.round(value)) 

  
  selectBox.addGroup(numGroup);

  scene.onAfterRenderObservable.add(() => {
    numGroup.header = "FPS: " + scene.getEngine().getFps().toFixed();
  })


  return scene;

};

Thin Instance

Thin Instances are often the most performant way of drawing many identical Meshes at once, but come with the most restrictions in how you manipulate those Meshes. With Thin Instances, we are essentially writing directly to the GPU buffer which allows us to draw upwards of millions of Meshes in a single draw call. However, these Meshes are not represented in the scene graph and instead are under a single Mesh: the root Mesh of the Thin Instance. Additionally, when we want to modify a Mesh in the Thin Instance, we need to rewrite the entire matrix buffer to do so.

To support Thin Instances, Anu provides the bindThinInstance() method as well as several special Thin Instance specific methods to make modifying them more connivent.

js
bindThinInstance(mesh: Mesh, data: [], scene: Scene)
FunctionParameters
thinInstanceAttributeAt(attribute, index, value): Selectionattribute: string, index: number, value: any
thinInstanceColor(value, staticBuffer?): Selectionvalue: Color4 | (d: any, n: Node, i: number) => Color4, staticBuffer: boolean (default: false)
thinInstanceColorAt(index, value): Selectionindex: number, value: Color4 | (d: any, n: Node, i: number) => Color4
thinInstanceColorFor(method, value): Selectionmethod: (d: any, n: Node, i: number) => boolean, value: Color4 | (d: any, n: Node, i: number) => Color4
thinInstanceMatrixAt(index, value): Selectionindex: number, value: Matrix | (d: any, n: Node, i: number) => Matrix
thinInstanceMatrixFor(method, value): Selectionmethod: (d: any, n: Node, i: number) => boolean, value: Matrix | (d: any, n: Node, i: number) => Matrix
thinInstancePosition(value, staticBuffer?): Selectionvalue: Vector3 | (d: any, n: Node, i: number) => Vector3, staticBuffer: boolean (default: false)
thinInstancePositionAt(index, value): Selectionindex: number, value: Vector3 | (d: any, n: Node, i: number) => Vector3
thinInstancePositionFor(method, value): Selectionmethod: (d: any, n: Node, i: number) => boolean, value: Vector3 | (d: any, n: Node, i: number) => Vector3
thinInstanceRegisterAttribute(attribute, stride): Selectionattribute: string, stride: number
thinInstanceRotation(value, staticBuffer?): Selectionvalue: Vector3 | (d: any, n: Node, i: number) => Vector3, staticBuffer: boolean (default: false)
thinInstanceRotationAt(index, value): Selectionindex: number, value: Vector3 | (d: any, n: Node, i: number) => Vector3
thinInstanceRotationFor(method, value): Selectionmethod: (d: any, n: Node, i: number) => boolean, value: Vector3 | (d: any, n: Node, i: number) => Vector3
thinInstanceScaling(value, staticBuffer?): Selectionvalue: Vector3 | (d: any, n: Node, i: number) => Vector3, staticBuffer: boolean (default: false)
thinInstanceScalingAt(index, value): Selectionindex: number, value: Vector3 | (d: any, n: Node, i: number) => Vector3
thinInstanceScalingFor(method, value): Selectionmethod: (d: any, n: Node, i: number) => boolean, value: Vector3 | (d: any, n: Node, i: number) => Vector3
thinInstanceSetAttribute(attribute, value): Selectionattribute: string, value: any
thinInstanceSetBuffer(attribute, value, stride?, staticBuffer?): Selectionattribute: string, value: Float32Array | (d: any, n: Node, i: number) => Float32Array, stride?: number, staticBuffer: boolean (default: false)
Source
js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.

import { HemisphericLight, ArcRotateCamera, Vector3, Scene} from '@babylonjs/core';
import { AdvancedDynamicTexture, Control, SelectionPanel, SliderGroup} from '@babylonjs/gui';
import * as anu from '@jpmorganchase/anu' //import anu, this project is using a local import of babylon js located at ../babylonjs-anu this may not be the latest version and is used for simplicity.


//create and export a function that takes a babylon engine and returns a scene
export const thinInstanceBench = function(engine){

  const scene = new Scene(engine);

  new HemisphericLight('light1', new Vector3(0, 10, 0), scene);

  const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
  camera.position = new Vector3(-25, 10, -50);
  camera.attachControl(true);


  let n = 100

  let box = anu.create('box', 'box');

  let boxes = anu.bindThinInstance(box.clone(), [...Array(n).keys()], scene)
                .thinInstancePosition(() => Vector3.Random(-20,20));

  let createBoxes = (num) => {
    boxes.dispose()
    boxes = anu.bindThinInstance(box.clone(), [...Array(num).keys()], scene)
                .thinInstancePosition(() => Vector3.Random(-20,20));

  }
  
	var advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI("instance");

  var selectBox = new SelectionPanel("instance");
  selectBox.width = 0.25;
  selectBox.height = 0.25;

  selectBox.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
  selectBox.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
   
  advancedTexture.addControl(selectBox);

  var numGroup = new SliderGroup("2",);
  numGroup.addSlider("Boxes", (value) => createBoxes(Math.round(value)), "", 100, 1000000, 100, (value) => Math.round(value)) 

  
  selectBox.addGroup(numGroup);

  scene.onAfterRenderObservable.add(() => {
    numGroup.header = "FPS: " + scene.getEngine().getFps().toFixed();
  })


  return scene;

};