Skip to content

High Performance Model Representation

xeolabs edited this page Jul 4, 2019 · 17 revisions

See also:

Introduction

While xeokit's standard scene graph is great for gizmos and medium-sized models, it doesn't scale up to millions of objects in terms of memory and rendering efficiency.

For huge models, we have the PerformanceModel representation, which is optimized to pack large amounts of geometry into memory and render it efficiently using WebGL.

PerformanceModel is the default model representation loaded by GLTFLoaderPlugin and BIMServerLoaderPlugin.

In this tutorial you'll learn how to use PerformanceModel to create high-detail content programmatically. Ordinarily you'd be learning about PerformanceModel if you were writing your own model loader plugins.

Contents

PerformanceModel

PerformanceModel uses two rendering techniques internally:

  1. Geometry batching for unique geometries, combining those into a single WebGL geometry buffer, to render in one draw call, and
  2. geometry instancing for geometries that are shared by multiple meshes, rendering all instances of each shared geometry in one draw call.

These techniques come with certain limitations:

  • Non-realistic rendering - while scene graphs can use xeokit's full set of material workflows, PerformanceModel uses simple Lambertian shading without textures.
  • Static transforms - transforms within a PerformanceModel are static and cannot be dynamically translated, rotated and scaled the way Nodes and Meshes in scene graphs can.
  • Immutable model representation - while scene graph Nodes and Meshes can be dynamically plugged together, PerformanceModel is immutable, since it packs its geometries into buffers and instanced arrays.

PerformanceModel's API allows us to exploit batching and instancing, while exposing its elements as abstract Entity types.

Entity is the abstract base class for the various xeokit components that represent models, objects, or 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.

  • A PerformanceModel is an Entity that represents a model.
  • A PerformanceModel represents each of its objects with an Entity.
  • Each Entity has one or more meshes that define its shape.
  • Each mesh has either its own unique geometry, or shares a geometry with other meshes.

GPU-Resident Geometry

For a low memory footprint, PerformanceModel stores its geometries in GPU memory only, compressed (quantized) as integers. Unfortunately, GPU-resident geometry is not readable by JavaScript.

Picking

PerformanceModel supports picking of individual Entities, but does not yet support picking of 3D positions on their surfaces, the way Meshes in scene graphs can.

Example 1: Geometry Instancing

In the example below, we'll use a PerformanceModel to build a simple table model using geometry instancing.

We'll start by adding a reusable box-shaped geometry to our PerformanceModel.

Then, for each object in our model we'll add an Entity that has a mesh that instances our box geometry, transforming and coloring the instance.

Click the image below for a live demo.

import {Viewer} from "../src/viewer/Viewer.js";
import {PerformanceModel} from
    "../src/viewer/scene/PerformanceModels/PerformanceModel.js";

const viewer = new Viewer({
    canvasId: "myCanvas",
    transparent: true
});

viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
viewer.scene.camera.look = [0, -5.75, 0];
viewer.scene.camera.up = [0.37, 0.91, -0.11];

// Build a PerformanceModel representing a table
// with four legs, using geometry instancing

const performanceModel = new PerformanceModel(viewer.scene, {
    id: "table",
    isModel: true, // <--- Registers PerformanceModel in viewer.scene.models
    position: [0, 0, 0],
    scale: [1, 1, 1],
    rotation: [0, 0, 0]
});

// Create a reusable geometry within the PerformanceModel
// We'll instance this geometry by five meshes

performanceModel.createGeometry({

    id: "myBoxGeometry",

    // The primitive type - allowed values are "points", "lines",
    // "line-loop", "line-strip", "triangles", "triangle-strip"
    // and "triangle-fan".  See the OpenGL/WebGL specification docs
    // for how the coordinate arrays are supposed to be laid out.
    primitive: "triangles",

    // The vertices - eight for our cube, each
    // one spanning three array elements for X,Y and Z
    positions: [
        1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, // v0-v1-v2-v3 front
        1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, // v0-v3-v4-v1 right
        1, 1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, // v0-v1-v6-v1 top
        -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, // v1-v6-v7-v2 left
        -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // v7-v4-v3-v2 bottom
        1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1 // v4-v7-v6-v1 back
    ],

    // Normal vectors, one for each vertex
    normals: [
        0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // v0-v1-v2-v3 front
        1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v3-v4-v5 right
        0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // v0-v5-v6-v1 top
        -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // v1-v6-v7-v2 left
        0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, // v7-v4-v3-v2 bottom
        0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1 // v4-v7-v6-v5 back
    ],

    // Indices - these organise the positions and and normals
    // into geometric primitives in accordance with the "primitive" parameter,
    // in this case a set of three indices for each triangle.
    //
    // Note that each triangle is specified in counter-clockwise winding order.
    //
    indices: [
        0, 1, 2, 0, 2, 3, // front
        4, 5, 6, 4, 6, 7, // right
        8, 9, 10, 8, 10, 11, // top
        12, 13, 14, 12, 14, 15, // left
        16, 17, 18, 16, 18, 19, // bottom
        20, 21, 22, 20, 22, 23
    ]
});

