itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
493 lines (396 loc) • 17.8 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof3 = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.$3dTilesCulling = $3dTilesCulling;
exports.pre3dTilesUpdate = pre3dTilesUpdate;
exports.computeNodeSSE = computeNodeSSE;
exports.init3dTilesLayer = init3dTilesLayer;
exports.process3dTilesNode = process3dTilesNode;
exports.$3dTilesSubdivisionControl = $3dTilesSubdivisionControl;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
var THREE = _interopRequireWildcard(require("three"));
var _Extent = _interopRequireDefault(require("../Core/Geographic/Extent"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof3(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
/** @module 3dTilesProcessing
*/
function requestNewTile(view, scheduler, geometryLayer, metadata, parent, redraw) {
var command = {
/* mandatory */
view: view,
requester: parent,
layer: geometryLayer,
priority: parent ? 1.0 / (parent.distance + 1) : 100,
/* specific params */
metadata: metadata,
redraw: redraw
};
return scheduler.execute(command);
}
function getChildTiles(tile) {
// only keep children that have the same layer and a valid tileId
return tile.children.filter(function (n) {
return 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);
}
}
var tmpBox3 = new THREE.Box3();
var tmpSphere = new THREE.Sphere();
function boundingVolumeToExtent(crs, volume, transform) {
if (volume.region) {
var box = tmpBox3.copy(volume.region.box3D).applyMatrix4(volume.region.matrixWorld);
return _Extent["default"].fromBox3(crs, box);
} else if (volume.box) {
var _box = tmpBox3.copy(volume.box).applyMatrix4(transform);
return _Extent["default"].fromBox3(crs, _box);
} else {
var sphere = tmpSphere.copy(volume.sphere).applyMatrix4(transform);
return new _Extent["default"](crs, {
west: sphere.center.x - sphere.radius,
east: sphere.center.x + sphere.radius,
south: sphere.center.y - sphere.radius,
north: sphere.center.y + sphere.radius
});
}
}
var tmpMatrix = new THREE.Matrix4();
function _subdivideNodeAdditive(context, layer, node, cullingTest) {
var _iterator = _createForOfIteratorHelper(layer.tileset.tiles[node.tileId].children),
_step;
try {
var _loop = function () {
var child = _step.value;
// child being downloaded => skip
if (child.promise || child.loaded) {
return "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
var overrideMatrixWorld = node.matrixWorld;
if (child.transform) {
overrideMatrixWorld = tmpMatrix.multiplyMatrices(node.matrixWorld, child.transform);
}
var isVisible = cullingTest ? !cullingTest(layer, context.camera, child, overrideMatrixWorld) : true; // child is not visible => skip
if (!isVisible) {
return "continue";
}
child.promise = requestNewTile(context.view, context.scheduler, layer, child, node, true).then(function (tile) {
node.add(tile);
tile.updateMatrixWorld();
var extent = boundingVolumeToExtent(layer.extent.crs, tile.boundingVolume, tile.matrixWorld);
tile.traverse(function (obj) {
obj.extent = extent;
});
layer.onTileContentLoaded(tile);
context.view.notifyChange(child);
child.loaded = true;
delete child.promise;
});
};
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _ret = _loop();
if (_ret === "continue") continue;
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
function _subdivideNodeSubstractive(context, layer, node) {
if (!node.pendingSubdivision && getChildTiles(node).length == 0) {
var _ret2 = function () {
var childrenTiles = layer.tileset.tiles[node.tileId].children;
if (childrenTiles === undefined || childrenTiles.length === 0) {
return {
v: void 0
};
}
node.pendingSubdivision = true;
var promises = [];
var _loop2 = function (i) {
promises.push(requestNewTile(context.view, context.scheduler, layer, childrenTiles[i], node, false).then(function (tile) {
childrenTiles[i].loaded = true;
node.add(tile);
tile.updateMatrixWorld();
if (node.additiveRefinement) {
context.view.notifyChange(node);
}
layer.tileset.tiles[tile.tileId].loaded = true;
layer.onTileContentLoaded(tile);
}));
};
for (var i = 0; i < childrenTiles.length; i++) {
_loop2(i);
}
Promise.all(promises).then(function () {
node.pendingSubdivision = false;
context.view.notifyChange(node);
});
}();
if ((0, _typeof2["default"])(_ret2) === "object") return _ret2.v;
}
}
/**
* 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
*/
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
if (node.boundingVolume && node.boundingVolume.boundingVolumeCulling(camera, tileMatrixWorld)) {
return true;
}
return false;
} // 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) {
var 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.
var canCleanCompletely = n.additiveRefinement || depth > 0;
for (var i = 0; i < n.children.length; i++) {
// skip non-tiles elements
if (!n.children[i].content) {
if (canCleanCompletely) {
n.children[i].traverse(_cleanupObject3D);
}
} 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.apply(n, (0, _toConsumableArray2["default"])(n.children)); // and finally remove from parent
if (depth == 0 && n.parent) {
n.parent.remove(n);
}
} else {
var tiles = getChildTiles(n);
n.remove.apply(n, (0, _toConsumableArray2["default"])(tiles));
}
} // This function is used to cleanup a Object3D hierarchy.
// (no 3dtiles spectific code here because this is managed by cleanup3dTileset)
function _cleanupObject3D(n) {
// all children of 'n' are raw Object3D
var _iterator2 = _createForOfIteratorHelper(n.children),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var child = _step2.value;
_cleanupObject3D(child);
} // free resources
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
if (n.material) {
// material can be either a THREE.Material object, or an array of
// THREE.Material objects
if (Array.isArray(n.material)) {
var _iterator3 = _createForOfIteratorHelper(n.material),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var material = _step3.value;
material.dispose();
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
} else {
n.material.dispose();
}
}
if (n.geometry) {
n.geometry.dispose();
}
n.remove.apply(n, (0, _toConsumableArray2["default"])(n.children));
} // this is a layer
function pre3dTilesUpdate() {
if (!this.visible) {
return [];
} // Elements removed are added in the layer._cleanableTiles list.
// Since we simply push in this array, the first item is always
// the oldest one.
var 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;
var i = 0;
for (; i < this._cleanableTiles.length; i++) {
var 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];
}
var boundingVolumeBox = new THREE.Box3();
var boundingVolumeSphere = new THREE.Sphere();
function computeNodeSSE(camera, node) {
node.distance = 0;
if (node.boundingVolume.region) {
boundingVolumeBox.copy(node.boundingVolume.region.box3D);
boundingVolumeBox.applyMatrix4(node.boundingVolume.region.matrixWorld);
node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position);
} else if (node.boundingVolume.box) {
// boundingVolume.box is affected by matrixWorld
boundingVolumeBox.copy(node.boundingVolume.box);
boundingVolumeBox.applyMatrix4(node.matrixWorld);
node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position);
} else if (node.boundingVolume.sphere) {
// boundingVolume.sphere is affected by matrixWorld
boundingVolumeSphere.copy(node.boundingVolume.sphere);
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);
}
function init3dTilesLayer(view, scheduler, layer, rootTile) {
return requestNewTile(view, scheduler, layer, rootTile, undefined, true).then(function (tile) {
layer.object3d.add(tile);
tile.updateMatrixWorld();
layer.tileset.tiles[tile.tileId].loaded = true;
layer.root = tile;
layer.extent = boundingVolumeToExtent(layer.crs || view.referenceCrs, tile.boundingVolume, tile.matrixWorld);
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 }
*/
function process3dTilesNode() {
var cullingTest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $3dTilesCulling;
var 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
var 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;
}
var 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);
var _iterator4 = _createForOfIteratorHelper(getChildTiles(node)),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var n = _step4.value;
n.visible = false;
markForDeletion(layer, n);
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
}
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}
*/
function $3dTilesSubdivisionControl(context, layer, node) {
if (layer.tileset.tiles[node.tileId].children === undefined) {
return false;
}
if (layer.tileset.tiles[node.tileId].isTileset) {
return true;
}
var sse = computeNodeSSE(context.camera, node);
return sse > layer.sseThreshold;
}