Skip to content

Scene Graphs

xeolabs edited this page Feb 2, 2019 · 28 revisions

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:

Contents

Scene Graph Components

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.

Creating a Scene Graph

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 Entities

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

Animating Entities

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

Getting Boundaries

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

Updating State

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

Classifying with Metadata

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

Querying Metadata

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

Metadata Structure

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.

Clone this wiki locally