Skip to content

Scene Graphs

xeolabs edited this page Jul 4, 2019 · 28 revisions

See also:

Introduction

In this tutorial you'll learn how to programmatically build a model using xeokit's scene graph representation, then classify it with metadata, which helps us to navigate it.

Scene graphs are xeokit's standard mechanism for representing content, involving node trees with dynamic transforms and boundary hierarchies, with meshes at the leaves.

While scene graphs are great for building 3D gizmos and representing medium-sized models that need high-quality rendering, they don't scale up in terms of memory and GPU performance for huge CAD and BIM models. For huge models, we have the PerformanceModel representation, which you can read about in the tutorial on High Performance Model Representation.

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, which are inherited by its child Nodes. A Node tree also represents a bounding volume hierarchy, in which each Node has a dynamic World-space boundary, which contains the boundaries of its children.

The Node and Mesh components implement the abstract Entity interface, which represents a model, an object, or just an anonymous scene element. 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, xeokit provides an alternative method to build scene content via its PerformanceModel component, which also represents its model and objects as Entity types. Although these two representations implement the inner workings of their scene representations differently, the Entity type provides us with an abstract interface through which we can treat content uniformly.

Creating a Scene Graph

In the example below we'll build a simple scene graph that represents a model of a table.

Our 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/viewer/scene/mesh/Mesh.js";
import {Node} from "../src/viewer/scene/nodes/Node.js";
import {PhongMaterial} from "../src/viewer/scene/materials/PhongMaterial.js";
import {buildBoxGeometry} from "../src/viewer/scene/geometry/builders/buildBoxGeometry.js";
import {ReadableGeometry} from "../src/viewer/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

As mentioned, 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, which also dynamically updates their boundaries.

// Periodically update transforms on some of 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);
});

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 properties, 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;

Nodes and Mesh hierarchies can be dynamically modified. When we add a child to a parent Node, then the child will inherit its initial rendering properties 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 create a MetaModel for our model, along with a MetaObject for each of it's objects.

The MetaModel and MetaObjects get the same IDs as the Entities they correspond to.

const furnitureMetaModel = viewer.metaScene // 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 our scene graph and classified it with metadata, we can now find its MetaModel and MetaObjects using the IDs of their corresponding Entities.

const furnitureMetaModel = scene.metaScene.metaModels["furniture"];

const redLegMetaObject = scene.metaScene.metaObjects["redLeg"];

To illustrate a common use case for metadata, we'll log information 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 organizes its MetaObjects in a tree that describes their structural composition:

// Get metadata on the root object
const tableMetaObject = furnitureMetaModel.rootMetaObject;

// Get metadata on the leg objects
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.

This hierarchy allows us to express the hierarchical structure of a model while representing it in various ways in the 3D scene (such as with PerformanceModel, which has a non-hierarchical scene representation).

Note also that a MetaObjects does not need to have a corresponding Entity and vice-versa.