Node Geometry
What Is Node Geometry
The NodeGeometry feature was inspired by Blender Geometry Nodes, where you can use a node system to build geometry procedurally. Think of it as a post-processing system for geometry. Ultimately, it produces geometry through a procedural approach.
Why Use Node Geometry?
Several reasons come to mind:
- The cost of downloading assets on the web is a strong limiting factor. Procedural data can help solve that by limiting the download to the core pieces, which are assembled later by the
NodeGeometrysystem. - The system is dynamic and can produce infinite variants at runtime. While many digital content creation tools can create procedural meshes with the same endless possibilities, the limiting factor again becomes downloading assets created offline. Examples include terrain or vegetation generation.
- It allows a new way of modeling in Babylon.js by assembling core shapes and experimenting with a node system.
Example
Here is a complete example of an advanced NodeGeometry which is used to generate a city with different buildings:
How to Use
The NodeGeometry class is a utility class, meaning it is autonomous and does not require access to an engine or a scene. So instantiation is straightforward:
import { NodeGeometry } from "@babylonjs/core/Meshes/Node/nodeGeometry";const nodeGeometry = new NodeGeometry("my node geometry");Once created, the system expects you to create a flow from a source to an endpoint. In a nutshell, NodeGeometry processes a VertexData structure (see more in the section Create Custom Meshes From Scratch).
By default, the NodeGeometry system supports the following sources:
- Box
- Capsule
- Cylinder
- Disc
- Grid
- Icosphere
- Mesh
- Plane
- Sphere
- Torus
Here is for example a complete graph that simply generates a sphere:
import { GeometryOutputBlock } from "@babylonjs/core/Meshes/Node/Blocks/geometryOutputBlock";import { SphereBlock } from "@babylonjs/core/Meshes/Node/Blocks/Sources/sphereBlock";import { NodeGeometry } from "@babylonjs/core/Meshes/Node/nodeGeometry";// Create node geometryvar nodegeo = new NodeGeometry("nodegeo");// Create source spherevar sphere = new SphereBlock("sphere");// Create outputvar output = new GeometryOutputBlock("geometryout");nodegeo.outputBlock = output;sphere.geometry.connectTo(output.geometry);While this example is very simple, it isn't hard to imagine how adding more operations to the graph will present opportunities to alter the geometry flow which is represented by the VertexData provided by the source node. Updating that VertexData will influence the geometry passed to the GeometryOutputBlock.
The GeometryOutputBlock will collect the final VertexData that can then be used to generate a mesh:
// Build and instantiate meshnodegeo.onBuildObservable.addOnce(() => { var mesh = nodegeo.createMesh("nodegeomesh");})nodegeo.build();Updating the Geometry Flow
Now that we have the basics in place, we can start to introduce blocks which will update the data.
The simplest blocks will be the setXXX blocks which focus on generating data for a specific mesh component like positions, normals, colors, uvs, etc.:
- setPositionsBlock
- setNormalsBlock
- setTangentsBlock
- setColorsBlock
- setUVsBlock
These blocks are updating the VertexData by generating an entry per vertex inside the geometry. For that specific reason you can imagine them like a loop going through the list of vertices and inserting data from their input.
One way to insert data into the block is to simply use a RandomBlock which will generate a random value. In this example we are setting random positions for each vertex in the sphere:
import { Vector3 } from "@babylonjs/core/Maths/math.vector";import { GeometryInputBlock } from "@babylonjs/core/Meshes/Node/Blocks/geometryInputBlock";import { GeometryOutputBlock } from "@babylonjs/core/Meshes/Node/Blocks/geometryOutputBlock";import { RandomBlock } from "@babylonjs/core/Meshes/Node/Blocks/randomBlock";import { SetPositionsBlock } from "@babylonjs/core/Meshes/Node/Blocks/Set/setPositionsBlock";import { SphereBlock } from "@babylonjs/core/Meshes/Node/Blocks/Sources/sphereBlock";import { NodeGeometryBlockConnectionPointTypes } from "@babylonjs/core/Meshes/Node/Enums/nodeGeometryConnectionPointTypes";import { NodeGeometry } from "@babylonjs/core/Meshes/Node/nodeGeometry";// Create node geometryvar nodegeo = new NodeGeometry("nodegeo");// Create source spherevar sphere = new SphereBlock("sphere");var setPositions = new SetPositionsBlock("set positions");sphere.geometry.connectTo(setPositions.geometry);var getRnd = new RandomBlock("random");var rndMin = new GeometryInputBlock("rndMin", NodeGeometryBlockConnectionPointTypes.Vector3);rndMin.value = Vector3.Zero();var rndMax = new GeometryInputBlock("rndMax", NodeGeometryBlockConnectionPointTypes.Vector3);rndMax.value = Vector3.One();rndMin.output.connectTo(getRnd.min);rndMax.output.connectTo(getRnd.max);getRnd.output.connectTo(setPositions.positions);// Create outputvar output = new GeometryOutputBlock("geometryout");nodegeo.outputBlock = output;setPositions.output.connectTo(output.geometry);// Build and instantiate meshnodegeo.onBuildObservable.addOnce(() => { var mesh = nodegeo.createMesh("nodegeomesh");});nodegeo.build();The setPositionsBlock will call the RandomBlock once per vertex to generate the final mesh:

