itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
304 lines (291 loc) • 11.2 kB
JavaScript
import * as THREE from 'three';
import ObjectRemovalHelper from "./ObjectRemovalHelper.js";
import { C3DTilesBoundingVolumeTypes } from "../Core/3DTiles/C3DTilesEnums.js";
import { C3DTILES_LAYER_EVENTS } from "../Layer/C3DTilesLayer.js";
/** @module 3dTilesProcessing
*/
function requestNewTile(view, scheduler, geometryLayer, metadata, parent, redraw) {
const command = {
/* mandatory */
view,
requester: parent,
layer: geometryLayer,
priority: parent ? 1.0 / (parent.distance + 1) : 100,
/* specific params */
metadata,
redraw
};
geometryLayer.dispatchEvent({
type: C3DTILES_LAYER_EVENTS.ON_TILE_REQUESTED,
metadata
});
return scheduler.execute(command);
}
function getChildTiles(tile) {
// only keep children that have the same layer and a valid tileId
return tile.children.filter(n => n.layer == tile.layer && n.tileId);
}
function subdivideNode(context, layer, node, cullingTest) {
if (node.additiveRefinement) {
// Additive refinement can only fetch visible children.
_subdivideNodeAdditive(context, layer, node, cullingTest);
} else {
// Substractive refinement on the other hand requires to replace
// node with all of its children
_subdivideNodeSubstractive(context, layer, node);
}
}
const tmpMatrix = new THREE.Matrix4();
function _subdivideNodeAdditive(context, layer, node, cullingTest) {
for (const child of layer.tileset.tiles[node.tileId].children) {
// child being downloaded => skip
if (child.promise || child.loaded) {
continue;
}
// 'child' is only metadata (it's *not* a THREE.Object3D). 'cullingTest' needs
// a matrixWorld, so we compute it: it's node's matrixWorld x child's transform
let overrideMatrixWorld = node.matrixWorld;
if (child.transform) {
overrideMatrixWorld = tmpMatrix.multiplyMatrices(node.matrixWorld, child.transform);
}
const isVisible = cullingTest ? !cullingTest(layer, context.camera, child, overrideMatrixWorld) : true;
// child is not visible => skip
if (!isVisible) {
continue;
}
child.promise = requestNewTile(context.view, context.scheduler, layer, child, node, true).then(tile => {
node.add(tile);
tile.updateMatrixWorld();
layer.onTileContentLoaded(tile);
context.view.notifyChange(child);
child.loaded = true;
delete child.promise;
});
}
}
function _subdivideNodeSubstractive(context, layer, node) {
if (!node.pendingSubdivision && getChildTiles(node).length == 0) {
const childrenTiles = layer.tileset.tiles[node.tileId].children;
if (childrenTiles === undefined || childrenTiles.length === 0) {
return;
}
node.pendingSubdivision = true;
const promises = [];
for (let i = 0; i < childrenTiles.length; i++) {
promises.push(requestNewTile(context.view, context.scheduler, layer, childrenTiles[i], node, false).then(tile => {
childrenTiles[i].loaded = true;
node.add(tile);
tile.updateMatrixWorld();
// TODO: remove because cannot happen?
if (node.additiveRefinement) {
context.view.notifyChange(node);
}
layer.tileset.tiles[tile.tileId].loaded = true;
layer.onTileContentLoaded(tile);
}));
}
Promise.all(promises).then(() => {
node.pendingSubdivision = false;
context.view.notifyChange(node);
});
}
}
/**
* Check if the node is visible in the camera.
*
* @param {C3DTilesLayer} layer node 3D tiles layer
* @param {Camera} camera camera
* @param {THREE.Object3D} node The 3d tile node to check.
* @param {THREE.Matrix4} tileMatrixWorld The node matrix world
* @return {boolean} return true if the node is visible
*/
export function $3dTilesCulling(layer, camera, node, tileMatrixWorld) {
// For viewer Request Volume
// https://github.com/AnalyticalGraphicsInc/3d-tiles-samples/tree/master/tilesets/TilesetWithRequestVolume
if (node.viewerRequestVolume && node.viewerRequestVolume.viewerRequestVolumeCulling(camera, tileMatrixWorld)) {
return true;
}
// For bounding volume
return !!(node.boundingVolume && node.boundingVolume.boundingVolumeCulling(camera, tileMatrixWorld));
}
// Cleanup all 3dtiles|three.js starting from a given node n.
// n's children can be of 2 types:
// - have a 'content' attribute -> it's a tileset and must
// be cleaned with cleanup3dTileset()
// - doesn't have 'content' -> it's a raw Object3D object,
// and must be cleaned with _cleanupObject3D()
function cleanup3dTileset(layer, n) {
let depth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
// If this layer is not using additive refinement, we can only
// clean a tile if all its neighbours are cleaned as well because
// a tile can only be in 2 states:
// - displayed and no children displayed
// - hidden and all of its children displayed
// So here we implement a conservative measure: if T is cleanable
// we actually only clean its children tiles.
const canCleanCompletely = n.additiveRefinement || depth > 0;
for (let i = 0; i < n.children.length; i++) {
// skip non-tiles elements
if (!n.children[i].content) {
if (canCleanCompletely) {
ObjectRemovalHelper.removeChildrenAndCleanupRecursively(n.children[i].layer, n.children[i]);
}
} else {
cleanup3dTileset(layer, n.children[i], depth + 1);
}
}
if (canCleanCompletely) {
if (n.dispose) {
n.dispose();
}
delete n.content;
layer.tileset.tiles[n.tileId].loaded = false;
n.remove(...n.children);
// and finally remove from parent
if (depth == 0 && n.parent) {
n.parent.remove(n);
}
} else {
const tiles = getChildTiles(n);
n.remove(...tiles);
}
}
// this is a layer
export function pre3dTilesUpdate(context) {
if (!this.visible) {
return [];
}
this.scale = context.camera._preSSE;
// Elements removed are added in the layer._cleanableTiles list.
// Since we simply push in this array, the first item is always
// the oldest one.
const now = Date.now();
if (this._cleanableTiles.length && now - this._cleanableTiles[0].cleanableSince > this.cleanupDelay) {
// Make sure we don't clean root tile
this.root.cleanableSince = undefined;
let i = 0;
for (; i < this._cleanableTiles.length; i++) {
const elt = this._cleanableTiles[i];
if (now - elt.cleanableSince > this.cleanupDelay) {
cleanup3dTileset(this, elt);
} else {
// later entries are younger
break;
}
}
// remove deleted elements from _cleanableTiles
this._cleanableTiles.splice(0, i);
}
return [this.root];
}
const boundingVolumeBox = new THREE.Box3();
const boundingVolumeSphere = new THREE.Sphere();
export function computeNodeSSE(camera, node) {
node.distance = 0;
if (node.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.box) {
boundingVolumeBox.copy(node.boundingVolume.volume);
boundingVolumeBox.applyMatrix4(node.matrixWorld);
node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position);
} else if (node.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.sphere || node.boundingVolume.initialVolumeType === C3DTilesBoundingVolumeTypes.region) {
boundingVolumeSphere.copy(node.boundingVolume.volume);
boundingVolumeSphere.applyMatrix4(node.matrixWorld);
// TODO: see https://github.com/iTowns/itowns/issues/800
node.distance = Math.max(0.0, boundingVolumeSphere.distanceToPoint(camera.camera3D.position));
} else {
return Infinity;
}
if (node.distance === 0) {
// This test is needed in case geometricError = distance = 0
return Infinity;
}
return camera._preSSE * (node.geometricError / node.distance);
}
export function init3dTilesLayer(view, scheduler, layer, rootTile) {
return requestNewTile(view, scheduler, layer, rootTile, undefined, true).then(tile => {
layer.object3d.add(tile);
tile.updateMatrixWorld();
layer.tileset.tiles[tile.tileId].loaded = true;
layer.root = tile;
layer.onTileContentLoaded(tile);
});
}
function setDisplayed(node, display) {
// The geometry of the tile is not in node, but in node.content
// To change the display state, we change node.content.visible instead of
// node.material.visible
if (node.content) {
node.content.visible = display;
}
}
function markForDeletion(layer, elt) {
if (!elt.cleanableSince) {
elt.cleanableSince = Date.now();
layer._cleanableTiles.push(elt);
}
}
/**
* This funcion builds the method to update 3d tiles node.
*
* The returned method checks the 3d tile visibility with `cullingTest` function.
* It subdivises visible node if `subdivisionTest` return `true`.
*
* @param {Function} [cullingTest=$3dTilesCulling] The culling test method.
* @param {Function} [subdivisionTest=$3dTilesSubdivisionControl] The subdivision test method.
* @return {Function} { description_of_the_return_value }
*/
export function process3dTilesNode() {
let cullingTest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $3dTilesCulling;
let subdivisionTest = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : $3dTilesSubdivisionControl;
return function (context, layer, node) {
// early exit if parent's subdivision is in progress
if (node.parent.pendingSubdivision && !node.parent.additiveRefinement) {
node.visible = false;
return undefined;
}
// do proper culling
const isVisible = cullingTest ? !cullingTest(layer, context.camera, node, node.matrixWorld) : true;
node.visible = isVisible;
if (isVisible) {
if (node.cleanableSince) {
layer._cleanableTiles.splice(layer._cleanableTiles.indexOf(node), 1);
node.cleanableSince = undefined;
}
let returnValue;
if (node.pendingSubdivision || subdivisionTest(context, layer, node)) {
subdivideNode(context, layer, node, cullingTest);
// display iff children aren't ready
setDisplayed(node, node.pendingSubdivision || node.additiveRefinement);
returnValue = getChildTiles(node);
} else {
setDisplayed(node, true);
for (const n of getChildTiles(node)) {
n.visible = false;
markForDeletion(layer, n);
}
}
return returnValue;
}
markForDeletion(layer, node);
};
}
/**
*
*
* the method returns true if the `node` should be subivised.
*
* @param {object} context The current context
* @param {Camera} context.camera The current camera
* @param {C3DTilesLayer} layer The 3d tile layer
* @param {THREE.Object3D} node The 3d tile node
* @return {boolean}
*/
export function $3dTilesSubdivisionControl(context, layer, node) {
if (layer.tileset.tiles[node.tileId].children === undefined) {
return false;
}
if (layer.tileset.tiles[node.tileId].isTileset) {
return true;
}
const sse = computeNodeSSE(context.camera, node);
return sse > layer.sseThreshold;
}