Instancing
Instancing is an efficient way to quickly duplicate individual parts or whole hierarchies of a model on the client. Instead of creating new meshes for the instanced entities, the HOOPS Web Viewer allows you to reference existing meshes within the duplicated node hierarchy.
This walkthrough describes code that is very similar to the code in the Multiple Instance example from the Communicator package. For the working sample, see Multiple Instance in the source of the web_viewer/examples directory.
The full source code is available at the bottom of the page.
Creating multiple instances from a part of a model
In the following code snippet, we are retrieving the part of the model that is currently selected by the user and then creating a new instance of the selected part in the WebViewer, offsetting it from the original model with a translation value that we’ll specify.
Here’s the model that we’ll use as the basis for our new instances:
Original model
First let’s define our translation offset. This will be applied to each part that we instance:
let translation_X = 100;
let translation_Y = 0;
let translation_Z = 0;
Depending on your model, your units may be significantly higher or lower, so be sure to adjust these values accordingly.
Next, we’ll define a function to gather the selected nodes. After performing a selection in the WebViewer, call this function to query the WebViewer’s selectionManager, and iterate through the results:
Selected parts in the WebViewer
var gatherLeafNodeIds = function () {
var selectionManager = hwv.selectionManager;
var selectionItems = selectionManager.getResults();
var selectedNodes = [];
for (var i = 0; i < selectionItems.length; i++) {
var selectionItem = selectionItems[i];
if (selectionItem !== null) {
var myNodeId = selectionItem.getNodeId();
selectedNodes.push(myNodeId);
}
}
return gatherChildLeafNodes(selectedNodes);
};
For any results returned by the selectionManager, we’ll add those node IDs to an array that we’ll use later to create our instances.
In the function above, there’s a call to gatherChildLeafNodes()
. This function, which is displayed below, is necessary because nodes returned from the selectionManager
can contain nodes with child nodes that themselves contain geometry. In order to retrieve the correct nodes for our new instances, we need to traverse down to the leaf nodes and gather those child node IDs.
Instead of using a recursive function, we’ll create a queue of nodes` that we'll query to see if they contain any child nodes. If they don't have any children, then they are a leaf node and will be added to our ``leaves
array, otherwise, we’ll add the node
to our traversal queue and continue down the tree:
var gatherChildLeafNodes = function (startNodes) {
var model = hwv.model;
var nodes = startNodes.slice();
var leaves = [];
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
var kids = model.getNodeChildren(node);
if (kids.length === 0) {
leaves.push(node);
}
for (var j = 0; j < kids.length; j++) {
var kid = kids[j];
nodes.push(kid);
}
}
return leaves;
};
When we call the function to gather the nodes for instancing, we’ll also need to gather all of the meshes within each node, since the mesh data is what we’ll actually be instancing (including color attributes). To do so, we’ll call the map() javascript function to create an array of arrays. Each element will contain an array with a model key as well as an index of the mesh id within the model:
var model = hwv.model;
var leafNodeIds = gatherLeafNodeIds();
var meshIdPromises = leafNodeIds.map(function (leafNodeId) {
return model.getMeshIds([leafNodeId]);
});
We’ll pass this data to build the properties for our new instances:
Promise.all(meshIdPromises).then(function (meshIdsByLeafNode) {
model.getNodesEffectiveFaceColor(leafNodeIds).then(function (myFaceColor) {
model.getNodesEffectiveLineColor(leafNodeIds).then(function (myLineColor) {
var currentMeshIndex = 0;
for (var i = 0; i < meshIdsByLeafNode.length; ++i) {
var myMeshIds = meshIdsByLeafNode[i];
var myNodeId = leafNodeIds[i];
var translationMatrix = model.getNodeNetMatrix(myNodeId);
translationMatrix.m[12] += translation_X; // Add translation to the X-axis.
translationMatrix.m[13] += translation_Y; // Add translation to the Y-axis.
translationMatrix.m[14] += translation_Z; // Add translation to the Z-axis.
for (var j = 0; j < myMeshIds.length; j++) {
var myMeshId = myMeshIds[j];
var myMeshInstanceData = new Communicator.MeshInstanceData(
myMeshId,
translationMatrix,
null,
myFaceColor[currentMeshIndex],
myLineColor[currentMeshIndex],
);
model.createMeshInstance(myMeshInstanceData);
currentMeshIndex++;
}
}
});
});
});
In this snippet, we’ve chained three promises together. This instructs the program to wait until all the necessary data has been returned before moving on to the next instruction. If we were to call these functions without waiting for the promises to resolve, then we would be subject to a race condition in which our new mesh instances may be created with incorrect values; chaining the promises together prevents that.
The first promise Promise.all(meshIdPromises)
will – before continuing to the next instruction – execute all of the promises that call getMeshIds()
for each leaf node; this will create the array of arrays containing the mesh ids for each node.
The functions getNodesEffectiveFaceColor and getNodesEffectiveLineColor are used here instead of their plain-vanilla counterparts (i.e, getNodesFaceColor and getNodesLineColor) because we need to retrieve the effective color attributes that are being displayed rather than the color set on a particular node, since in many cases color attributes may be inherited from a parent node.
Another important piece of this example code is the call to hwv.model.getNodeNetMatrix(myNodeID). Here we are retrieving the net matrix of our original prototype so that when multiple pieces are instanced they still fit together properly. Skipping this step would result in the newly instanced pieces having unpredictable translation and rotation.
Now that we have our net matrix, we can modify the new instance’s translation matrix manually by adding our previously defined translation values to the existing net translation matrix. We’ll pass the results as a parameter to the constructor of MeshInstanceData, at which point we’ll be ready to call createMeshInstance() for our new instance.
Instanced parts in the WebViewer
Full instancing source code
//! [webviewer_instance_child_leaf_nodes]
var gatherChildLeafNodes = function (startNodes) {
var model = hwv.model;
var nodes = startNodes.slice();
var leaves = [];
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
var kids = model.getNodeChildren(node);
if (kids.length === 0) {
leaves.push(node);
}
for (var j = 0; j < kids.length; j++) {
var kid = kids[j];
nodes.push(kid);
}
}
return leaves;
};
//! [webviewer_instance_child_leaf_nodes]
//! [webviewer_instance_select]
var gatherLeafNodeIds = function () {
var selectionManager = hwv.selectionManager;
var selectionItems = selectionManager.getResults();
var selectedNodes = [];
for (var i = 0; i < selectionItems.length; i++) {
var selectionItem = selectionItems[i];
if (selectionItem !== null) {
var myNodeId = selectionItem.getNodeId();
selectedNodes.push(myNodeId);
}
}
return gatherChildLeafNodes(selectedNodes);
};
//! [webviewer_instance_select]
//! [webviewer_instance_translation]
let translation_X = 100;
let translation_Y = 0;
let translation_Z = 0;
//! [webviewer_instance_translation]
//! [webviewer_instance_get_mesh_ids]
var model = hwv.model;
var leafNodeIds = gatherLeafNodeIds();
var meshIdPromises = leafNodeIds.map(function (leafNodeId) {
return model.getMeshIds([leafNodeId]);
});
//! [webviewer_instance_get_mesh_ids]
//! [webviewer_instance_create_mesh]
Promise.all(meshIdPromises).then(function (meshIdsByLeafNode) {
model.getNodesEffectiveFaceColor(leafNodeIds).then(function (myFaceColor) {
model.getNodesEffectiveLineColor(leafNodeIds).then(function (myLineColor) {
var currentMeshIndex = 0;
for (var i = 0; i < meshIdsByLeafNode.length; ++i) {
var myMeshIds = meshIdsByLeafNode[i];
var myNodeId = leafNodeIds[i];
var translationMatrix = model.getNodeNetMatrix(myNodeId);
translationMatrix.m[12] += translation_X; // Add translation to the X-axis.
translationMatrix.m[13] += translation_Y; // Add translation to the Y-axis.
translationMatrix.m[14] += translation_Z; // Add translation to the Z-axis.
for (var j = 0; j < myMeshIds.length; j++) {
var myMeshId = myMeshIds[j];
var myMeshInstanceData = new Communicator.MeshInstanceData(
myMeshId,
translationMatrix,
null,
myFaceColor[currentMeshIndex],
myLineColor[currentMeshIndex],
);
model.createMeshInstance(myMeshInstanceData);
currentMeshIndex++;
}
}
});
});
});
//! [webviewer_instance_create_mesh]