-
Notifications
You must be signed in to change notification settings - Fork 293
High Performance Model Representation
See also:
- Scene Graphs - standard scene graph representation
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 representation loaded by GLTFLoaderPlugin and BIMServerLoaderPlugin.
In this tutorial you'll learn how to use PerformanceModel to create high-detail content programmatically.
- PerformanceModel
- GPU-Resident Geometry
- Picking
- Example 1: Geometry Instancing
- Finalizing a PerformanceModel
- Finding Entities
- Example 2: Geometry Batching
- Classifying with Metadata
- Querying Metadata
- Metadata Structure
PerformanceModel uses two rendering techniques internally:
- Geometry batching for unique geometries, combining those into a single WebGL geometry buffer, to render in one draw call, and
- 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.
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.
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.
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/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
});
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();
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"];
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/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"];
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"
}
}
]
});
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"));
}
}
});
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.