Skip to content

Scene Graphs

xeolabs edited this page Jan 22, 2019 · 28 revisions

A scene graph forms the backbone of xeokit's 3D engine. It consists of Nodes 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, 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.

See also:

Contents

Example

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]
            })
        })
    ]
});

Finding Nodes

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"];

Updating Nodes

Let's animate some transforms on our Nodes:

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);
});

Bounding Volumes

Each Node 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:

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;
});

Recursively Updating State

Each Node 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);
Clone this wiki locally