-
Notifications
You must be signed in to change notification settings - Fork 293
Scene Graphs
In this user guide you'll learn how to programmatically built a model using xeokit's scene graph representation, then classify it with metadata, which helps us navigate it.
Scene graphs are xeokit's standard method for constructing content, involving node trees with dynamic transforms and boundary hierarchies, with meshes that can have realistic materials etc.
While scene graphs are great for 3D gizmos and medium-sized models that need high-quality rendering, they don't scale up so well in terms of memory and GPU performance for huge CAD and BIM models. For those, we have the PerformanceModel representation, which you can read about in High Performance Models.
See also:
- Importing Models - loading models from files
- High Performance Models - an alternative high-performance scene representation for huge models
- Scene Graph Components
- Creating a Scene Graph
- Finding Entities
- Animating Entities
- Getting Boundaries
- Updating State
- Classifying with Metadata
- Querying Metadata
- Metadata Structure
A scene graph is a model representation consisting of Nodes composed into hierarchies, with Meshes at the leaves.
Each Node has its own dynamic transformation and rendering attributes, inherited by its sub-Nodes. A Node tree also represents a bounding volume hierarchy, where each Node has a dynamic World-space boundary, which contains the boundaries of its sub-Nodes.
Node and Mesh implement the abstract Entity interface. An Entity represents a model, an object, or just ab anonymous visible elements. An Entity has a unique ID and can be individually shown, hidden, selected, highlighted, ghosted, culled, picked and clipped, and has its own World-space boundary.
As mentioned earlier, an alternative method to build scene content is the PerformanceModel component, which also represents its model and objects as Entity types. The Entity type provides an abstract interface through which we can treat differently-implemented scene content uniformly.
In the example below we'll build a simple scene graph that represents a model of a table.
The root Node gets isModel:true
, which identifies it as a model,
while each child Node gets isObject: true
to identify them as objects.
We're then able to find the root Node by its ID in viewer.scene.models
and the
child Nodes by their IDs in viewer.scene.objects
.
Click the image below for a live demo.
import {Viewer} from "../src/viewer/Viewer.js";
import {Mesh} from "../src/scene/mesh/Mesh.js";
import {Node} from "../src/scene/nodes/Node.js";
import {PhongMaterial} from "../src/scene/materials/PhongMaterial.js";
import {buildBoxGeometry} from "../src/scene/geometry/builders/buildBoxGeometry.js";
import {ReadableGeometry} from "../src/scene/geometry/ReadableGeometry.js";
const myViewer = new Viewer({
canvasId: "myCanvas",
transparent: true
});
const boxGeometry = buildBoxGeometry(ReadableGeometry, myViewer.scene, {
xSize: 1,
ySize: 1,
zSize: 1
});
new Node(myViewer.scene, {
modelId: "table", // <---------- Node with "modelId" represents a model
rotation: [0, 50, 0],
position: [0, 0, 0],
scale: [1, 1, 1],
children: [
new Mesh(myViewer.scene, { // Red table leg
id: "redLeg",
isObject: true, // <---------- Node represents an object
position: [-4, -6, -4],
scale: [1, 3, 1],
rotation: [0, 0, 0],
geometry: boxGeometry,
material: new PhongMaterial(myViewer.scene, {
diffuse: [1, 0.3, 0.3]
})
}),
new Mesh(myViewer.scene, { // Green table leg
id: "greenLeg",
isObject: true, // <---------- Node represents an object
position: [4, -6, -4],
scale: [1, 3, 1],
rotation: [0, 0, 0],
geometry: boxGeometry,
material: new PhongMaterial(myViewer.scene, {
diffuse: [0.3, 1.0, 0.3]
})
}),
new Mesh(myViewer.scene, { // Blue table leg
id: "blueLeg",
isObject: true, // <---------- Node represents an object
position: [4, -6, 4],
scale: [1, 3, 1],
rotation: [0, 0, 0],
geometry: boxGeometry,
material: new PhongMaterial(myViewer.scene, {
diffuse: [0.3, 0.3, 1.0]
})
}),
new Mesh(myViewer.scene, { // Yellow table leg
id: "yellowLeg",
isObject: true, // <---------- Node represents an object
position: [-4, -6, 4],
scale: [1, 3, 1],
rotation: [0, 0, 0],
geometry: boxGeometry,
material: new PhongMaterial(myViewer.scene, {
diffuse: [1.0, 1.0, 0.0]
})
}),
new Mesh(myViewer.scene, { // Purple table top
id: "tableTop",
isObject: true, // <---------- Node represents an object
position: [0, -3, 0],
scale: [6, 0.5, 6],
rotation: [0, 0, 0],
geometry: boxGeometry,
material: new PhongMaterial(myViewer.scene, {
diffuse: [1.0, 0.3, 1.0]
})
})
]
});
Entity is the abstract base class for components that represent models, objects, or just anonymous visible elements. An Entity has a unique ID, and can be individually shown, hidden, selected, highlighted, ghosted, culled, picked and clipped, and queried for its World-space boundary.
In this example, our Entity's are implemented by Nodes. and Meshes, which also allow us to dynamically update their transforms, as we'll see in the next section.
Since the root Node has isModel: true
, we're able to find it by
ID in viewer.scene.models
, and since the child Nodes (Meshes)
each have isObject: true
we're able to find them in viewer.scene.objects
.
// Get the whole table model
const table = viewer.scene.model["table"];
// Get some leg objects
const redLeg = viewer.scene.objects["redLeg"];
const greenLeg = viewer.scene.objects["greenLeg"];
const blueLeg = viewer.scene.objects["blueLeg"];
Since our particular Entities are Nodes, which allow us to update their transforms, we'll go ahead and animate some transforms on them:
viewer.scene.on("tick", function() {
// Rotate legs
redLeg.rotateY(0.5);
greenLeg.rotateY(0.5);
blueLeg.rotateY(0.5);
// Rotate table
table.rotateY(0.5);
table.rotateX(0.3);
});
Each Node Entity provides its current axis-aligned World-space boundary, which dynamically updates as we transform, create or destroy Nodes within its subtree.
Get their boundaries like this:
// Get boundaries:
const tableBoundary = table.aabb; // [xmin, ymin, zmax, xmax, ymax, zmax]
const redLegBoundary = redLeg.aabb;
// Subscribe to boundary updates:
table.on("boundary", function(aabb) {
tableBoundary = aabb;
});
redLeg.on("boundary", function(aabb) {
redLegBoundary = aabb;
});
Each Node Entity has its own rendering attributes, which it applies recursively to its sub-Nodes. Let's highlight a table leg, then colorize the whole table and make it transparent.
redLeg.highlighted = true;
table.colorize = [1,0,0];
table.opacity = 0.4;
When we add a child to a parent Node, then the child will inherit rendering attributes from the parent by default. We can override that with a flag, as shown below.
If table
was colorized, as shown in the previous code
snippet, and we wanted to add a sub-Node without inheriting that attribute,
then we supply a flag param set false
, liks this:
table.addChild(new Node(myVewer.scene, { /* New Node's attributes */}, false);
Having created our scene graph, we'll now classify it with metadata.
We'll now create a MetaModel for our model, which will contain a MetaObject for each of it's objects.
The MetaModel and MetaObjects will have the same IDs as the Entities that represent their model and objects within our scene.
const furnitureMetaModel = viewer.metaScene // This is the MetaScene for the Viewer
.createMetaModel("furniture", { // Creates a MetaModel in the MetaScene
"projectId": "myTableProject",
"revisionId": "V1.0",
"metaObjects": [
{ // Creates a MetaObject in the MetaModel
"id": "table",
"name": "Table",
"type": "furniture", // Arbitrary type, could be IFC type
"properties": { // Arbitrary properties, could be IfcPropertySet
"cost": "200"
}
},
{
"id": "redLeg",
"name": "Red table Leg",
"type": "leg",
"parent": "table", // References first MetaObject as parent
"properties": {
"material": "wood"
}
},
{
"id": "greenLeg", // Node with corresponding id does not need to exist
"name": "Green table leg", // and MetaObject does not need to exist for Node with an id
"type": "leg",
"parent": "table",
"properties": {
"material": "wood"
}
},
{
"id": "blueLeg",
"name": "Blue table leg",
"type": "leg",
"parent": "table",
"properties": {
"material": "wood"
}
},
{
"id": "yellowLeg",
"name": "Yellow table leg",
"type": "leg",
"parent": "table",
"properties": {
"material": "wood"
}
},
{
"id": "tableTop",
"name": "Purple table top",
"type": "surface",
"parent": "table",
"properties": {
"material": "formica",
"width": "60",
"depth": "60",
"thickness": "5"
}
}
]
});
Having created and classified our scene graph, we can now find the MetaModel and MetaObjects using the IDs of their corresponding Entity's.
const furnitureMetaModel = scene.metaScene.metaModels["furniture"];
const redLegMetaObject = scene.metaScene.metaObjects["redLeg"];
In the snippet below, we'll log metadata on each Entity we click on:
viewer.scene.input.on("mouseclicked", function (coords) {
const hit = viewer.scene.pick({
canvasPos: coords
});
if (hit) {
const entity = hit.entity;
const metaObject = viewer.metaScene.metaObjects[entity.id];
if (metaObject) {
console.log(JSON.stringify(metaObject.getJSON(), null, "\t"));
}
}
});
The MetaModel also has its MetaObjects organized in a tree that describes their structural composition:
const tableMetaObject = furnitureMetaModel.rootMetaObject;
const redLegMetaObject = tableMetaObject.children[0];
const greenLegMetaObject = tableMetaObject.children[1];
const blueLegMetaObject = tableMetaObject.children[2];
const yellowLegMetaObject = tableMetaObject.children[3];
Given an Entity, we can find the object or model of which it is a part, or the objects that comprise it. We can also generate UI components from the metadata, such as the tree view demonstrated in this demo.