UNPKG

@xeokit/xeokit-sdk

Version:

3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision

1,320 lines (1,267 loc) 142 kB
import {Component} from "../Component.js"; import {math} from "../math/math.js"; import {buildEdgeIndices} from '../math/buildEdgeIndices.js'; import {SceneModelMesh} from './SceneModelMesh.js'; import {DTXLayer} from './layer/DTXLayer.js'; import {VBOLayer} from './layer/VBOLayer.js'; import {ENTITY_FLAGS} from './ENTITY_FLAGS.js'; import {RenderFlags} from "../webgl/RenderFlags.js"; import {worldToRTCPositions} from "../math/rtcCoords.js"; import {SceneModelTextureSet} from "./SceneModelTextureSet.js"; import {SceneModelTexture} from "./SceneModelTexture.js"; import {Texture2D} from "../webgl/Texture2D.js"; import {utils} from "../utils.js"; import {getKTX2TextureTranscoder} from "../utils/textureTranscoders/KTX2TextureTranscoder/KTX2TextureTranscoder.js"; import { ClampToEdgeWrapping, LinearEncoding, LinearFilter, LinearMipmapLinearFilter, LinearMipMapNearestFilter, MirroredRepeatWrapping, NearestFilter, NearestMipMapLinearFilter, NearestMipMapNearestFilter, RepeatWrapping, sRGBEncoding } from "../constants/constants.js"; import {createPositionsDecodeMatrix, quantizePositions} from "./compression.js"; import {uniquifyPositions} from "./calculateUniquePositions.js"; import {rebucketPositions} from "./rebucketPositions.js"; import {SceneModelEntity} from "./SceneModelEntity.js"; import {geometryCompressionUtils} from "../math/geometryCompressionUtils.js"; import {SceneModelTransform} from "./SceneModelTransform.js"; const tempVec3a = math.vec3(); const tempOBB3 = math.OBB3(); const tempQuaternion = math.vec4(); const DEFAULT_SCALE = math.vec3([1, 1, 1]); const DEFAULT_POSITION = math.vec3([0, 0, 0]); const DEFAULT_ROTATION = math.vec3([0, 0, 0]); const DEFAULT_QUATERNION = math.identityQuaternion(); const DEFAULT_MATRIX = math.identityMat4(); const DEFAULT_COLOR_TEXTURE_ID = "defaultColorTexture"; const DEFAULT_METAL_ROUGH_TEXTURE_ID = "defaultMetalRoughTexture"; const DEFAULT_NORMALS_TEXTURE_ID = "defaultNormalsTexture"; const DEFAULT_EMISSIVE_TEXTURE_ID = "defaultEmissiveTexture"; const DEFAULT_OCCLUSION_TEXTURE_ID = "defaultOcclusionTexture"; const DEFAULT_TEXTURE_SET_ID = "defaultTextureSet"; const defaultCompressedColor = new Uint8Array([255, 255, 255]); const VBO_INSTANCED = 0; const VBO_BATCHED = 1; const DTX = 2; /** * @desc A high-performance model representation for efficient rendering and low memory usage. * * # Examples * * Internally, SceneModel uses a combination of several different techniques to render and represent * the different parts of a typical model. Each of the live examples at these links is designed to "unit test" one of these * techniques, in isolation. If some bug occurs in SceneModel, we use these tests to debug, but they also * serve to demonstrate how to use the capabilities of SceneModel programmatically. * * * [Loading building models into SceneModels](/examples/buildings) * * [Loading city models into SceneModels](/examples/cities) * * [Loading LiDAR scans into SceneModels](/examples/lidar) * * [Loading CAD models into SceneModels](/examples/cad) * * [SceneModel feature tests](/examples/scenemodel) * * # Overview * * While xeokit's standard [scene graph](https://github.com/xeokit/xeokit-sdk/wiki/Scene-Graphs) 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 ````SceneModel```` representation, which is optimized to pack large amounts of geometry into memory and render it efficiently using WebGL. * * ````SceneModel```` is the default model representation loaded by (at least) {@link GLTFLoaderPlugin}, {@link XKTLoaderPlugin} and {@link WebIFCLoaderPlugin}. * * In this tutorial you'll learn how to use ````SceneModel```` to create high-detail content programmatically. Ordinarily you'd be learning about ````SceneModel```` if you were writing your own model loader plugins. * * # Contents * * - [SceneModel](#DataTextureSceneModel) * - [GPU-Resident Geometry](#gpu-resident-geometry) * - [Picking](#picking) * - [Example 1: Geometry Instancing](#example-1--geometry-instancing) * - [Finalizing a SceneModel](#finalizing-a-DataTextureSceneModel) * - [Finding Entities](#finding-entities) * - [Example 2: Geometry Batching](#example-2--geometry-batching) * - [Classifying with Metadata](#classifying-with-metadata) * - [Querying Metadata](#querying-metadata) * - [Metadata Structure](#metadata-structure) * - [RTC Coordinates](#rtc-coordinates-for-double-precision) * - [Example 3: RTC Coordinates with Geometry Instancing](#example-2--rtc-coordinates-with-geometry-instancing) * - [Example 4: RTC Coordinates with Geometry Batching](#example-2--rtc-coordinates-with-geometry-batching) * * ## SceneModel * * ````SceneModel```` 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. * * <br> * These techniques come with certain limitations: * * * Non-realistic rendering - while scene graphs can use xeokit's full set of material workflows, ````SceneModel```` uses simple Lambertian shading without textures. * * Static transforms - transforms within a ````SceneModel```` are static and cannot be dynamically translated, rotated and scaled the way {@link Node}s and {@link Mesh}es in scene graphs can. * * Immutable model representation - while scene graph {@link Node}s and * {@link Mesh}es can be dynamically plugged together, ````SceneModel```` is immutable, * since it packs its geometries into buffers and instanced arrays. * * ````SceneModel````'s API allows us to exploit batching and instancing, while exposing its elements as * abstract {@link Entity} types. * * {@link 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 ````SceneModel```` is an {@link Entity} that represents a model. * * A ````SceneModel```` represents each of its objects with an {@link Entity}. * * Each {@link 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, ````SceneModel```` stores its geometries in GPU memory only, compressed (quantized) as integers. Unfortunately, GPU-resident geometry is * not readable by JavaScript. * * * ## Example 1: Geometry Instancing * * In the example below, we'll use a ````SceneModel```` * to build a simple table model using geometry instancing. * * We'll start by adding a reusable box-shaped geometry to our ````SceneModel````. * * Then, for each object in our model we'll add an {@link Entity} * that has a mesh that instances our box geometry, transforming and coloring the instance. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_instancing) * * ````javascript * import {Viewer, SceneModel} from "xeokit-sdk.es.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 SceneModel representing a table * // with four legs, using geometry instancing * * const sceneModel = new SceneModel(viewer.scene, { * id: "table", * isModel: true, // <--- Registers SceneModel in viewer.scene.models * position: [0, 0, 0], * scale: [1, 1, 1], * rotation: [0, 0, 0] * }); * * // Create a reusable geometry within the SceneModel * // We'll instance this geometry by five meshes * * sceneModel.createGeometry({ * * id: "myBoxGeometry", * * // The primitive type - allowed values are "points", "lines" and "triangles". * // 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 * * sceneModel.createMesh({ * id: "redLegMesh", * geometryId: "myBoxGeometry", * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3] * }); * * sceneModel.createEntity({ * id: "redLeg", * meshIds: ["redLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Green table leg * * sceneModel.createMesh({ * id: "greenLegMesh", * geometryId: "myBoxGeometry", * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3] * }); * * sceneModel.createEntity({ * id: "greenLeg", * meshIds: ["greenLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Blue table leg * * sceneModel.createMesh({ * id: "blueLegMesh", * geometryId: "myBoxGeometry", * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0] * }); * * sceneModel.createEntity({ * id: "blueLeg", * meshIds: ["blueLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Yellow table leg * * sceneModel.createMesh({ * id: "yellowLegMesh", * geometryId: "myBoxGeometry", * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0] * }); * * sceneModel.createEntity({ * id: "yellowLeg", * meshIds: ["yellowLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Purple table top * * sceneModel.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] * }); * * sceneModel.createEntity({ * id: "purpleTableTop", * meshIds: ["purpleTableTopMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * ```` * * ## Finalizing a SceneModel * * Before we can view and interact with our ````SceneModel````, we need to **finalize** it. Internally, this causes the ````SceneModel```` to build the * vertex buffer objects (VBOs) that support our geometry instances. When using geometry batching (see next example), * this causes ````SceneModel```` to build the VBOs that combine the batched geometries. Note that you can do both instancing and * batching within the same ````SceneModel````. * * Once finalized, we can't add anything more to our ````SceneModel````. * * ```` javascript * SceneModel.finalize(); * ```` * * ## Finding Entities * * As mentioned earlier, {@link Entity} is * the abstract base class for components that represent models, objects, or just * anonymous visible elements. * * Since we created configured our ````SceneModel```` 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````. * * * ````javascript * // 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 ````SceneModel```` * to build the simple table model, this time exploiting geometry batching. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching) * * ````javascript * import {Viewer, SceneModel} from "xeokit-sdk.es.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 SceneModel representing a table with four legs, using geometry batching * const sceneModel = new SceneModel(viewer.scene, { * id: "table", * isModel: true, // <--- Registers SceneModel in viewer.scene.models * position: [0, 0, 0], * scale: [1, 1, 1], * rotation: [0, 0, 0] * }); * * // Red table leg * * sceneModel.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] * }); * * sceneModel.createEntity({ * id: "redLeg", * meshIds: ["redLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Green table leg * * sceneModel.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] * }); * * sceneModel.createEntity({ * id: "greenLeg", * meshIds: ["greenLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Blue table leg * * sceneModel.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] * }); * * sceneModel.createEntity({ * id: "blueLeg", * meshIds: ["blueLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Yellow table leg object * * sceneModel.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] * }); * * sceneModel.createEntity({ * id: "yellowLeg", * meshIds: ["yellowLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Purple table top * * sceneModel.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] * }); * * sceneModel.createEntity({ * id: "purpleTableTop", * meshIds: ["purpleTableTopMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Finalize the SceneModel. * * SceneModel.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 ````SceneModel```` to build * two versions of the same table model, to demonstrate geometry batching and geometry instancing. * * We'll now classify our {@link Entity}s with metadata. This metadata * will work the same for both our examples, since they create the exact same structure of {@link Entity}s * 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 {@link MetaModel} for our model, * with a {@link MetaObject} for each of it's objects. The MetaModel and MetaObjects * get the same IDs as the {@link Entity}s that represent their model and objects within our scene. * * ```` javascript * const furnitureMetaModel = viewer.metaScene.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 {@link MetaModel} * and {@link MetaObject}s using the IDs of their * corresponding {@link Entity}s. * * ````JavaScript * const furnitureMetaModel = scene.metaScene.metaModels["furniture"]; * * const redLegMetaObject = scene.metaScene.metaObjects["redLeg"]; * ```` * * In the snippet below, we'll log metadata on each {@link Entity} we click on: * * ````JavaScript * 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 {@link MetaModel} * organizes its {@link MetaObject}s in * a tree that describes their structural composition: * * ````JavaScript * // 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 {@link 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](https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_glTF_OTCConferenceCenter). * * 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 ````SceneModel````, which * has a non-hierarchical scene representation). * * Note also that a {@link MetaObject} does not need to have a corresponding * {@link Entity} and vice-versa. * * # RTC Coordinates for Double Precision * * ````SceneModel```` can emulate 64-bit precision on GPUs using relative-to-center (RTC) coordinates. * * Consider a model that contains many small objects, but with such large spatial extents that 32 bits of GPU precision (accurate to ~7 digits) will not be sufficient to render all of the the objects without jittering. * * To prevent jittering, we could spatially subdivide the objects into "tiles". Each tile would have a center position, and the positions of the objects within the tile would be relative to that center ("RTC coordinates"). * * While the center positions of the tiles would be 64-bit values, the object positions only need to be 32-bit. * * Internally, when rendering an object with RTC coordinates, xeokit first temporarily translates the camera viewing matrix by the object's tile's RTC center, on the CPU, using 64-bit math. * * Then xeokit loads the viewing matrix into its WebGL shaders, where math happens at 32-bit precision. Within the shaders, the matrix is effectively down-cast to 32-bit precision, and the object's 32-bit vertex positions are transformed by the matrix. * * We see no jittering, because with RTC a detectable loss of GPU accuracy only starts happening to objects as they become very distant from the camera viewpoint, at which point they are too small to be discernible anyway. * * ## RTC Coordinates with Geometry Instancing * * To use RTC with ````SceneModel```` geometry instancing, we specify an RTC center for the geometry via its ````origin```` parameter. Then ````SceneModel```` assumes that all meshes that instance that geometry are within the same RTC coordinate system, ie. the meshes ````position```` and ````rotation```` properties are assumed to be relative to the geometry's ````origin````. * * For simplicity, our example's meshes all instance the same geometry. Therefore, our example model has only one RTC center. * * Note that the axis-aligned World-space boundary (AABB) of our model is ````[ -6, -9, -6, 1000000006, -2.5, 1000000006]````. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching) * * ````javascript * const origin = [100000000, 0, 100000000]; * * sceneModel.createGeometry({ * id: "box", * 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, ... ], * }); * * sceneModel.createMesh({ * id: "leg1", * geometryId: "box", * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg1"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg2", * geometryId: "box", * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg2"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg3", * geometryId: "box", * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg3"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg4", * geometryId: "box", * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg4"], * isObject: true * }); * * sceneModel.createMesh({ * id: "top", * geometryId: "box", * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["top"], * isObject: true * }); * ```` * * ## RTC Coordinates with Geometry Batching * * To use RTC with ````SceneModel```` geometry batching, we specify an RTC center (````origin````) for each mesh. For performance, we try to have as many meshes share the same value for ````origin```` as possible. Each mesh's ````positions````, ````position```` and ````rotation```` properties are assumed to be relative to ````origin````. * * For simplicity, the meshes in our example all share the same RTC center. * * The axis-aligned World-space boundary (AABB) of our model is ````[ -6, -9, -6, 1000000006, -2.5, 1000000006]````. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching) * * ````javascript * const origin = [100000000, 0, 100000000]; * * sceneModel.createMesh({ * id: "leg1", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * 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] * }); * * sceneModel.createEntity({ * meshIds: ["leg1"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg2", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * 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] * }); * * sceneModel.createEntity({ * meshIds: ["leg2"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg3", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * 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] * }); * * sceneModel.createEntity({ * meshIds: ["leg3"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg4", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * 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, 1.0, 0.0] * }); * * sceneModel.createEntity({ * meshIds: ["leg4"], * isObject: true * }); * * sceneModel.createMesh({ * id: "top", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * 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: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0] * }); * * sceneModel.createEntity({ * meshIds: ["top"], * isObject: true * }); * ```` * * ## Positioning at World-space coordinates * * To position a SceneModel at given double-precision World coordinates, we can * configure the ````origin```` of the SceneModel itself. The ````origin```` is a double-precision * 3D World-space position at which the SceneModel will be located. * * Note that ````position```` is a single-precision offset relative to ````origin````. * * ````javascript * const origin = [100000000, 0, 100000000]; * * const sceneModel = new SceneModel(viewer.scene, { * id: "table", * isModel: true, * origin: origin, // Everything in this SceneModel is relative to this RTC center * position: [0, 0, 0], * scale: [1, 1, 1], * rotation: [0, 0, 0] * }); * * sceneModel.createGeometry({ * id: "box", * 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, ... ], * }); * * sceneModel.createMesh({ * id: "leg1", * geometryId: "box", * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3] * }); * * sceneModel.createEntity({ * meshIds: ["leg1"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg2", * geometryId: "box", * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3] * }); * * sceneModel.createEntity({ * meshIds: ["leg2"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg3", * geometryId: "box", * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0] * }); * * sceneModel.createEntity({ * meshIds: ["leg3"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg4", * geometryId: "box", * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0] * }); * * sceneModel.createEntity({ * meshIds: ["leg4"], * isObject: true * }); * * sceneModel.createMesh({ * id: "top", * geometryId: "box", * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0] * }); * * sceneModel.createEntity({ * meshIds: ["top"], * isObject: true * }); * ```` * * # Textures * * ## Loading KTX2 Texture Files into a SceneModel * * A {@link SceneModel} that is configured with a {@link KTX2TextureTranscoder} will * allow us to load textures into it from KTX2 buffers or files. * * In the example below, we'll create a {@link Viewer}, containing a {@link SceneModel} configured with a * {@link KTX2TextureTranscoder}. We'll then programmatically create a simple object within the SceneModel, consisting of * a single mesh with a texture loaded from a KTX2 file, which our SceneModel internally transcodes, using * its {@link KTX2TextureTranscoder}. Note how we configure our {@link KTX2TextureTranscoder} with a path to the Basis Universal * transcoder WASM module. * * ````javascript * 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]; * * const textureTranscoder = new KTX2TextureTranscoder({ * viewer, * transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module * }); * * const sceneModel = new SceneModel(viewer.scene, { * id: "myModel", * textureTranscoder // <<-------------------- Configure model with our transcoder * }); * * sceneModel.createTexture({ * id: "myColorTexture", * src: "../assets/textures/compressed/sample_uastc_zstd.ktx2" // <<----- KTX2 texture asset * }); * * sceneModel.createTexture({ * id: "myMetallicRoughnessTexture", * src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset * }); * * sceneModel.createTextureSet({ * id: "myTextureSet", * colorTextureId: "myColorTexture", * metallicRoughnessTextureId: "myMetallicRoughnessTexture" * }); * * sceneModel.createMesh({ * id: "myMesh", * textureSetId: "myTextureSet", * primitive: "triangles", * positions: [1, 1, 1, ...], * normals: [0, 0, 1, 0, ...], * uv: [1, 0, 0, ...], * indices: [0, 1, 2, ...], * }); * * sceneModel.createEntity({ * id: "myEntity", * meshIds: ["myMesh"] * }); * * sceneModel.finalize(); * ```` * * ## Loading KTX2 Textures from ArrayBuffers into a SceneModel * * A SceneModel that is configured with a {@link KTX2TextureTranscoder} will allow us to load textures into * it from KTX2 ArrayBuffers. * * In the example below, we'll create a {@link Viewer}, containing a {@link SceneModel} configured with a * {@link KTX2TextureTranscoder}. We'll then programmatically create a simple object within the SceneModel, consisting of * a single mesh with a texture loaded from a KTX2 ArrayBuffer, which our SceneModel internally transcodes, using * its {@link KTX2TextureTranscoder}. * * ````javascript * 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]; * * const textureTranscoder = new KTX2TextureTranscoder({ * viewer, * transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module * }); * * const sceneModel = new SceneModel(viewer.scene, { * id: "myModel", * textureTranscoder // <<-------------------- Configure model with our transcoder * }); * * utils.loadArraybuffer("../assets/textures/compressed/sample_uastc_zstd.ktx2",(arrayBuffer) => { * * sceneModel.createTexture({ * id: "myColorTexture", * buffers: [arrayBuffer] // <<----- KTX2 texture asset * }); * * sceneModel.createTexture({ * id: "myMetallicRoughnessTexture", * src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset * }); * * sceneModel.createTextureSet({ * id: "myTextureSet", * colorTextureId: "myColorTexture", * metallicRoughnessTextureId: "myMetallicRoughnessTexture" * }); * * sceneModel.createMesh({ * id: "myMesh", * textureSetId: "myTextureSet", * primitive: "triangles", * positions: [1, 1, 1, ...], * normals: [0, 0, 1, 0, ...], * uv: [1, 0, 0, ...], * indices: [0, 1, 2, ...], * }); * * sceneModel.createEntity({ * id: "myEntity", * meshIds: ["myMesh"] * }); * * sceneModel.finalize(); * }); * ```` * * @implements {Entity} */ export class SceneModel extends Component { /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted. * @param {Boolean} [cfg.isModel] Specify ````true```` if this SceneModel represents a model, in which case the SceneModel will be registered by {@link SceneModel#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}. * @param {Number[]} [cfg.origin=[0,0,0]] World-space double-precision 3D origin. * @param {Number[]} [cfg.position=[0,0,0]] Local, single-precision 3D position, relative to the origin parameter. * @param {Number[]} [cfg.scale=[1,1,1]] Local scale. * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] Local modelling transform matrix. Overrides the position, scale and rotation parameters. * @param {Boolean} [cfg.visible=true] Indicates if the SceneModel is initially visible. * @param {Boolean} [cfg.culled=false] Indicates if the SceneModel is initially culled from view. * @param {Boolean} [cfg.pickable=true] Indicates if the SceneModel is initially pickable. * @param {Boolean} [cfg.clippable=true] Indicates if the SceneModel is initially clippable. * @param {Boolean} [cfg.collidable=true] Indicates if the SceneModel is initially included in boundary calculations. * @param {Boolean} [cfg.xrayed=false] Indicates if the SceneModel is initially xrayed. * @param {Boolean} [cfg.highlighted=false] Indicates if the SceneModel is initially highlighted. * @param {Boolean} [cfg.selected=false] Indicates if the SceneModel is initially selected. * @param {Boolean} [cfg.edges=false] Indicates if the SceneModel's edges are initially emphasized. * @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] SceneModel's initial RGB colorize color, multiplies by the rendered fragment colors. * @param {Number} [cfg.opacity=1.0] SceneModel's initial opacity factor, multiplies by the rendered fragment alpha. * @param {Number} [cfg.backfaces=false] When we set this ````true````, then we force rendering of backfaces for this SceneModel. When * we leave this ````false````, then we allow the Viewer to decide when to render backfaces. In that case, the * Viewer will hide backfaces on watertight meshes, show backfaces on open meshes, and always show backfaces on meshes when we slice them open with {@link SectionPlane}s. * @param {Boolean} [cfg.saoEnabled=true] Indicates if Scalable Ambient Obscurance (SAO) will apply to this SceneModel. SAO is configured by the Scene's {@link SAO} component. * @param {Boolean} [cfg.pbrEnabled=true] Indicates if physically-based rendering (PBR) will apply to the SceneModel when {@link Scene#pbrEnabled} is ````true````. * @param {Boolean} [cfg.colorTextureEnabled=true] Indicates if base color textures will be rendered for the SceneModel when {@link Scene#colorTextureEnabled} is ````true````. * @param {Number} [cfg.edgeThreshold=10] When xraying, highlighting, selecting or edging, this is the threshold angle between normals of adjacent triangles, below which their shared wireframe edge is not drawn. * @param {Number} [cfg.maxGeometryBatchSize=50000000] Maximum geometry batch size, as number of vertices. This is optionally supplied * to limit the size of the batched geometry arrays that SceneModel internally creates for batched geometries. * A lower value means less heap allocation/de-allocation while creating/loading batched geometries, but more draw calls and * slower rendering speed. A high value means larger heap allocation/de-allocation while creating/loading, but less draw calls * and faster rendering speed. It's recommended to keep this somewhere roughly between ````50000```` and ````50000000```. * @param {TextureTranscoder} [cfg.textureTranscoder] Transcoder that will be used internally by {@link SceneModel#createTexture} * to convert transcoded texture data. Only required when we'll be providing transcoded data * to {@link SceneModel#createTexture}. We assume that all transcoded texture data added to a ````SceneModel```` * will then in a format supported by this transcoder. * @param {Boolean} [cfg.dtxEnabled=true] When ````true```` (default) use data textures (DTX), where appropriate, to * represent the returned model. Set false to always use vertex buffer objects (VBOs). Note that DTX is only applicable * to non-textured triangle meshes, and that VBOs are always used for meshes that have textures, line segments, or point * primitives. Only works while {@link DTX#enabled} is also ````true````. * @param {Number} [cfg.renderOrder=0] Specifies the rendering order for this SceneModel. This is used to control the order in which * SceneModels are drawn when they have transparent objects, to give control over the order in which those objects are blended within the transparent * render pass. */ constructor(owner, cfg = {}) { super(owner, cfg); this.renderOrder = cfg.renderOrder || 0; this._dtxEnabled = this.scene.dtxEnabled && (cfg.dtxEnabled !== false); this._enableVertexWelding = false; // Not needed for most objects, and very expensive, so disabled this._enableIndexBucketing = false; // Until fixed: https://github.com/xeokit/xeokit-sdk/issues/1204 this._textureTranscoder = cfg.textureTranscoder || getKTX2TextureTranscoder(this.scene.viewer); this._maxGeometryBatchSize = cfg.maxGeometryBatchSize ?? 5000000; this._aabb = math.collapseAABB3(); this._aabbDirty = true; this._quantizationRanges = {}; this._vboLayers = {}; this._dtxLayers = {}; this._meshList = []; this.layerList = []; // For GL state efficiency when drawing, InstancingLayers are in first part, BatchingLayers are in second this._layersToFinalize = []; this._entityList = []; this._entitiesToFinalize = []; this._geometries = {}; this._dtxBuckets = {}; // Geometries with optimizations used for data texture representation this._textures = {}; this._textureSets = {}; this._transforms = {}; this._meshes = {}; this._unusedMeshes = {}; this._entities = {}; /** @private **/ this.renderFlags = new RenderFlags(); /** * @private */ this.numGeometries = 0; // Number of geometries created with createGeometry() // These counts are used to avoid unnecessary render passes // They are incremented or decremented exclusively by BatchingLayer and InstancingLayer /** * @private */ this.numPortions = 0; /** * @private */ this.numVisibleLayerPortions = 0; /** * @private */ this.numTransparentLayerPortions = 0; /** * @private */ this.numXRayedLayerPortions = 0; /** * @private */ this.numHighlightedLayerPortions = 0; /** * @private */ this.numSelectedLayerPortions = 0; /** * @private */ this.numEdgesLayerPortions = 0; /** * @private */ this.numPickableLayerPortions = 0; /** * @private */ this.numClippableLayerPortions = 0; /** * @private */ this.numCulledLayerPortions = 0; this.numEntities = 0; this._numTriangles = 0; this._numLines = 0; this._numPoints = 0; this._layersFinalized = false; this._edgeThreshold = cfg.edgeThreshold || 10; // Build static matrix this._origin = math.vec3(cfg.origin || [0, 0, 0]); this._position = math.vec3(cfg.position || [0, 0, 0]); this._rotation = math.vec3(cfg.rotation || [0, 0, 0]); this._quaternion = math.vec4(cfg.quaternion || [0, 0, 0, 1]); this._conjugateQuaternion = math.vec4(cfg.quaternion || [0, 0, 0, 1]); if (cfg.rotation) { math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion); } this._scale = math.vec3(cfg.scale || [1, 1, 1]); this._worldRotationMatrix = math.mat4(); this._worldRotationMatrixConjugate = math.mat4(); this._matrix = math.mat4(); this._matrixDirty = true; this._rebuildMatrices(); this._worldNormalMatrix = math.mat4(); math.inverseMat4(this._matrix, this._worldNormalMatrix); math.transposeMat4(this._worldNormalMatrix); if (cfg.matrix || cfg.position || cfg.rotation || cfg.scale || cfg.quaternion) { this._viewMatrix = math.mat4(); this._viewNormalMatrix = math.mat4(); this._viewMatrixDirty = true; this._matrixNonIdentity = true; } this._opacity = 1.0; this._colorize = [1, 1, 1]; this._saoEnabled = (cfg.saoEnabled !== false); this._pbrEnabled = (cfg.pbrEnabled !== false); this._colorTextureEnabled = (cfg.colorTextureEnabled !== false); this._isModel = cfg.isModel; if (this._isModel) { this.scene._registerModel(this); } this._onCameraViewMatrix = this.scene.camera.on("matrix", () => { this._viewMatrixDirty = true; }); this._meshesWithDirtyMatrices = []; this._numMeshesWithDirtyMatrices = 0; this._onTick = this.scene.on("tick", () => { while (this._numMeshesWithDirtyMatrices > 0) { this._meshesWithDirtyMatrices[--this._numMeshesWithDirtyMatrices]._updateMatrix(); } }); // Every SceneModelMesh gets at least the default TextureSet, // which contains empty default textures filled with color const defineTex = (texId, preloadColor) => { return this._textures[texId] = new SceneModelTexture({ id: texId, texture: new Texture2D({ gl: this.scene.canvas.gl, preloadColor: preloadColor }) }); }; this._textureSets[DEFAULT_TEXTURE_SET_ID] = new SceneModelTextureSet({ id: DEFAULT_TEXTURE_SET_ID, colorTexture: defineTex(DEFAULT_COLOR_TEXTURE_ID, [1, 1, 1, 1]), // [r, g, b, a]}), metallicRoughnessTexture: defineTex(DEFAULT_METAL_ROUGH_TEXTURE_ID, [0, 1, 1, 1]), // [_, roughness, metalness, _], normalsTexture: defineTex(DEFAULT_NORMALS_TEXTURE_ID, [0, 0, 0, 0]), // [x, y, z, _] - these must be zeros emissiveTexture: defineTex(DEFAULT_EMISSIVE_TEXTURE_ID, [0, 0, 0, 1]), // [x, y, z, _] occlusionTexture: defineTex(DEFAULT_OCCLUSION_TEXTURE_ID, [1, 1, 1, 1]) // [x, y, z, _] }); this.visible = cfg.visible; this.culled = cfg.culled; this.pickable = cfg.pickable; this.clippable = cfg.clippable; this.collidable = cfg.collidable; this.castsShadow = cfg.castsShadow; this.receivesShadow = cfg.receivesShadow; this.xrayed = cfg.xrayed; this.highlighted = cfg.highlighted; this.selected = cfg.selected; this.edges = cfg.edges; this.co