UNPKG

@geodanresearch/mapbox-3dtiles

Version:
799 lines (683 loc) 25.5 kB
import * as THREE from 'three'; import { DEBUG } from "./Constants.mjs" import { DebugColors, CreateDebugLabel, CreateDebugBox, CreateDebugLine } from "./Debugging.mjs" import { PNTS, B3DM, CMPT } from "./TileLoaders.mjs" import { IMesh } from "./InstancedMesh.mjs" import { LatToScale, YToLat } from "./Utils.mjs" import Tileset from './Tileset.mjs'; import { applyStyle } from './Styler.mjs' export default class ThreeDeeTile { constructor(json, resourcePath, styleParams, updateCallback, renderCallback, parentRefine, parentTransform, projectToMercator, loader, renderOptions) { this.loaded = false; this.styleParams = styleParams; this.updateCallback = updateCallback; this.renderCallback = renderCallback; this.resourcePath = resourcePath; this.projectToMercator = projectToMercator; this.loader = loader; this.totalContent = new THREE.Group(); // Three JS Object3D Group for this tile and all its children this.tileContent = new THREE.Group(); // Three JS Object3D Group for this tile's content this.childContent = new THREE.Group(); // Three JS Object3D Group for this tile's children this.totalContent.add(this.tileContent); this.totalContent.add(this.childContent); this.boundingVolume = json.boundingVolume; this.tileContentVisible = false; this.renderOptions = renderOptions; if (this.boundingVolume && this.boundingVolume.box) { let b = this.boundingVolume.box; let extent = [b[0] - b[3], b[1] - b[7], b[0] + b[3], b[1] + b[7]]; let sw = new THREE.Vector3(extent[0], extent[1], b[2] - b[11]); let ne = new THREE.Vector3(extent[2], extent[3], b[2] + b[11]); this.box = new THREE.Box3(sw, ne); } else { this.extent = null; this.sw = null; this.ne = null; this.box = null; this.center = null; } this.refine = json.refine ? json.refine.toUpperCase() : parentRefine; this.geometricError = json.geometricError; this.worldTransform = parentTransform ? parentTransform.clone() : new THREE.Matrix4(); this.transform = json.transform; if (this.transform) { let tileMatrix = new THREE.Matrix4().fromArray(this.transform); this.totalContent.applyMatrix4(tileMatrix); this.worldTransform.multiply(tileMatrix); } this.content = json.content; this.children = []; if (json.children) { for (let i = 0; i < json.children.length; i++) { let child = new ThreeDeeTile( json.children[i], resourcePath, this.styleParams, updateCallback, renderCallback, this.refine, this.worldTransform, this.projectToMercator, this.loader, this.renderOptions ); this.childContent.add(child.totalContent); this.children.push(child); } } } //ThreeDeeTile.load async load() { if (this.loaded) { return this.loaded; } if (this.loading) { return this.loaded; } this.loading = true; if (this.content) { let url = this.content.uri ? this.content.uri : this.content.url; if (!url) { this.loading = false; this.loaded = true; return this.loaded; } if (url.substr(0, 4) != 'http') { url = this.resourcePath + url; } let type = url.slice(-4); switch (type) { case 'json': // child is a tileset json this.isParentTileset = true; this.originalBox = this.box.clone(); this.originalWorldTransform = this.worldTransform.clone(); try { let subTileset = new Tileset((ts) => this.updateCallback(ts), () => this.renderCallback(), this.loader); await subTileset.load(url, this.styleParams, this.projectToMercator, this.renderOptions); //console.log(`loaded json from url ${url}`); if (subTileset.root) { this.children.push(subTileset.root); subTileset.root.box.applyMatrix4(this.worldTransform); this.childContent.add(subTileset.root.totalContent); // Threejs > 119 let inverseMatrix = new THREE.Matrix4(); inverseMatrix.copy(this.worldTransform).invert(); //end // Threejs < 120 //let inverseMatrix = new THREE.Matrix4().getInverse(this.worldTransform); //end subTileset.root.totalContent.applyMatrix4(inverseMatrix); subTileset.root.totalContent.updateMatrixWorld(); await subTileset.root.checkLoad(this.frustum, this.cameraPosition, subTileset.geometricError); } } catch (error) { // load failed (wrong url? connection issues?) // log error, do not break program flow console.error(error); } break; case 'b3dm': try { this.tileLoader = new B3DM(url); let b3dmData = await this.tileLoader.load(); this.tileLoader = null; this.b3dmAdd(b3dmData, url); } catch (error) { if (error.name === "AbortError") { //console.log(`cancelled ${url}`) this.loading = false; this.loaded = false; return this.loaded; } console.error(error); } break; case 'i3dm': try { this.tileLoader = new B3DM(url); let i3dmData = await this.tileLoader.load(); this.tileLoader = null; this.i3dmAdd(i3dmData); } catch (error) { if (error.name === "AbortError") { this.loading = false; this.loaded = false; return this.loaded; } console.error(error.message); } break; case 'pnts': try { this.tileLoader = new PNTS(url); let pointData = await this.tileLoader.load(); this.tileLoader = null; this.pntsAdd(pointData); } catch (error) { if (error.name === "AbortError") { this.loading = false; this.loaded = false; return this.loaded; } console.error(error); } break; case 'cmpt': try { this.tileLoader = new CMPT(url); let compositeTiles = await this.tileLoader.load(); this.tileLoader = null; this.cmptAdd(compositeTiles, url); } catch (error) { if (error.name === "AbortError") { this.loading = false; this.loaded = false; return this.loaded; } console.error(error); } break; default: throw new Error('invalid tile type: ' + type); } } this.loading = false; this.loaded = true; this.updateCallback(this); return this.loaded; } async cmptAdd(compositeTiles, url) { if (this.cmptAdded) { // prevent duplicate adding return; } this.cmptAdded = true; for (let innerTile of compositeTiles) { switch (innerTile.type) { case 'i3dm': let i3dm = new B3DM('.i3dm'); let i3dmData = await i3dm.parseResponse(innerTile.data); this.i3dmAdd(i3dmData); break; case 'b3dm': let b3dm = new B3DM('.b3dm'); let b3dmData = await b3dm.parseResponse(innerTile.data); this.b3dmAdd(b3dmData, url.slice(0, -4) + 'b3dm'); break; case 'pnts': let pnts = new PNTS('.pnts'); let pointData = pnts.parseResponse(innerTile.data); this.pntsAdd(pointData); break; case 'cmpt': let cmpt = new CMPT('.cmpt'); let subCompositeTiles = cmpt.parseResponse(innerTile.data); this.cmptAdd(subCompositeTiles); break; default: console.error(`Composite type ${innerTile.type} not supported`); break; } //console.log(`type: ${innerTile.type}, size: ${innerTile.data.byteLength}`); } } pntsAdd(pointData) { if (this.pntsAdded && !this.cmptAdded) { // prevent duplicate adding return; } this.pntsAdded = true; let geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(pointData.points, 3)); let material = new THREE.PointsMaterial(); material.size = this.styleParams.pointsize != null ? this.styleParams.pointsize : 1.0; if (this.styleParams.color) { material.vertexColors = THREE.NoColors; material.color = new THREE.Color(this.styleParams.color); material.opacity = this.styleParams.opacity != null ? this.styleParams.opacity : 1.0; } else if (pointData.rgba) { geometry.setAttribute('color', new THREE.Float32BufferAttribute(pointData.rgba, 4)); material.vertexColors = THREE.VertexColors; } else if (pointData.rgb) { geometry.setAttribute('color', new THREE.Float32BufferAttribute(pointData.rgb, 3)); material.vertexColors = THREE.VertexColors; } this.tileContent.add(new THREE.Points(geometry, material)); if (pointData.rtc_center) { let c = pointData.rtc_center; this.tileContent.applyMatrix4(new THREE.Matrix4().makeTranslation(c[0], c[1], c[2])); } this.tileContent.add(new THREE.Points(geometry, material)); this.renderCallback(this); } b3dmAdd(b3dmData, url) { if (this.b3dmAdded && !this.cmptAdded) { // prevent duplicate adding return; } this.b3dmAdded = true; this.loader.parse( b3dmData.glbData, this.resourcePath, (gltf) => { let scene = gltf.scene || gltf.scenes[0]; let rotateX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI / 2); scene.applyMatrix4(rotateX); // convert from GLTF Y-up to Z-up //Add the batchtable to the userData since gltfLoader doesn't deal with it scene.userData = b3dmData.batchTableJson; scene.userData.b3dm = url.replace(this.resourcePath, '').replace('.b3dm', ''); if (scene.userData && Array.isArray(b3dmData.batchTableJson.attr)) { scene.userData.attr = scene.userData.attr.map((d) => d.split(',')); delete b3dmData.batchTableJson.attr; } if (scene.userData && Array.isArray(b3dmData.batchTableJson.featureinfo)) { for (let i = 0; i < scene.userData.featureinfo.length; i++) { const data = JSON.parse(scene.userData.featureinfo[i]); let keys = Object.keys(data); if (keys.length) { for (let key of keys) { if (!scene.userData[key]) { scene.userData[key] = []; } scene.userData[key].push(data[key]); } } } delete b3dmData.batchTableJson.featureinfo; } if (this.projectToMercator) { //TODO: must be a nicer way to get the local Y in webmerc. than worldTransform.elements scene.scale.setScalar(LatToScale(YToLat(this.worldTransform.elements[13]))); } scene.traverse((child) => { if (child instanceof THREE.Mesh) { child.stylable = true; child.castShadow = this.renderOptions.castShadow; child.receiveShadow = this.renderOptions.receiveShadow; child.userData = scene.userData; child.modelType = "b3dm"; if (this.styleParams && Object.keys(this.styleParams).length > 0) { child.material = new THREE.MeshStandardMaterial({ color: '#ffffff' }); } } }); if (this.styleParams && Object.keys(this.styleParams).length > 0) { this.appliedStyle = this.styleParams.id; scene = applyStyle(scene, this.styleParams, this.renderOptions); } if (this.renderOptions.doubleSided) { scene.traverse(child=>{ if (child instanceof THREE.Mesh) { child.material.side = THREE.DoubleSide; } }) } this.tileContent.add(scene); this.renderCallback(); }, (error) => { throw new Error('error parsing gltf: ' + error); } ); } i3dmAdd(i3dmData) { if (this.i3dmAdded && !this.cmptAdded) { // prevent duplicate adding return; } this.i3dmAdded = true; // Check what metadata is present in the featuretable, currently using: https://github.com/CesiumGS/3d-tiles/tree/master/specification/TileFormats/Instanced3DModel#instance-orientation. let metadata = i3dmData.featureTableJSON; if (!metadata.POSITION) { console.error(`i3dm missing position metadata`); return; } let instancesParams = { positions: new Float32Array(i3dmData.featureTableBinary, metadata.POSITION.byteOffset, metadata.INSTANCES_LENGTH * 3) } if (metadata.RTC_CENTER) { if (Array.isArray(metadata.RTC_CENTER) && metadata.RTC_CENTER.length === 3) { instancesParams.rtcCenter = [metadata.RTC_CENTER[0], metadata.RTC_CENTER[1], metadata.RTC_CENTER[2]]; } } if (metadata.NORMAL_UP && metadata.NORMAL_RIGHT) { instancesParams.normalsRight = new Float32Array(i3dmData.featureTableBinary, metadata.NORMAL_RIGHT.byteOffset, metadata.INSTANCES_LENGTH * 3); instancesParams.normalsUp = new Float32Array(i3dmData.featureTableBinary, metadata.NORMAL_UP.byteOffset, metadata.INSTANCES_LENGTH * 3); } if (metadata.SCALE) { instancesParams.scales = new Float32Array(i3dmData.featureTableBinary, metadata.SCALE.byteOffset, metadata.INSTANCES_LENGTH); } if (metadata.SCALE_NON_UNIFORM) { instancesParams.xyzScales = new Float32Array(i3dmData.featureTableBinary, metadata.SCALE_NON_UNIFORM.byteOffset, metadata.INSTANCES_LENGTH); } // Threejs > 119 let inverseMatrix = new THREE.Matrix4(); inverseMatrix.copy(this.worldTransform).invert(); // in order to offset by the tile // end // Threejs < 120 //let inverseMatrix = new THREE.Matrix4().getInverse(this.worldTransform); // end let self = this; this.loader.parse(i3dmData.glbData, this.resourcePath, (gltf) => { let scene = gltf.scene || gltf.scenes[0]; scene.rotateX(Math.PI / 2); // convert from GLTF Y-up to Mapbox Z-up scene.updateMatrixWorld(true); scene.traverse(child => { if (child instanceof THREE.Mesh) { child.userData = i3dmData.batchTableJson; IMesh(child, instancesParams, inverseMatrix, i3dmData.modelUrl, self.renderOptions.castShadow, self.renderOptions.receiveShadow) .then(d => self.tileContent.add(d)); //const d = this._createMesh(child, instancesParams, inverseMatrix, i3dmData.modelUrl, self.castShadow, self.receiveShadow); //self.tileContent.add(d); } }); }); this.renderCallback(this); } _createMesh(inmesh, instancesParams, inverseMatrix, modelName, castShadow, receiveShadow) { const group = new THREE.Scene(); let matrix = new THREE.Matrix4(); let position = new THREE.Vector3(); let rotation = new THREE.Euler(); let quaternion = new THREE.Quaternion(); let scale = new THREE.Vector3(); let rtcCenter = instancesParams.rtcCenter ? instancesParams.rtcCenter : [0.0, 0.0, 0.0]; let geometry = inmesh.geometry; geometry.applyMatrix4(inmesh.matrixWorld); // apply world modifiers to geometry let material = inmesh.material; let positions = instancesParams.positions; let instanceCount = positions.length / 3; //let instancedMesh = new THREE.InstancedMesh(geometry, material, instanceCount); let instancedMesh = new THREE.Mesh(geometry, material); instancedMesh.modelType = "i3dm"; //instancedMesh.userData = inmesh.userData; instancedMesh.model = modelName; if (instancesParams.rtcCenter) { rtcCenter = instancesParams.rtcCenter; } for (let i = 0; i < instanceCount; i++) { position = { x: positions[i * 3] + (rtcCenter[0] + inverseMatrix.elements[12]), y: positions[i * 3 + 1] + (rtcCenter[1] + inverseMatrix.elements[13]), z: positions[i * 3 + 2] + (rtcCenter[2] + inverseMatrix.elements[14]) }; if (instancesParams.normalsRight) { rotation.set(0, 0, Math.atan2(instancesParams.normalsRight[i * 3 + 1], instancesParams.normalsRight[i * 3])); quaternion.setFromEuler(rotation); } scale.x = scale.y = scale.z = LatToScale(YToLat(positions[i * 3 + 1])); if (instancesParams.scales) { scale.x *= instancesParams.scales[i]; scale.y *= instancesParams.scales[i]; scale.z *= instancesParams.scales[i]; } if (instancesParams.xyzScales) { scale.x *= instancesParams.xyzScales[i * 3]; scale.y *= instancesParams.xyzScales[i * 3 + 1]; scale.z *= instancesParams.xyzScales[i * 3 + 2]; } matrix.compose(position, quaternion, scale); const clone = instancedMesh.clone(); clone.applyMatrix4(matrix); clone.castShadow = castShadow; clone.receiveShadow = receiveShadow; group.add(clone); } return group; } _hide() { //time: sometimes tiles are not removed with this check //if (this.tileContentVisible === true) { this.totalContent.remove(this.tileContent); this.tileContentVisible = false; //} } _hideChildren() { if (this.childContentVisible) { this.totalContent.remove(this.childContent); this.childContentVisible = false; } } _show() { if (this.tileContentVisible === false) { this.tileContentVisible = true; if (this.appliedStyle != this.styleParams.id) { applyStyle(this.tileContent, this.styleParams, this.renderOptions); } this.totalContent.add(this.tileContent); } } _exposeChildren() { if (!this.childContentVisible) { this.totalContent.add(this.childContent); this.childContentVisible = true; } } _remove(includeChildren) { if (includeChildren) { for (const child of this.children) { child._remove(includeChildren); } } if (this.loading) { if (this.tileLoader) { this.tileLoader.abortLoad(); } this.loading = false; } if (!this.loaded) { return; } this.loaded = false; this.unloadedTileContent = true; this.tileContentVisible = false; this.totalContent.remove(this.tileContent); this.freeObjectFromMemory(this.tileContent); this.tileContent = new THREE.Group(); this.totalContent.add(this.tileContent); this.b3dmAdded = false; this.i3dmAdded = false; this.cmptAdded = false; if (includeChildren) { this.unloadedChildContent = true; this.totalContent.remove(this.childContent); this.freeObjectFromMemory(this.childContent); this.totalContent.add(this.childContent); // add empty childContent to totalContent if (this.isParentTileset) { this.children = []; this.isParentTileset = false; this.unloadedChildContent = false; this.unloadedTileContent = false; } } if (DEBUG) { this._removeDebugGroup(); } } unload(includeChildren) { if (this.tileLoader) { this.tileLoader.abortLoad(); } this._remove(includeChildren); } hasLoadingChildren(node) { if (node.inView && node.children.length) { for (const child of node.children) { if (child.loading) { return true; } if (this.hasLoadingChildren(child)) { return true; } } } return false; } async checkLoad(frustum, cameraPosition, maxGeometricError) { this.frustum = frustum; this.cameraPosition = cameraPosition; let transformedBox = this.box.clone(); transformedBox.applyMatrix4(this.totalContent.matrixWorld); // is this tile inside the view? if (!frustum.intersectsBox(transformedBox)) { this.inView = false; this._hide(); this._hideChildren(); this.unload(true); return false; } this.inView = true; //console.log(`checkLoad: ${this.content?this.content.uri:this.children.length?`parent of ${this.children[0].content.uri}`:'empty leaf'}`) let worldBox = this.box.clone().applyMatrix4(this.worldTransform); let dist = worldBox.distanceToPoint(cameraPosition); if (this.renderOptions.horizonClip) { let horizon = Math.round(Math.sqrt(Math.abs(cameraPosition.z)) * this.renderOptions.horizonFactor); if (dist > horizon) { this._hide(); this._hideChildren(); return false; } } const error = Math.sqrt(dist) * 10; //const height = Math.abs(cameraPosition.z); //let mod = (height >= 1400 ? 1 : (1400 / (height)) * 1.1); const mod = 1; const renderError = error; const modMax = maxGeometricError / mod; const modLocal = this.geometricError / mod; //console.log(`modLocal: ${modLocal}, modMax: ${modMax}, renderError: ${renderError}`); if (renderError > modMax) { // tile too far this._hide(); this._hideChildren(); } else if (renderError > modLocal) { // tile in range this.load(); this._show(); if (this.loading) { if (DEBUG) { this._updateDebugGroup(renderError); } return; } // update children for range for (const child of this.children) { if (child.geometricError < modLocal) { child.checkLoad(frustum, cameraPosition, this.geometricError); } else { child.checkLoad(frustum, cameraPosition, maxGeometricError); } } this._exposeChildren(); } else if (renderError <= modLocal) { this._exposeChildren(); for (let child of this.children) { if (this.refine === 'REPLACE') { // show all immediate children, including those that are further away due to oblique viewing await child.checkLoad(frustum, cameraPosition, maxGeometricError); } else { // add children depending on viewing distance if (child.geometricError < modLocal) { await child.checkLoad(frustum, cameraPosition, this.geometricError); } else { await child.checkLoad(frustum, cameraPosition, maxGeometricError); } } } if (this.refine === 'REPLACE' && modLocal > 0) { if (!this.hasLoadingChildren(this)) { this._hide(); } } else { this.load(); this._show(); } } if (DEBUG) { this._updateDebugGroup(renderError); } return true; } disposeObject(obj) { if (obj.material && obj.material.dispose) { obj.material.dispose(); if (obj.material.map) { obj.material.map.dispose(); } } if (obj.geometry && obj.geometry.dispose) { obj.geometry.attributes.color = {}; obj.geometry.attributes.normal = {}; obj.geometry.attributes.position = {}; obj.geometry.attributes.uv = {}; obj.geometry.attributes._batchid = {}; obj.geometry.attributes = {}; obj.geometry.dispose(); obj.material = {}; } } freeObjectFromMemory(object) { object.traverse(obj => { this.disposeObject(obj); }); this.disposeObject(object); } _updateDebugGroup(distance) { if (this.tileContentVisible === false) { this._removeDebugGroup(); return; } else { this._addDebugGroup(); } const debugColor = this._getDebugColor(); const volumeBox = this.boundingVolume.box; const translation = new THREE.Matrix4().makeTranslation(volumeBox[0], volumeBox[1], volumeBox[2]); if (!this.debugGroup) { const box = CreateDebugBox(translation, volumeBox, debugColor); const line = CreateDebugLine(translation, debugColor); this.debugGroup = new THREE.Scene(); this.debugGroup.add(box); this.debugGroup.add(line); } this.debugGroup.remove(this.sprite); const tileTitle = this._getTileTitle(); const msg = " " + tileTitle + " - " + distance.toFixed(0) + " "; this.sprite = CreateDebugLabel(translation, volumeBox[11], distance, msg, debugColor); this.debugGroup.add(this.sprite); this.renderCallback(this); } _getTileTitle() { let title = ""; if (this.content) { title = this.content.uri ? this.content.uri : this.content.url; title = title.split('/')[1]; } return title; } _getDebugColor() { const parents = this._getParentCount(this.totalContent); return DebugColors[parents]; } _getParentCount(o, count = 0) { if (o.parent) { count++; return this._getParentCount(o.parent, count); } return count; } _addDebugGroup() { if (this.debugGroup && !this.debugAdded) { this.debugAdded = true; this.totalContent.add(this.debugGroup); } } _removeDebugGroup() { if (this.debugGroup && this.debugAdded) { this.totalContent.remove(this.debugGroup); this.debugAdded = false; } } }