Correct/performant way to build my own mesh/geometry?

Hi, I’m making something where I dynamically create a lot of large meshes. I am currently doing it like this as described in the docs:

        var mesh = new Mesh(name, scene)
        var vdat = new VertexData()

        vdat.positions = // big array of numbers
        vdat.normals =   // big array of numbers
        vdat.indices =   // big array of numbers
        vdat.colors =    // big array of numbers
        vdat.uvs =       // big array of numbers
        vdat.applyToMesh(mesh)

However there seem to be some drawbacks. E.g.:

  1. Babylon appears to look through the entire position array in order to check its bounds, which in my case I already know. I could easily create a BoundingInfo object, but I can’t find a way to get Babylon to use it.
  2. I’m not sure but it looks like some of the data arrays are being internally recreated (maybe to convert to typed arrays)?

There could be other issues. I understand that the default API is meant to be simple and easy to use, but in my case I’m creating lots and lots of large meshes. Is there any way that I can create custom meshes while avoiding internal overhead?

Your arrays should be Float32Array and not plain javascript array to avoid a conversion when applyToMesh is calling setVerticesData internally.

You can call mesh.getBoundingInfo().reConstruct(...) to set the min/max bounds of the bounding infos to overwrite the values computed by the applyToMesh call but I can’t see a way for you to stop Babylon doing the automatic reconstruction…

2 Likes

@Evgeni_Popov Thanks for the reply!

However, from looking at the source it looks like incoming data arrays get converted (i.e. duplicated) to typed arrays even if they were typed already.

Is this correct? It seems like a pretty big waste.

Also - even the indices array gets converted to Float32? Even though it’s all integers?

The conversion is done here:
https://github.com/BabylonJS/Babylon.js/blob/master/src/Meshes/geometry.ts#L238-L245

In fact it’s only done if you flag the data as updatable.

But later on, when we pass the data to WebGL, the API expects a ArrayBuffer / ArrayBufferView. If not, we convert it:
https://github.com/BabylonJS/Babylon.js/blob/master/src/Engines/thinEngine.ts#L1609-L1613

So, to be sure there’s no conversion even if the data are updatable, you should use a Float32Array.

Where are you seeing this?

Indices are generally of type IndicesArray, which is defined as type IndicesArray = number[] | Int32Array | Uint32Array | Uint16Array;.

In the low level layer it’s also IndicesArray or Uint16Array / Uint32Array:
https://github.com/BabylonJS/Babylon.js/blob/master/src/Engines/thinEngine.ts#L1641-L1682

Where are you seeing indices being converted to float?

Hi, thanks for the answers - the internals are quite hard to follow but I sort of see what’s going on.

That said, is there any more performant way to create meshes? Basically I have code that builds lots of dynamic meshes, something like this:

// part A
var vdat =   // lots of very heavy math to build a VertexData object
var ranges = // more heavy math to partition the data into subMesh ranges

// part B
var mesh = new Mesh(name, scene)
vdat.applyToMesh(mesh)
mesh.subMeshes = ranges.map(r => new SubMesh( /* .. */ ))

And basically what I’m finding is that Part B takes a significant amount of time compared to part A, even though all the work should already be done. So apparently Babylon is doing some heavy (unnecessary?) work by default, and I’m wondering if there’s any way to skip such work.

E.g. can I perhaps create a Geometry object manually and then somehow create a Mesh around it? Or something like that. I’m not sure what Babylon is doing, but I’m finding that even new Mesh(name, scene) takes a measurable amount of time, so I’m wondering if I’m invoking lots of stuff that could be skipped.

Thanks for any info!

I don’t think there’s other ways to create meshes… Part B is what all the mesh builders (sphere, plane, etc) are doing.

Regarding the constructor of mesh taking some time, it may be because it calls refreshBoundingInfo that computes the min/max bounding box/sphere. What you could do is temporarily overrides this method so that it does nothing:

const saved = BABYLON.Mesh.prototype.refreshBoundingInfo;
BABYLON.Mesh.prototype.refreshBoundingInfo = function() { }
...your work...
BABYLON.Mesh.prototype.refreshBoundingInfo = saved

[…] My bad, it is calling refreshBoundingInfo only if passing a source mesh to the constructor (4th parameter)!

I don’t really see why the mesh constructor would be so slow, you could maybe try to use the performance tab of the browser and see what’s going on? Even for the applyToMesh call, maybe there’s something to optimize here.