Contextual Values
To go further you may want to READ from the geometry. To do that the system provides several contextual values that are capable of pulling data FROM the active geometry.
The active geometry is the geometry connected to the block that seeks to read contextual values. In our example below, that will be the SetPositionsBlock.
We can rewrite our graph but instead of adding random values directly we can add the normal of each vertex scaled by a random value to the position of each vertex.
The associated code is:
import { GeometryInputBlock } from "@babylonjs/core/Meshes/Node/Blocks/geometryInputBlock";import { GeometryOutputBlock } from "@babylonjs/core/Meshes/Node/Blocks/geometryOutputBlock";import { MathBlock, MathBlockOperations } from "@babylonjs/core/Meshes/Node/Blocks/mathBlock";import { RandomBlock } from "@babylonjs/core/Meshes/Node/Blocks/randomBlock";import { SetPositionsBlock } from "@babylonjs/core/Meshes/Node/Blocks/Set/setPositionsBlock";import { SphereBlock } from "@babylonjs/core/Meshes/Node/Blocks/Sources/sphereBlock";import { VectorConverterBlock } from "@babylonjs/core/Meshes/Node/Blocks/vectorConverterBlock";import { NodeGeometryBlockConnectionPointTypes } from "@babylonjs/core/Meshes/Node/Enums/nodeGeometryConnectionPointTypes";import { NodeGeometryContextualSources } from "@babylonjs/core/Meshes/Node/Enums/nodeGeometryContextualSources";import { NodeGeometry } from "@babylonjs/core/Meshes/Node/nodeGeometry";// Create node geometryvar nodeGeometry = new NodeGeometry("nodegeo");// GeometryInputBlockvar Positions = new GeometryInputBlock("Positions");Positions.contextualValue = NodeGeometryContextualSources.Positions;// MathBlockvar Add = new MathBlock("Add");Add.operation = MathBlockOperations.Add;// MathBlockvar Multiply = new MathBlock("Multiply");Multiply.operation = MathBlockOperations.Multiply;// GeometryInputBlockvar Normals = new GeometryInputBlock("Normals");Normals.contextualValue = NodeGeometryContextualSources.Normals;// VectorConverterBlockvar Converter = new VectorConverterBlock("Converter");// RandomBlockvar random = new RandomBlock("random");// GeometryInputBlockvar Min = new GeometryInputBlock("Min", NodeGeometryBlockConnectionPointTypes.Float);Min.value = 0;// GeometryInputBlockvar Max = new GeometryInputBlock("Max", NodeGeometryBlockConnectionPointTypes.Float);Max.value = 1;// SetPositionsBlockvar setpositions = new SetPositionsBlock("set positions");// SphereBlockvar sphere = new SphereBlock("sphere");// GeometryOutputBlockvar geometryout = new GeometryOutputBlock("geometryout");// Connectionssphere.geometry.connectTo(setpositions.geometry);Positions.output.connectTo(Add.left);Normals.output.connectTo(Multiply.left);Min.output.connectTo(random.min);Max.output.connectTo(random.max);random.output.connectTo(Converter.xIn);random.output.connectTo(Converter.yIn);random.output.connectTo(Converter.zIn);Converter.xyzOut.connectTo(Multiply.right);Multiply.output.connectTo(Add.right);Add.output.connectTo(setpositions.positions);setpositions.output.connectTo(geometryout.geometry);// Output nodesnodeGeometry.outputBlock = geometryout;nodegeo.onBuildObservable.addOnce(() => { var mesh = nodegeo.createMesh("nodegeomesh");});nodegeo.build();Which will generate the following mesh:

To better understand the graph, here is a visual representation. See the section on the Node Geometry Editor below.

Please note that we used the VectorConverter to produce a Vector3 out of the RandomBlock generating a float.
We are also using the MathBlock twice to get Add and Multiply operations. We also have access to all trigonometry operations with the GeometryTrigonometryBlock.
To complete this graph, we need to add a ComputeNormalsBlock to make sure the normals are rebuilt using the new positions:

