-
Notifications
You must be signed in to change notification settings - Fork 293
Scene Graphs
A scene graph forms the backbone of xeokit's 3D engine. It consists of Nodes that composed into hierarchies, with Meshes at the leaves. Meshes are Node subtypes.
A Node can represent a model, a 3D object, or just an anonymous grouping of child Nodes and Meshes.
Each Node has its own dynamic transformation and rendering attributes, which it applies recursively to the Nodes within its subtree.
A Node tree also represents a bounding volume hierarchy, where each Node has is dynamic World-space boundary, which contains the boundaries of its sub-Nodes.
We can also optionally load metadata into xeokit to apply classification schemas to our models and objects.
- See Scene Metadata for classifying scene content with metadata.
- See Importing Models for info on importing models into scene graphs.
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]
})
})
]
});
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"];
Let's periodically update transforms on our Nodes (Meshes):
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 can provide 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:
var tableBoundary = table.aabb; // [xmin, ymin, zmax, xmax, ymax, zmax]
var redLegBoundary = redLeg.aabb;
// Subscribe to boundary updates:
table.on("boundary", function(aabb) {
tableBoundary = aabb;
});
redLeg.on("boundary", function(aabb) {
redLegBoundary = aabb;
});
Each Node has its own rendering attributes, which it applies recursively to its sub-Nodes.