I’m profiling my application which uses @fenomas’s noa code (GitHub - fenomas/noa: Experimental voxel game engine.)

thinEngine._createVertexBuffer is the biggest culprit. Babylon.js/thinEngine.ts at master · BabylonJS/Babylon.js · GitHub

the createBuffer call is showing up inside createVertexBuffer:
image

so it seems likely that’s not the culprit. Unsure which of the other calls are likely to be the problem.

Perhaps gl.bufferData? Would this allocating memory on the GPU be the dominant cost? (Our meshes here are for voxel chunks. so much of the time, when a block is placed or broken, we have an existing mesh. perhaps gl.bufferSubData would make a big difference?)

OpenGL glBufferData cause stuttering while loading terrain LOD - #2 by Silence - OpenGL: Basic Coding - Khronos Forums suggests bufferSubData but you are the expert, and perhaps unfeasible with the babylon api.

I think the problem is to call createVertexBuffer at each frame (maybe several times). It would be better to create the vertex buffer once and update it (with ThinEngine.updateArrayBuffer, which uses bufferSubData), but I don’t know if this is possible, I don’t know this voxel engine.

On the other hand, do you know Divine Voxel Engine - Voxel Engine Made With Babylon.js - Alpha 1.0 Out Now ? Maybe it could be an alternative choice.

This is all I do for DVE:


export class DVEMesh {
 meshes: Mesh[] = [];
 pickable = false;
 checkCollisions = false;
 seralize = false;
 clearCachedGeometry = false;
 defaultBb: BoundingInfo;

 constructor(public name: string, public dveMat: DVEMaterial) {
  this.defaultBb = new DVEBabylon.system.BoundingInfo(
   DVEBabylon.system.Vector3.Zero(),
   new DVEBabylon.system.Vector3(16, 16, 16)
  );
 }

 createTemplateMesh(scene: Scene) {
  let mesh = this.meshes.shift();
  if (!mesh) {
   mesh = new DVEBabylon.system.Mesh(this.name, scene);
   this._setEmptyData(mesh);
  } else {
   mesh.setEnabled(true);
  }

  mesh.isPickable = this.pickable;
  mesh.checkCollisions = this.checkCollisions;
  (mesh as any).type = "chunk";
  mesh.alwaysSelectAsActiveMesh = true;

  if (!this.checkCollisions) {
   mesh.doNotSyncBoundingInfo = true;
  }

  mesh.doNotSerialize = this.seralize;
  mesh.cullingStrategy = DVEBabylon.system.Mesh.CULLINGSTRATEGY_STANDARD;
  mesh.material = this.dveMat.getMaterial();
  mesh.isVisible = false;
  mesh.setEnabled(false);
  return mesh;
 }

 syncSettings(settings: EngineSettingsData) {
  if (settings.meshes.pickable) {
   this.pickable = true;
  }
  if (settings.meshes.clearChachedGeometry) {
   this.clearCachedGeometry = true;
  }
  if (settings.meshes.seralize) {
   this.seralize = true;
  }
 }

 _setEmptyData(mesh: Mesh) {
  let chunkVertexData = (mesh as any).vertexData;
  if (!chunkVertexData) {
   chunkVertexData = new DVEBabylon.system.VertexData();
   (mesh as any).vertexData = chunkVertexData;
  }

  mesh.position.x = 0;
  mesh.position.y = 0;
  mesh.position.z = 0;
  chunkVertexData.positions = [0];
  chunkVertexData.normals = [0];
  chunkVertexData.indices = [0];
  mesh.setVerticesData("voxelData", [0], false, 1);
  mesh.setVerticesData("cuv3", [0], false, 3);
  mesh.setVerticesData("ocuv3", [0], false, 4);
  mesh.setVerticesData("colors", [0], false, 4);
  chunkVertexData.applyToMesh(mesh, false);
 }

 _clearCached(mesh: Mesh) {
  if (this.clearCachedGeometry) {
   if (mesh.subMeshes) {
    for (const sm of mesh.subMeshes) {
     sm.setBoundingInfo(this.defaultBb);
    }
   }
   mesh.geometry?.clearCachedData();
  }
 }

 removeMesh(mesh: Mesh) {
  this._clearCached(mesh);
  this._setEmptyData(mesh);
  this.meshes.push(mesh);
 }