This will produce our weird random based sphere:

The list of available contextual values contains the following:
- positions: Contextual value pointing at the positions array of the active geometry
- normals: Contextual value pointing at the normals array of the active geometry
- colors: Contextual value pointing at the colors array of the active geometry
- tangents: Contextual value pointing at the tangents array of the active geometry
- uvs1: Contextual value pointing at the uvs array of the active geometry
- uvs2: Contextual value pointing at the uvs2 array of the active geometry
- uvs3: Contextual value pointing at the uvs3 array of the active geometry
- uvs4: Contextual value pointing at the uvs4 array of the active geometry
- uvs5: Contextual value pointing at the uvs5 array of the active geometry
- uvs6: Contextual value pointing at the uvs6 array of the active geometry
- vertexID: Contextual value representing the vertex index of the current vertex of the active geometry
Many of the previous contextual values CANNOT be used with the InstantiateOnFacesBlock block. We will detail which of them can be used below.
Node Geometry Editor
As we can see above, creating even a relatively simple NodeGeometry with code can quickly become very long. This is why we introduced a visual tool to help build NodeGeometry graphs. Learn how to use the editor in the tools section of our documentation.
Instancing Geometries
Ok, now it is time to really unleash the core power of the NodeGeometry!
With the InstantiateOnVerticesBlock class and the InstantiateOnFacesBlock class, you can instantiate a new geometry per vertex, or multiple times per face.
So let's look at this graph:

The InstantiateOnVerticesBlock block is used to place a box on each vertex of the sphere:

You can apply rotation or scaling per instance by connecting values to the rotation and scaling inputs.
The density input can be used to affect a percentage of the overall vertices. For example, placing an instance of the box only on 15% of the active geometry's vertices.
The InstantiateOnFacesBlock class works similarly but generates several instances per face. This block does not have a density input but instead a count input that lets you decide how many instances in total you want to distribute across all faces of the active geometry.
When using an InstantiateOnFacesBlock, you can only use the following contextual values:
- positions: Contextual value pointing at the positions array of the active geometry
- normals: Contextual value pointing at the normals array of the active geometry
- faceID: Contextual value representing the face index of the current face of the active geometry
Controlling the Flow
The ConditionalBlock is the central block if we want to control what is going on with the geometry flow. It can help decide which branch of the node graph to take based on a condition that can be:
- Equals
- Not equals
- Greater than
- Greater or equals
- Lower than
- Lower or equals
- Xor
- Or
- And
For instance, we can decide to create a new sphere made of boxes, but we want one hemisphere to use one material and the other hemisphere to use another.
This graph will do it:

The ConditionBlock is used here with a Greater than setup and picks the value 0 or 1 based on the y value of the normal. The normal being evaluated belongs to the active geometry. In this case, the active geometry is the sphere, as it is the geometry manipulated by the InstantiateOnFacesBlock block. If the y value of the normal is greater than 0.2, it passes a value of 0 to the SetMaterialIDBlock. Otherwise, it passes a value of 1.
The outcome:

Random and Noise
In order to get random values, we have already seen the RandomBlock. However, there is another one that can be used which is the NoiseBlock.
This block will generate a noise pattern based on a Perlin noise algorithm.
Here is our example again with the SetPositions block:

Which will produce this mesh:

Material ID
If working with multiple mesh sources is desired, we can merge them easily with the MergeBlock:

Which will generate this mesh:

The generated mesh will be made of one unified geometry and be rendered with one draw call.
But we can go further and actually attach a material ID per geometry with the SetMaterialID block:

Which will generate this mesh:

