UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

459 lines (434 loc) 17.5 kB
import * as THREE from 'three'; import GeometryLayer from "./GeometryLayer.js"; import { init3dTilesLayer, pre3dTilesUpdate, process3dTilesNode } from "../Process/3dTilesProcessing.js"; import C3DTileset from "../Core/3DTiles/C3DTileset.js"; import C3DTExtensions from "../Core/3DTiles/C3DTExtensions.js"; import { PNTS_MODE, PNTS_SHAPE, PNTS_SIZE_MODE } from "../Renderer/PointsMaterial.js"; // eslint-disable-next-line no-unused-vars import Style from "../Core/Style.js"; import C3DTFeature from "../Core/3DTiles/C3DTFeature.js"; import { optimizeGeometryGroups } from "../Utils/ThreeUtils.js"; export const C3DTILES_LAYER_EVENTS = { /** * Fires when a tile content has been loaded * @event C3DTilesLayer#on-tile-content-loaded * @type {object} * @property {THREE.Object3D} tileContent - object3D of the tile */ ON_TILE_CONTENT_LOADED: 'on-tile-content-loaded', /** * Fires when a tile is requested * @event C3DTilesLayer#on-tile-requested * @type {object} * @property {object} metadata - tile */ ON_TILE_REQUESTED: 'on-tile-requested' }; const update = process3dTilesNode(); /** * Find tileId of object * @param {THREE.Object3D} object - object * * @returns {number} tileId */ function findTileID(object) { let currentObject = object; let result = currentObject.tileId; while (isNaN(result) && currentObject.parent) { currentObject = currentObject.parent; result = currentObject.tileId; } return result; } /** * Check if object3d has feature * @param {THREE.Object3D} object3d - object3d to check * * @returns {boolean} - true if object3d has feature */ function object3DHasFeature(object3d) { return object3d.geometry && object3d.geometry.attributes._BATCHID; } /** * @extends GeometryLayer */ class C3DTilesLayer extends GeometryLayer { #fillColorMaterialsBuffer; /** * @deprecated Deprecated 3D Tiles layer. Use {@link OGC3DTilesLayer} instead. * * @example * // Create a new 3d-tiles layer from a web server * const l3dt = new C3DTilesLayer('3dtiles', { * name: '3dtl', * source: new C3DTilesSource({ * url: 'https://tileset.json' * }) * }, view); * View.prototype.addLayer.call(view, l3dt); * * // Create a new 3d-tiles layer from a Cesium ion server * const l3dt = new C3DTilesLayer('3dtiles', { * name: '3dtl', * source: new C3DTilesIonSource({ * accessToken: 'myAccessToken', assetId: 12 * }) * }, view); * View.prototype.addLayer.call(view, l3dt); * * @param {string} id - The id of the layer, that should be unique. * It is not mandatory, but an error will be emitted if this layer is * added a * {@link View} that already has a layer going by that id. * @param {object} config configuration, all elements in it * will be merged as is in the layer. * @param {C3DTilesSource} config.source The source of 3d Tiles. * * name. * @param {Number} [config.sseThreshold=16] The [Screen Space Error](https://github.com/CesiumGS/3d-tiles/blob/main/specification/README.md#geometric-error) * threshold at which child nodes of the current node will be loaded and added to the scene. * @param {Number} [config.cleanupDelay=1000] The time (in ms) after which a tile content (and its children) are * removed from the scene. * @param {C3DTExtensions} [config.registeredExtensions] 3D Tiles extensions managers registered for this tileset. * @param {String} [config.pntsMode= PNTS_MODE.COLOR] {@link PointsMaterial} Point cloud coloring mode. * Only 'COLOR' or 'CLASSIFICATION' are possible. COLOR uses RGB colors of the points, * CLASSIFICATION uses a classification property of the batch table to color points. * @param {String} [config.pntsShape= PNTS_SHAPE.CIRCLE] Point cloud point shape. Only 'CIRCLE' or 'SQUARE' are possible. * @param {String} [config.pntsSizeMode= PNTS_SIZE_MODE.VALUE] {@link PointsMaterial} Point cloud size mode. Only 'VALUE' or 'ATTENUATED' are possible. VALUE use constant size, ATTENUATED compute size depending on distance from point to camera. * @param {Number} [config.pntsMinAttenuatedSize=3] Minimum scale used by 'ATTENUATED' size mode * @param {Number} [config.pntsMaxAttenuatedSize=10] Maximum scale used by 'ATTENUATED' size mode * @param {Style} [config.style=null] - style used for this layer * @param {View} view The view */ constructor(id, config, view) { console.warn('C3DTilesLayer is deprecated and will be removed in iTowns 3.0 version. Use OGC3DTilesLayer instead.'); super(id, new THREE.Group(), { source: config.source }); this.isC3DTilesLayer = true; this.sseThreshold = config.sseThreshold || 16; this.cleanupDelay = config.cleanupDelay || 1000; this.protocol = '3d-tiles'; this.name = config.name; this.registeredExtensions = config.registeredExtensions || new C3DTExtensions(); this.pntsMode = PNTS_MODE.COLOR; this.pntsShape = PNTS_SHAPE.CIRCLE; this.classification = config.classification; this.pntsSizeMode = PNTS_SIZE_MODE.VALUE; this.pntsMinAttenuatedSize = config.pntsMinAttenuatedSize || 1; this.pntsMaxAttenuatedSize = config.pntsMaxAttenuatedSize || 7; if (config.pntsMode) { const exists = Object.values(PNTS_MODE).includes(config.pntsMode); if (!exists) { console.warn("The points cloud mode doesn't exist. Use 'COLOR' or 'CLASSIFICATION' instead."); } else { this.pntsMode = config.pntsMode; } } if (config.pntsShape) { const exists = Object.values(PNTS_SHAPE).includes(config.pntsShape); if (!exists) { console.warn("The points cloud point shape doesn't exist. Use 'CIRCLE' or 'SQUARE' instead."); } else { this.pntsShape = config.pntsShape; } } if (config.pntsSizeMode) { const exists = Object.values(PNTS_SIZE_MODE).includes(config.pntsSizeMode); if (!exists) { console.warn("The points cloud size mode doesn't exist. Use 'VALUE' or 'ATTENUATED' instead."); } else { this.pntsSizeMode = config.pntsSizeMode; } } /** @type {Style | null} */ this._style = config.style || null; /** @type {Map<string, THREE.MeshStandardMaterial>} */ this.#fillColorMaterialsBuffer = new Map(); /** * Map all C3DTFeature of the layer according their tileId and their batchId * Map< tileId, Map< batchId, C3DTFeature>> * * @type {Map<number, Map<number,C3DTFeature>>} */ this.tilesC3DTileFeatures = new Map(); if (config.onTileContentLoaded) { console.warn('DEPRECATED onTileContentLoaded should not be passed at the contruction, use C3DTILES_LAYER_EVENTS.ON_TILE_CONTENT_LOADED event instead'); this.addEventListener(C3DTILES_LAYER_EVENTS.ON_TILE_CONTENT_LOADED, config.onTileContentLoaded); } if (config.overrideMaterials) { console.warn('overrideMaterials is deprecated, use style API instead'); this.overrideMaterials = config.overrideMaterials; } this._cleanableTiles = []; const resolve = this.addInitializationStep(); this.source.whenReady.then(tileset => { this.tileset = new C3DTileset(tileset, this.source.baseUrl, this.registeredExtensions); // Verify that extensions of the tileset have been registered in the layer if (this.tileset.extensionsUsed) { for (const extensionUsed of this.tileset.extensionsUsed) { // if current extension is not registered if (!this.registeredExtensions.isExtensionRegistered(extensionUsed)) { // if it is required to load the tileset if (this.tileset.extensionsRequired && this.tileset.extensionsRequired.includes(extensionUsed)) { console.error(`3D Tiles tileset required extension "${extensionUsed}" must be registered to the 3D Tiles layer of iTowns to be parsed and used.`); } else { console.warn(`3D Tiles tileset used extension "${extensionUsed}" must be registered to the 3D Tiles layer of iTowns to be parsed and used.`); } } } } // TODO: Move all init3dTilesLayer code to constructor init3dTilesLayer(view, view.mainLoop.scheduler, this, tileset.root).then(resolve); }); } preUpdate(context) { return pre3dTilesUpdate.bind(this)(context); } update(context, layer, node) { return update(context, layer, node); } getObjectToUpdateForAttachedLayers(meta) { if (meta.content) { const result = []; meta.content.traverse(obj => { if (obj.isObject3D && obj.material && obj.layer == meta.layer) { result.push(obj); } }); const p = meta.parent; if (p && p.content) { return { elements: result, parent: p.content }; } else { return { elements: result }; } } } /** * Get the closest c3DTileFeature of an intersects array. * @param {Array} intersects - @return An array containing all * targets picked under specified coordinates. Intersects can be * computed with view.pickObjectsAt(..). See fillHTMLWithPickingInfo() * in 3dTilesHelper.js for an example. * * @returns {C3DTileFeature} - the closest C3DTileFeature of the intersects array */ getC3DTileFeatureFromIntersectsArray(intersects) { // find closest intersect with an attributes _BATCHID + face != undefined let closestIntersect = null; for (let index = 0; index < intersects.length; index++) { const i = intersects[index]; if (i.object.geometry && i.object.geometry.attributes._BATCHID && i.face && // need face to get batch id i.layer == this // just to be sure that the right layer intersected ) { closestIntersect = i; break; } } if (!closestIntersect) { return null; } const tileId = findTileID(closestIntersect.object); // face is a Face3 object of THREE which is a // triangular face. face.a is its first vertex const vertex = closestIntersect.face.a; const batchID = closestIntersect.object.geometry.attributes._BATCHID.getX(vertex); return this.tilesC3DTileFeatures.get(tileId).get(batchID); } /** * Called when a tile content is loaded * @param {THREE.Object3D} tileContent - tile as THREE.Object3D */ onTileContentLoaded(tileContent) { this.initC3DTileFeatures(tileContent); // notify observer this.dispatchEvent({ type: C3DTILES_LAYER_EVENTS.ON_TILE_CONTENT_LOADED, tileContent }); // only update style of tile features this.updateStyle([tileContent.tileId]); } /** * Initialize C3DTileFeatures from tileContent * @param {THREE.Object3D} tileContent - tile as THREE.Object3D */ initC3DTileFeatures(tileContent) { this.tilesC3DTileFeatures.set(tileContent.tileId, new Map()); // initialize tileContent.traverse(child => { if (object3DHasFeature(child)) { const batchIdAttribute = child.geometry.getAttribute('_BATCHID'); let currentBatchId = batchIdAttribute.getX(0); let start = 0; let count = 0; const registerBatchIdGroup = () => { if (this.tilesC3DTileFeatures.get(tileContent.tileId).has(currentBatchId)) { // already created const c3DTileFeature = this.tilesC3DTileFeatures.get(tileContent.tileId).get(currentBatchId); // add new group c3DTileFeature.groups.push({ start, count }); } else { // first occurence const c3DTileFeature = new C3DTFeature(tileContent.tileId, currentBatchId, [{ start, count }], // initialize with current group {}, child); this.tilesC3DTileFeatures.get(tileContent.tileId).set(currentBatchId, c3DTileFeature); } }; // TODO: Could be simplified by incrementing of 1 and stopping the iteration at positionAttributeSize.count // See https://github.com/iTowns/itowns/pull/2266#discussion_r1483285122 const positionAttribute = child.geometry.getAttribute('position'); const positionAttributeSize = positionAttribute.count * positionAttribute.itemSize; for (let index = 0; index < positionAttributeSize; index += positionAttribute.itemSize) { const batchIndex = index / positionAttribute.itemSize; const batchId = batchIdAttribute.getX(batchIndex); // check if batchId is currentBatchId if (currentBatchId !== batchId) { registerBatchIdGroup(); // reset currentBatchId = batchId; start = batchIndex; count = 0; } // record this position in current C3DTileFeature count++; // check if end of the array if (index + positionAttribute.itemSize >= positionAttributeSize) { registerBatchIdGroup(); } } } }); } /** * Update style of the C3DTFeatures, an allowList of tile id can be passed to only update certain tile. * Note that this function only update THREE.Object3D materials, in order to see style changes you should call view.notifyChange() * @param {Array<number>|null} [allowTileIdList] - tile ids to allow in updateStyle computation if null all tiles are updated * * @returns {boolean} true if style updated false otherwise */ updateStyle() { let allowTileIdList = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; if (!this._style) { return false; } if (!this.object3d) { return false; } const currentMaterials = []; // list materials used for this update const mapObjects3d = new Map(); this.object3d.traverse(child => { if (object3DHasFeature(child)) { const tileId = findTileID(child); if (allowTileIdList && !allowTileIdList.includes(tileId)) { return; // this tileId is not updated } // push for update style if (!mapObjects3d.has(tileId)) { mapObjects3d.set(tileId, []); } mapObjects3d.get(tileId).push(child); } }); for (const [tileId, objects3d] of mapObjects3d) { const c3DTileFeatures = this.tilesC3DTileFeatures.get(tileId); // features of this tile objects3d.forEach(object3d => { // clear object3d.geometry.clearGroups(); object3d.material = []; for (const [, c3DTileFeature] of c3DTileFeatures) { if (c3DTileFeature.object3d != object3d) { continue; // this feature do not belong to object3d } this._style.context.setGeometry({ properties: c3DTileFeature }); /** @type {THREE.Color} */ const color = new THREE.Color(this._style.fill.color); /** @type {number} */ const opacity = this._style.fill.opacity; const materialId = color.getHexString() + opacity; let material = null; if (this.#fillColorMaterialsBuffer.has(materialId)) { material = this.#fillColorMaterialsBuffer.get(materialId); } else { material = new THREE.MeshStandardMaterial({ color, opacity, transparent: opacity < 1, alphaTest: 0.09 }); this.#fillColorMaterialsBuffer.set(materialId, material); // bufferize } // compute materialIndex let materialIndex = -1; for (let index = 0; index < object3d.material.length; index++) { const childMaterial = object3d.material[index]; if (material.uuid === childMaterial.uuid) { materialIndex = index; break; } } if (materialIndex < 0) { // not in object3d.material add it object3d.material.push(material); materialIndex = object3d.material.length - 1; } // materialIndex groups is computed c3DTileFeature.groups.forEach(group => { object3d.geometry.addGroup(group.start, group.count, materialIndex); }); } optimizeGeometryGroups(object3d); // record material(s) used in object3d if (object3d.material instanceof Array) { object3d.material.forEach(material => { if (!currentMaterials.includes(material)) { currentMaterials.push(material); } }); } else if (!currentMaterials.includes(object3d.material)) { currentMaterials.push(object3d.material); } }); } // remove buffered materials not in currentMaterials for (const [id, fillMaterial] of this.#fillColorMaterialsBuffer) { if (!currentMaterials.includes(fillMaterial)) { fillMaterial.dispose(); this.#fillColorMaterialsBuffer.delete(id); } } return true; } get materialCount() { return this.#fillColorMaterialsBuffer.size; } set style(value) { if (value instanceof Style) { this._style = value; } else if (!value) { this._style = null; } else { this._style = new Style(value); } this.updateStyle(); } get style() { return this._style; } } export default C3DTilesLayer;