// Red table leg

performanceModel.createMesh({
    id: "redLegMesh",
    geometryId: "myBoxGeometry",
    position: [-4, -6, -4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [1, 0.3, 0.3]
});

performanceModel.createEntity({ 
    id: "redLeg",
    meshIds: ["redLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Green table leg

performanceModel.createMesh({
    id: "greenLegMesh",
    geometryId: "myBoxGeometry",
    position: [4, -6, -4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [0.3, 1.0, 0.3]
});

performanceModel.createEntity({ 
    id: "greenLeg",
    meshIds: ["greenLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Blue table leg

performanceModel.createMesh({
    id: "blueLegMesh",
    geometryId: "myBoxGeometry",
    position: [4, -6, 4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [0.3, 0.3, 1.0]
});

performanceModel.createEntity({ 
    id: "blueLeg",
    meshIds: ["blueLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Yellow table leg

performanceModel.createMesh({
    id: "yellowLegMesh",
    geometryId: "myBoxGeometry",
    position: [-4, -6, 4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [1.0, 1.0, 0.0]
});

performanceModel.createEntity({ 
    id: "yellowLeg",
    meshIds: ["yellowLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Purple table top

performanceModel.createMesh({
    id: "purpleTableTopMesh",
    geometryId: "myBoxGeometry",
    position: [0, -3, 0],
    scale: [6, 0.5, 6],
    rotation: [0, 0, 0],
    color: [1.0, 0.3, 1.0]
});

performanceModel.createEntity({ 
    id: "purpleTableTop",
    meshIds: ["purpleTableTopMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

Finalizing a PerformanceModel

Before we can view and interact with our PerformanceModel, we need to finalize it. Internally, this causes the PerformanceModel to build the vertex buffer objects (VBOs) that support our geometry instances. When using geometry batching (see next example), this causes PerformanceModel to build the VBOs that combine the batched geometries. Note that you can do both instancing and batching within the same PerformanceModel.

Once finalized, we can't add anything more to our PerformanceModel.

performanceModel.finalize();

Finding Entities

As mentioned earlier, Entity is the abstract base class for components that represent models, objects, or just anonymous visible elements.

Since we created configured our PerformanceModel with isModel: true, we're able to find it as an Entity by ID in viewer.scene.models. Likewise, since we configured each of its Entities with isObject: true, we're able to find them in viewer.scene.objects.

// Get the whole table model Entity
const table = viewer.scene.models["table"];

// Get some leg object Entities
const redLeg = viewer.scene.objects["redLeg"];
const greenLeg = viewer.scene.objects["greenLeg"];
const blueLeg = viewer.scene.objects["blueLeg"];

Example 2: Geometry Batching

Let's once more use a PerformanceModel to build the simple table model, this time exploiting geometry batching.

Click the image below for a live demo.

import {Viewer} from "../src/viewer/Viewer.js";
import {PerformanceModel} from "../src/viewer/scene/PerformanceModel/PerformanceModel.js";

const viewer = new Viewer({
    canvasId: "myCanvas",
    transparent: true
});

viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
viewer.scene.camera.look = [0, -5.75, 0];
viewer.scene.camera.up = [0.37, 0.91, -0.11];

// Create a PerformanceModel representing a table with four legs, using geometry batching
const performanceModel = new PerformanceModel(viewer.scene, {
    id: "table",
    isModel: true,  // <--- Registers PerformanceModel in viewer.scene.models
    position: [0, 0, 0],
    scale: [1, 1, 1],
    rotation: [0, 0, 0]
});

// Red table leg

performanceModel.createMesh({
    id: "redLegMesh",

    // Geometry arrays are same as for the earlier batching example
    primitive: "triangles",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [-4, -6, -4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [1, 0.3, 0.3]
});

performanceModel.createEntity({
    id: "redLeg",
    meshIds: ["redLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Green table leg

performanceModel.createMesh({
    id: "greenLegMesh",
    primitive: "triangles",
    primitive: "triangles",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [4, -6, -4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [0.3, 1.0, 0.3]
});

performanceModel.createEntity({
    id: "greenLeg",
    meshIds: ["greenLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Blue table leg

performanceModel.createMesh({
    id: "blueLegMesh",
    primitive: "triangles",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [4, -6, 4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [0.3, 0.3, 1.0]
});

performanceModel.createEntity({
    id: "blueLeg",
    meshIds: ["blueLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Yellow table leg object

performanceModel.createMesh({
    id: "yellowLegMesh",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [-4, -6, 4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [1.0, 1.0, 0.0]
});

performanceModel.createEntity({
    id: "yellowLeg",
    meshIds: ["yellowLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Purple table top

performanceModel.createMesh({
    id: "purpleTableTopMesh",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [0, -3, 0],
    scale: [6, 0.5, 6],
    rotation: [0, 0, 0],
    color: [1.0, 0.3, 1.0]
});

performanceModel.createEntity({
    id: "purpleTableTop",
    meshIds: ["purpleTableTopMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Finalize the PerformanceModel.

performanceModel.finalize();

// Find BigModelNodes by their model and object IDs

// Get the whole table model
const table = viewer.scene.models["table"];

// Get some leg objects
const redLeg = viewer.scene.objects["redLeg"];
const greenLeg = viewer.scene.objects["greenLeg"];
const blueLeg = viewer.scene.objects["blueLeg"];

Classifying with Metadata

In the previous examples, we used PerformanceModel to build two versions of the same table model, to demonstrate geometry batching and geometry instancing.

We'll now classify our Entities with metadata. This metadata will work the same for both our examples, since they create the exact same structure of Entities to represent their models and objects. The abstract Entity type is, after all, intended to provide an abstract interface through which differently-implemented scene content can be accessed uniformly.

To create the metadata, we'll create a MetaModel for our model, with a MetaObject for each of it's objects. The MetaModel and MetaObjects get 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",            // Same ID as an object Entity
                "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 model (either the instancing or batching example), we can now find the MetaModel and MetaObjects using the IDs of their corresponding Entities.

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 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 MetaObject does not need to have a corresponding Entity and vice-versa.

Streaming Support

PerformanceModel supports incremental loading using the concept of tiles. These allow us to build a PerformanceModel one tile at a time, finalizing and rendering each tile as soon as we've built it. This is often preferable to only rendering the model once the whole thing has loaded. Using tiles, we might load the most visually-interesting features (like walls, floor and roof) into the first tile, then finalize and begin rendering that tile and allow the user to explore its entities, while we concurrently load less visually-important features (eg. power outlets, furniture) in the next tile, and so on.

In the example below, we'll create another PerformanceModel representing our table, this time partitioning the entities into two tiles, with two legs in the first tile, and the other two legs and table top in the second tile.

We'll finalize each tile as soon as we've built it, which will cause it to be immediately added to the scene and begin rendering. While not shown in this minimal example, tiles are useful when we're loading large models and need to be able to interact with the 3D scene while they are still loading.

const performanceModel2 = new PerformanceModel(viewer.scene, {
    id: "table2",
    isModel: true,  // <--- Registers PerformanceModel in viewer.scene.models
    position: [0, 0, 0],
    scale: [1, 1, 1],
    rotation: [0, 0, 0]
});

// Create a tile in our PerformanceModel

performanceModel2.createTile({
    id: "myTile"
});

// Add a red table leg entity to our tile

performanceModel2.createMesh({
    id: "redLegMesh",
    tileId: "myTile", // <<----- Create mesh in tile
    // Geometry arrays are same as for the earlier batching example
    primitive: "triangles",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [-4, -6, -4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [1, 0.3, 0.3]
});

performanceModel2.createEntity({
    id: "redLeg",
    tileId: "myTile", // <<----- Create entity in tile
    meshIds: ["redLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Add a green table leg entity to our tile

performanceModel2.createMesh({
    id: "greenLegMesh",
    tileId: "myTile", // <<----- Create mesh in tile
    primitive: "triangles",
    primitive: "triangles",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [4, -6, -4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [0.3, 1.0, 0.3]
});

performanceModel2.createEntity({
    id: "greenLeg",
    tileId: "myTile2", // <<----- Create entity in tile
    meshIds: ["greenLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Finalize our tile, which starts rendering it

performanceModel2.finalizeTile("myTile");

// Create a second tile

performanceModel2.createTile({
    id: "myTile2"
});

// Add a blue table leg entity to our second tile

performanceModel2.createMesh({
    id: "blueLegMesh",
    tileId: "myTile2", // <<----- Create mesh in tile
    primitive: "triangles",
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [4, -6, 4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [0.3, 0.3, 1.0]
});

performanceModel2.createEntity({
    id: "blueLeg",
    tileId: "myTile2", // <<----- Create entity in tile
    meshIds: ["blueLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Add a Yellow table leg entity to our second tile

performanceModel2.createMesh({
    id: "yellowLegMesh",
    tileId: "myTile2", // <<----- Create mesh in tile
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [-4, -6, 4],
    scale: [1, 3, 1],
    rotation: [0, 0, 0],
    color: [1.0, 1.0, 0.0]
});

performanceModel2.createEntity({
    id: "yellowLeg",
    tileId: "myTile2", // <<----- Create entity in tile
    meshIds: ["yellowLegMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Add a purple table top entity to our second tile

performanceModel2.createMesh({
    id: "purpleTableTopMesh",
    tileId: "myTile2", // <<----- Create mesh in tile
    positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
    normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
    indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
    position: [0, -3, 0],
    scale: [6, 0.5, 6],
    rotation: [0, 0, 0],
    color: [1.0, 0.3, 1.0]
});

performanceModel2.createEntity({
    id: "purpleTableTop",
    tileId: "myTile2", // <<----- Create entity in tile
    meshIds: ["purpleTableTopMesh"],
    isObject: true // <---- Registers Entity by ID on viewer.scene.objects
});

// Finalize the PerformanceModel. This finalizes tiles that have not yet
// been finalized. In this case, that's "myTile2", which now begins
// rendering alongside "tile1".

performanceModel2.finalize();
Clone this wiki locally