The mesh will now have a list of subMeshes in mesh.subMeshes and will be rendered with a MultiMaterial using one material per ID.
Serialization
A NodeGeometry entity can be serialized to a JSON object:
geometry.serialize(true);The boolean indicates whether the geometry produced by the MeshBlock should also be serialized into the JSON object. Use caution when doing this because the resulting JSON can quickly become very large.
To load a NodeGeometry from a JSON object, call this code:
nodeGeometry.parseSerializedObject(json);If using the Node Geometry Editor instead, NodeGeometry can be loaded directly from our snippet server:
import { NodeGeometry } from "@babylonjs/core/Meshes/Node/nodeGeometry";const geometry = await NodeGeometry.ParseFromSnippetAsync("IJA02K#11");Loading and Updating From File
In addition to loading from the snippet server, node geometry can be loaded and parsed directly from local storage. This allows a common node geometry flow to be shared between multiple projects while allowing updates to input parameters or source meshes. Asset Manager is a great way to load node geometry files to your scene and can also be used to load meshes to use with the node geometry at the same time. Simply add a new TextFileAssetTask for each node geometry file that needs to be loaded.
import { AssetsManager } from "@babylonjs/core/Misc/assetsManager";const assetsManager = new AssetsManager(scene);const nodeGeometryFile = assetsManager.addTextFileTask("load my node geometry", "nodeGeometry.json");// load all tasksassetsManager.load();Asset Manager has a callback we can use to convert the loaded text into a JSON object and then parse it into node geometry.
import { NodeGeometry } from "@babylonjs/core/Meshes/Node/nodeGeometry";// callbackassetsManager.onFinish = async (tasks) { console.log("all tasks successful", tasks);
// files loaded as text need to be parsed to JSON to use const nodeGeometryJSON = JSON.parse(nodeGeometryFile.text);
// parse json object into node geometry const nodeGeometry = await NodeGeometry.Parse(nodeGeometryJSON);}Remember that we still need to build the node geometry and then use createMesh to actually render it into the scene. However, before we build the node geometry, this is the time to get any node that may need to be assigned a value such as a color, vector, or mesh. There are a few options for finding blocks within the graph, such as getBlockByName, getBlockByPredicate, or getInputBlocks. These methods can be used to find specific blocks and set their values.
import { Vector3 } from "@babylonjs/core/Maths/math.vector";nodeGeometry.getBlockByName("my_vector").value = new Vector3(1.0, 0.0, 1.0);nodeGeometry.getBlockByName("my_mesh").mesh = myLoadedMesh;Once we are done setting parameters or attaching meshes to our node geometry, we then call build and createMesh.
nodeGeometry.onBuildObservable.addOnce(() => { const myGeometry = nodeGeometry.createMesh("myGeometry");})nodeGeometry.build();The order of operations here is important. If node geometry is built and then we try to update any values on the blocks within the graph, no changes will be visible until nodeGeometry.build() is called and we call createMesh again. This also means that we can load and build a node geometry and keep it in memory until we need it with a call to createMesh. Or we could dispose of a mesh created from nodeGeometry and simply call createMesh again later to bring the node geometry back into the scene. In this way, node geometry acts a little like an Asset Container, where we always have it in memory and ready to create new meshes whenever we need them.
Optimizations
The NodeGeometry class uses the CPU to process data, which means that we have to be cautious if we expect to generate several meshes.
For instance let's take this graph:

We can see that the InstantiateOnVerticesBlock will call the Transform of the geometry flow for each vertex of the sphere. Note the use of the MergeBlock to combine multiple geometries. In this case, as nothing in the instance part of the graph is using contextual values - such as reading positions or normals - we can ask the GeometryTransformBlock to not reevaluate its context on each call.
To do so simply call:
myBlock.evaluateContext = false;Iterating and Debugging
When creating NodeGeometry in a scene, it can always be edited simply by launching the Node Geometry Editor with the NodeGeometry mesh selected in the inspector. The Edit button to launch the Node Geometry Editor can be found under the Node Geometry section of the mesh properties which appears right after the Transforms section.

Launching the editor from here will connect the editor to the scene allowing changes to the graph to be reflected in the scene. This is similar to the Node Material Editor with one specific difference. Where the Node Material Editor will automatically update the material in the scene whenever there is a change to the graph, the Node Geometry Editor requires a manual refresh to update the generated mesh. This is due to the increased time needed to build the graph and generate a new mesh which would cause performance issues if updates were forced with each change. To update the Node Geometry mesh in the connected scene, simply click on an empty part of the graph to get the top level options in the properties panel of the editor. Under the Sync section, there will be two buttons, one labeled Update mesh in scene and the second labeled Rebuild.

The Update in scene button will regenerate the mesh in the scene based on the current graph, so any changes made to the graph will be reflected in the new mesh. The Rebuild button will only rebuild the graph within the tool, which will update the mesh generated in the preview window. While the graph automatically rebuilds with every change to the graph, the rebuild button is useful when the graph includes nodes that produce procedurally generated output like Random or Instantiate. Pressing the rebuild node will allow testing of the graph to see the types of output generated by procedural nodes without needing to make a change to the graph itself.
There may be times when it is necessary to debug the interaction between the NodeGeometry graph and the code generating the scene. If, for some reason, the NodeGeometry graph fails to generate a mesh, the ability to open the Node Geometry Editor from the inspector is lost because there is no mesh holding the button that launches the editor. In cases like this, there is still a way to debug the graph by opening the Node Geometry Editor with the call:
nodeGeometry.edit();This will open the editor with the referenced graph loaded into it. Note, however, that launching the editor this way does not attach to a mesh in the scene which means the Update mesh in scene button will not be available.