 async setMeshData(mesh: Mesh, location: LocationData, data: ChunkMeshData) {
  mesh.unfreezeWorldMatrix();
  mesh.position.x = location[1];
  mesh.position.y = location[2];
  mesh.position.z = location[3];

  const chunkVertexData: VertexData = (mesh as any).vertexData;
  chunkVertexData.positions = data[1];
  chunkVertexData.normals = data[2];
  chunkVertexData.indices = data[3];
  mesh.setVerticesData("voxelData", data[4], false, 1);
  mesh.setVerticesData("cuv3", data[5], false, 3);

  mesh.setVerticesData("ocuv3", data[6], false, 4);
  mesh.setVerticesData("colors", data[7], false, 4);

  chunkVertexData.applyToMesh(mesh, false);

  this._clearCached(mesh);

  mesh.freezeWorldMatrix();
  return mesh;
 }
}

With the ChunkMeshData type being:

export type ChunkMeshData = [
 substanceType: VoxelTemplateSubstanceType,
 positions: Float32Array,
 normals: Float32Array,
 indices: Uint16Array,
 voxelData: Float32Array,
 colors: Float32Array,
 uvs: Float32Array,
 overlayUVs: Float32Array
];

The chunk mesh is built in a web worker and then the mesh’s data is transferred over.

I am not sure what the NOA engine is doing but creating a vertex buffer every frame seems excessive.

If you are some how trying to animate meshes by updating their vertex data you would use the same buffer (probably a SharedArrayBuffer) and somehow mark the buffer as dirty/updated so the same view of the data would be passed again to be rendered.

1 Like

Okay so I did discover something in relation to what could be a bottleneck for setting meshes from raw data.
I first tried to search if BJS was using VAOs and it looks it does under the hood so the next thing I looked at was the bounding box.

Even though I have doNotSyncBoundingInfo set to true for the mesh the bounding info was still being updated. I guess I never bothered to checked if the bounding info stayed 16x16x16.

I added this to the DVEMesh class createTemplateMesh function to stop it from being updated:

  mesh.buildBoundingInfo = () => {
   return this.defaultBb;
  };
  mesh._updateBoundingInfo = () => {
   return mesh;
  };
  mesh._refreshBoundingInfo = () => {
   return mesh;
  };

So, maybe I don’t understand what doNotSyncBoundingInfo is meant to do. But either way that seemed to make mesh setting for DVE faster.

@Evgeni_Popov I have an example of it in a playground:

Hi - for what Noa is doing here, currently it does something like this:

var nf = faceData.numFaces
var indices = new Uint16Array(nf * 6)
var positions = new Float32Array(nf * 12)
var normals = new Float32Array(nf * 12)
var colors = new Float32Array(nf * 16)
var uvs = new Float32Array(nf * 8)
if (usesAtlas) var atlasIndexes = new Float32Array(nf * 4)

// build all the data...

var name = `chunk_${chunk.requestID}_${terrainID}`
var mesh = new Mesh(name, scene)
mesh.doNotSyncBoundingInfo = true

var vdat = new VertexData()
vdat.positions = positions
vdat.indices = indices
vdat.normals = normals
vdat.colors = colors
vdat.uvs = uvs
vdat.applyToMesh(mesh)
if (usesAtlas) mesh.setVerticesData('texAtlasIndices', atlasIndexes, false, 1)

Then when the chunk is no longer needed I dispose it - I’m not pooling mesh objects or using updatable meshes.

In my stress-test scene, neither _createVertexBuffer nor bounding box updates seem to be taking significant time (anymore?) so I haven’t looked at them closely. However I’ve worked on this code recently, so I’m not sure if the same was true a month or a year ago.

mesh.setVerticesData creates a new VertexBuffer each time it is called. So, if you have to update data each frame, it’s better to use mesh.updateVerticesDataDirectly instead.

Are you calling mesh.updateVerticesData with a true value as the 3rd parameter? If you do, you should pass false, or use mesh.updateVerticesDataDirectly instead, which never updates the bounding box and which executes a little less code than updateVerticesData.

The bounding box is still created/set at creation time: doNotSyncBoundingInfo applies only when the bounding info must be updated afterwards.

If you want to avoid the bounding box recalculation at creation time, you can do something like this (this is what the glTFLoader is doing):

1 Like

Awesome thank you for letting me know about how/when the geometry gets updated I will make those changes to DVE.

This topic also got my interest lately cause I officerly started working at Bitreel and been dealing with some performance issues when creating meshes from raw data. But since we know the bounding box ahead of time I was able to bypass this process on the front end and make loading in sectors way faster.