diff --git a/Apps/Sandcastle/gallery/Sample Height from 3D Tiles.html b/Apps/Sandcastle/gallery/Sample Height from 3D Tiles.html new file mode 100644 index 000000000000..c8ee45d89b89 --- /dev/null +++ b/Apps/Sandcastle/gallery/Sample Height from 3D Tiles.html @@ -0,0 +1,106 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Sample Height from 3D Tiles.jpg b/Apps/Sandcastle/gallery/Sample Height from 3D Tiles.jpg new file mode 100644 index 000000000000..bda74e9bdfb4 Binary files /dev/null and b/Apps/Sandcastle/gallery/Sample Height from 3D Tiles.jpg differ diff --git a/CHANGES.md b/CHANGES.md index 77259ebefb55..832b23c0bc59 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ Change Log ### 1.52 - 2018-12-03 ##### Additions :tada: +* Added functions to get the most detailed height of 3D Tiles on-screen or off-screen. [#7115](https://github.com/AnalyticalGraphicsInc/cesium/pull/7115) + * Added `Scene.sampleHeightMostDetailed`, an asynchronous version of `Scene.sampleHeight` that uses the maximum level of detail for 3D Tiles. + * Added `Scene.clampToHeightMostDetailed`, an asynchronous version of `Scene.clampToHeight` that uses the maximum level of detail for 3D Tiles. * Added `Scene.invertClassificationSupported` for checking if invert classification is supported. * Added `computeLineSegmentLineSegmentIntersection` to `Intersections2D`. [#7228](https://github.com/AnalyticalGraphicsInc/Cesium/pull/7228) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index e0449e75ae1b..8b394390f0b6 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -29,6 +29,7 @@ define([ './Cesium3DTileContentState', './Cesium3DTileOptimizations', './Cesium3DTileRefine', + './Cesium3DTilesetAsyncTraversal', './Cesium3DTilesetCache', './Cesium3DTilesetStatistics', './Cesium3DTilesetTraversal', @@ -73,6 +74,7 @@ define([ Cesium3DTileContentState, Cesium3DTileOptimizations, Cesium3DTileRefine, + Cesium3DTilesetAsyncTraversal, Cesium3DTilesetCache, Cesium3DTilesetStatistics, Cesium3DTilesetTraversal, @@ -204,6 +206,7 @@ define([ this._statistics = new Cesium3DTilesetStatistics(); this._statisticsLastRender = new Cesium3DTilesetStatistics(); this._statisticsLastPick = new Cesium3DTilesetStatistics(); + this._statisticsLastAsync = new Cesium3DTilesetStatistics(); this._tilesLoaded = false; this._initialTilesLoaded = false; @@ -1707,9 +1710,9 @@ define([ function updateTiles(tileset, frameState) { tileset._styleEngine.applyStyle(tileset, frameState); + var statistics = tileset._statistics; var passes = frameState.passes; var isRender = passes.render; - var statistics = tileset._statistics; var commandList = frameState.commandList; var numberOfInitialCommands = commandList.length; var selectedTiles = tileset._selectedTiles; @@ -1899,69 +1902,76 @@ define([ /////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - Cesium3DTileset.prototype.update = function(frameState) { + function update(tileset, frameState) { if (frameState.mode === SceneMode.MORPHING) { - return; + return false; } - if (!this.show || !this.ready) { - return; + if (!tileset.show || !tileset.ready) { + return false; } - if (!defined(this._loadTimestamp)) { - this._loadTimestamp = JulianDate.clone(frameState.time); + if (!defined(tileset._loadTimestamp)) { + tileset._loadTimestamp = JulianDate.clone(frameState.time); } // Update clipping planes - var clippingPlanes = this._clippingPlanes; - this._clippingPlanesOriginMatrixDirty = true; + var clippingPlanes = tileset._clippingPlanes; + tileset._clippingPlanesOriginMatrixDirty = true; if (defined(clippingPlanes) && clippingPlanes.enabled) { clippingPlanes.update(frameState); } - this._timeSinceLoad = Math.max(JulianDate.secondsDifference(frameState.time, this._loadTimestamp) * 1000, 0.0); + tileset._timeSinceLoad = Math.max(JulianDate.secondsDifference(frameState.time, tileset._loadTimestamp) * 1000, 0.0); - this._skipLevelOfDetail = this.skipLevelOfDetail && !defined(this._classificationType) && !this._disableSkipLevelOfDetail && !this._allTilesAdditive; + tileset._skipLevelOfDetail = tileset.skipLevelOfDetail && !defined(tileset._classificationType) && !tileset._disableSkipLevelOfDetail && !tileset._allTilesAdditive; // Do out-of-core operations (new content requests, cache removal, // process new tiles) only during the render pass. var passes = frameState.passes; var isRender = passes.render; var isPick = passes.pick; + var isAsync = passes.async; - var statistics = this._statistics; + var statistics = tileset._statistics; statistics.clear(); - if (this.dynamicScreenSpaceError) { - updateDynamicScreenSpaceError(this, frameState); + if (tileset.dynamicScreenSpaceError) { + updateDynamicScreenSpaceError(tileset, frameState); } if (isRender) { - this._cache.reset(); + tileset._cache.reset(); + } + + var ready; + + if (isAsync) { + ready = Cesium3DTilesetAsyncTraversal.selectTiles(tileset, frameState); + } else { + ready = Cesium3DTilesetTraversal.selectTiles(tileset, frameState); } - Cesium3DTilesetTraversal.selectTiles(this, frameState); + if (isRender || isAsync) { + requestTiles(tileset); + } if (isRender) { - requestTiles(this); - processTiles(this, frameState); + processTiles(tileset, frameState); } - updateTiles(this, frameState); + updateTiles(tileset, frameState); if (isRender) { - unloadTiles(this); + unloadTiles(tileset); // Events are raised (added to the afterRender queue) here since promises // may resolve outside of the update loop that then raise events, e.g., // model's readyPromise. - raiseLoadProgressEvent(this, frameState); + raiseLoadProgressEvent(tileset, frameState); if (statistics.selected !== 0) { - var credits = this._credits; + var credits = tileset._credits; if (defined(credits)) { var length = credits.length; for (var i = 0; i < length; i++) { @@ -1972,8 +1982,24 @@ define([ } // Update last statistics - var statisticsLast = isPick ? this._statisticsLastPick : this._statisticsLastRender; + var statisticsLast = isAsync ? tileset._statisticsLastAsync : (isPick ? tileset._statisticsLastPick : tileset._statisticsLastRender); Cesium3DTilesetStatistics.clone(statistics, statisticsLast); + + return ready; + } + + /** + * @private + */ + Cesium3DTileset.prototype.update = function(frameState) { + update(this, frameState); + }; + + /** + * @private + */ + Cesium3DTileset.prototype.updateAsync = function(frameState) { + return update(this, frameState); }; /** diff --git a/Source/Scene/Cesium3DTilesetAsyncTraversal.js b/Source/Scene/Cesium3DTilesetAsyncTraversal.js new file mode 100644 index 000000000000..604961041135 --- /dev/null +++ b/Source/Scene/Cesium3DTilesetAsyncTraversal.js @@ -0,0 +1,137 @@ +define([ + '../Core/Intersect', + '../Core/ManagedArray', + './Cesium3DTileRefine' + ], function( + Intersect, + ManagedArray, + Cesium3DTileRefine) { + 'use strict'; + + /** + * Traversal that loads all leaves that intersect the camera frustum. + * Used to determine ray-tileset intersections during a pickFromRayMostDetailed call. + * + * @private + */ + function Cesium3DTilesetAsyncTraversal() { + } + + var asyncTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0 + }; + + Cesium3DTilesetAsyncTraversal.selectTiles = function(tileset, frameState) { + tileset._selectedTiles.length = 0; + tileset._requestedTiles.length = 0; + tileset._hasMixedContent = false; + + var ready = true; + + var root = tileset.root; + root.updateVisibility(frameState); + + if (!isVisible(root)) { + return ready; + } + + var stack = asyncTraversal.stack; + stack.push(tileset.root); + + while (stack.length > 0) { + asyncTraversal.stackMaximumLength = Math.max(asyncTraversal.stackMaximumLength, stack.length); + + var tile = stack.pop(); + var add = (tile.refine === Cesium3DTileRefine.ADD); + var replace = (tile.refine === Cesium3DTileRefine.REPLACE); + var traverse = canTraverse(tileset, tile); + + if (traverse) { + updateAndPushChildren(tileset, tile, stack, frameState); + } + + if (add || (replace && !traverse)) { + loadTile(tileset, tile); + selectDesiredTile(tileset, tile, frameState); + + if (!hasEmptyContent(tile) && !tile.contentAvailable) { + ready = false; + } + } + + visitTile(tileset); + touchTile(tileset, tile); + } + + asyncTraversal.stack.trim(asyncTraversal.stackMaximumLength); + + return ready; + }; + + function isVisible(tile) { + return tile._visible && tile._inRequestVolume; + } + + function hasEmptyContent(tile) { + return tile.hasEmptyContent || tile.hasTilesetContent; + } + + function hasUnloadedContent(tile) { + return !hasEmptyContent(tile) && tile.contentUnloaded; + } + + function canTraverse(tileset, tile) { + if (tile.children.length === 0) { + return false; + } + + if (tile.hasTilesetContent) { + // Traverse external tileset to visit its root tile + // Don't traverse if the subtree is expired because it will be destroyed + return !tile.contentExpired; + } + + if (tile.hasEmptyContent) { + return true; + } + + return true; // Keep traversing until a leave is hit + } + + function updateAndPushChildren(tileset, tile, stack, frameState) { + var children = tile.children; + var length = children.length; + + for (var i = 0; i < length; ++i) { + var child = children[i]; + child.updateVisibility(frameState); + if (isVisible(child)) { + stack.push(child); + } + } + } + + function loadTile(tileset, tile) { + if (hasUnloadedContent(tile) || tile.contentExpired) { + tile._priority = 0.0; // Highest priority + tileset._requestedTiles.push(tile); + } + } + + function touchTile(tileset, tile) { + tileset._cache.touch(tile); + } + + function visitTile(tileset) { + ++tileset.statistics.visited; + } + + function selectDesiredTile(tileset, tile, frameState) { + if (tile.contentAvailable && (tile.contentVisibility(frameState) !== Intersect.OUTSIDE)) { + tileset._selectedTiles.push(tile); + } + } + + return Cesium3DTilesetAsyncTraversal; +}); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 23c4e52a018f..e9c0a0e962fe 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -84,6 +84,8 @@ define([ descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength); selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength); selectionTraversal.ancestorStack.trim(selectionTraversal.ancestorStackMaximumLength); + + return true; }; function executeBaseTraversal(tileset, root, frameState) { diff --git a/Source/Scene/FrameState.js b/Source/Scene/FrameState.js index a4796c2352be..e886cf2085de 100644 --- a/Source/Scene/FrameState.js +++ b/Source/Scene/FrameState.js @@ -166,7 +166,14 @@ define([ * @type {Boolean} * @default false */ - offscreen : false + offscreen : false, + + /** + * true if the primitive should update for an async pass, false otherwise. + * @type {Boolean} + * @default false + */ + async : false }; /** diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index 504883467866..be9e08da12ae 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -49,9 +49,11 @@ define([ '../Renderer/ShaderProgram', '../Renderer/ShaderSource', '../Renderer/Texture', + '../ThirdParty/when', './BrdfLutGenerator', './Camera', './Cesium3DTileFeature', + './Cesium3DTileset', './CreditDisplay', './DebugCameraPrimitive', './DepthPlane', @@ -128,9 +130,11 @@ define([ ShaderProgram, ShaderSource, Texture, + when, BrdfLutGenerator, Camera, Cesium3DTileFeature, + Cesium3DTileset, CreditDisplay, DebugCameraPrimitive, DepthPlane, @@ -166,6 +170,14 @@ define([ }; }; + function AsyncRayPick(ray, primitives) { + this.ray = ray; + this.primitives = primitives; + this.ready = false; + this.deferred = when.defer(); + this.promise = this.deferred.promise; + } + /** * The container for all 3D graphical objects and state in a Cesium virtual scene. Generally, * a scene is not created directly; instead, it is implicitly created by {@link CesiumWidget}. @@ -279,6 +291,8 @@ define([ this._primitives = new PrimitiveCollection(); this._groundPrimitives = new PrimitiveCollection(); + this._asyncRayPicks = []; + this._logDepthBuffer = context.fragmentDepth; this._logDepthBufferDirty = true; @@ -885,13 +899,14 @@ define([ }, /** - * Returns true if the {@link Scene#sampleHeight} function is supported. + * Returns true if the {@link Scene#sampleHeight} and {@link Scene#sampleHeightMostDetailed} functions are supported. * @memberof Scene.prototype * * @type {Boolean} * @readonly * * @see Scene#sampleHeight + * @see Scene#sampleHeightMostDetailed */ sampleHeightSupported : { get : function() { @@ -900,13 +915,14 @@ define([ }, /** - * Returns true if the {@link Scene#clampToHeight} function is supported. + * Returns true if the {@link Scene#clampToHeight} and {@link Scene#clampToHeightMostDetailed} functions are supported. * @memberof Scene.prototype * * @type {Boolean} * @readonly * * @see Scene#clampToHeight + * @see Scene#clampToHeightMostDetailed */ clampToHeightSupported : { get : function() { @@ -1586,6 +1602,7 @@ define([ passes.depth = false; passes.postProcess = false; passes.offscreen = false; + passes.async = false; } function updateFrameNumber(scene, frameNumber, time) { @@ -3019,6 +3036,8 @@ define([ scene.globe.update(frameState); } + updateAsyncRayPicks(scene); + frameState.creditDisplay.update(); } @@ -3660,6 +3679,85 @@ define([ camera.right = right; } + function updateAsyncRayPick(scene, asyncRayPick) { + var context = scene._context; + var uniformState = context.uniformState; + var frameState = scene._frameState; + + var view = scene._pickOffscreenView; + scene._view = view; + + var ray = asyncRayPick.ray; + var primitives = asyncRayPick.primitives; + + updateCameraFromRay(ray, view.camera); + + updateFrameState(scene); + frameState.passes.offscreen = true; + frameState.passes.async = true; + + uniformState.update(frameState); + + var commandList = frameState.commandList; + var commandsLength = commandList.length; + + var ready = true; + var primitivesLength = primitives.length; + for (var i = 0; i < primitivesLength; ++i) { + var primitive = primitives[i]; + if (primitive.show && scene.primitives.contains(primitive)) { + // Only update primitives that are still contained in the scene's primitive collection and are still visible + // Update primitives continually until all primitives are ready. This way tiles are never removed from the cache. + var primitiveReady = primitive.updateAsync(frameState); + ready = (ready && primitiveReady); + } + } + + // Ignore commands pushed during async pass + commandList.length = commandsLength; + + scene._view = scene._defaultView; + + if (ready) { + asyncRayPick.deferred.resolve(); + } + + return ready; + } + + function updateAsyncRayPicks(scene) { + // Modifies array during iteration + var asyncRayPicks = scene._asyncRayPicks; + for (var i = 0; i < asyncRayPicks.length; ++i) { + if (updateAsyncRayPick(scene, asyncRayPicks[i])) { + asyncRayPicks.splice(i--, 1); + } + } + } + + function launchAsyncRayPick(scene, ray, objectsToExclude, callback) { + var asyncPrimitives = []; + var primitives = scene.primitives; + var length = primitives.length; + for (var i = 0; i < length; ++i) { + var primitive = primitives.get(i); + if ((primitive instanceof Cesium3DTileset) && primitive.show) { + if (!defined(objectsToExclude) || objectsToExclude.indexOf(primitive) === -1) { + asyncPrimitives.push(primitive); + } + } + } + if (asyncPrimitives.length === 0) { + return when.resolve(callback()); + } + + var asyncRayPick = new AsyncRayPick(ray, asyncPrimitives); + scene._asyncRayPicks.push(asyncRayPick); + return asyncRayPick.promise.then(function() { + return callback(); + }); + } + function isExcluded(object, objectsToExclude) { if (!defined(object) || !defined(objectsToExclude) || objectsToExclude.length === 0) { return false; @@ -3669,7 +3767,7 @@ define([ (objectsToExclude.indexOf(object.id) > -1); } - function getRayIntersection(scene, ray, objectsToExclude, requirePosition) { + function getRayIntersection(scene, ray, objectsToExclude, requirePosition, async) { var context = scene._context; var uniformState = context.uniformState; var frameState = scene._frameState; @@ -3689,6 +3787,7 @@ define([ frameState.invertClassification = false; frameState.passes.pick = true; frameState.passes.offscreen = true; + frameState.passes.async = async; uniformState.update(frameState); @@ -3727,22 +3826,22 @@ define([ } } - function getRayIntersections(scene, ray, limit, objectsToExclude, requirePosition) { + function getRayIntersections(scene, ray, limit, objectsToExclude, requirePosition, async) { var pickCallback = function() { - return getRayIntersection(scene, ray, objectsToExclude, requirePosition); + return getRayIntersection(scene, ray, objectsToExclude, requirePosition, async); }; return drillPick(limit, pickCallback); } - function pickFromRay(scene, ray, objectsToExclude, requirePosition) { - var results = getRayIntersections(scene, ray, 1, objectsToExclude, requirePosition); + function pickFromRay(scene, ray, objectsToExclude, requirePosition, async) { + var results = getRayIntersections(scene, ray, 1, objectsToExclude, requirePosition, async); if (results.length > 0) { return results[0]; } } - function drillPickFromRay(scene, ray, limit, objectsToExclude, requirePosition) { - return getRayIntersections(scene, ray, limit, objectsToExclude, requirePosition); + function drillPickFromRay(scene, ray, limit, objectsToExclude, requirePosition, async) { + return getRayIntersections(scene, ray, limit, objectsToExclude, requirePosition, async); } /** @@ -3758,7 +3857,7 @@ define([ * @private * * @param {Ray} ray The ray. - * @param {Object[]} [objectsToExclude] A list of primitives, entities, or features to exclude from the ray intersection. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to exclude from the ray intersection. * @returns {Object} An object containing the object and position of the first intersection. * * @exception {DeveloperError} Ray intersections are only supported in 3D mode. @@ -3770,7 +3869,7 @@ define([ throw new DeveloperError('Ray intersections are only supported in 3D mode.'); } //>>includeEnd('debug'); - return pickFromRay(this, ray, objectsToExclude, false); + return pickFromRay(this, ray, objectsToExclude, false, false); }; /** @@ -3788,7 +3887,7 @@ define([ * * @param {Ray} ray The ray. * @param {Number} [limit=Number.MAX_VALUE] If supplied, stop finding intersections after this many intersections. - * @param {Object[]} [objectsToExclude] A list of primitives, entities, or features to exclude from the ray intersection. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to exclude from the ray intersection. * @returns {Object[]} List of objects containing the object and position of each intersection. * * @exception {DeveloperError} Ray intersections are only supported in 3D mode. @@ -3800,7 +3899,62 @@ define([ throw new DeveloperError('Ray intersections are only supported in 3D mode.'); } //>>includeEnd('debug'); - return drillPickFromRay(this, ray, limit, objectsToExclude, false); + return drillPickFromRay(this, ray, limit, objectsToExclude, false, false); + }; + + /** + * Initiates an asynchronous {@link Scene#pickFromRay} request using the maximum level of detail for 3D Tilesets + * regardless of visibility. + * + * @private + * + * @param {Ray} ray The ray. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to exclude from the ray intersection. + * @returns {Promise.} A promise that resolves to an object containing the object and position of the first intersection. + * + * @exception {DeveloperError} Ray intersections are only supported in 3D mode. + */ + Scene.prototype.pickFromRayMostDetailed = function(ray, objectsToExclude) { + //>>includeStart('debug', pragmas.debug); + Check.defined('ray', ray); + if (this._mode !== SceneMode.SCENE3D) { + throw new DeveloperError('Ray intersections are only supported in 3D mode.'); + } + //>>includeEnd('debug'); + var that = this; + ray = Ray.clone(ray); + objectsToExclude = defined(objectsToExclude) ? objectsToExclude.slice() : objectsToExclude; + return launchAsyncRayPick(this, ray, objectsToExclude, function() { + return pickFromRay(that, ray, objectsToExclude, false, true); + }); + }; + + /** + * Initiates an asynchronous {@link Scene#drillPickFromRay} request using the maximum level of detail for 3D Tilesets + * regardless of visibility. + * + * @private + * + * @param {Ray} ray The ray. + * @param {Number} [limit=Number.MAX_VALUE] If supplied, stop finding intersections after this many intersections. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to exclude from the ray intersection. + * @returns {Promise.} A promise that resolves to a list of objects containing the object and position of each intersection. + * + * @exception {DeveloperError} Ray intersections are only supported in 3D mode. + */ + Scene.prototype.drillPickFromRayMostDetailed = function(ray, limit, objectsToExclude) { + //>>includeStart('debug', pragmas.debug); + Check.defined('ray', ray); + if (this._mode !== SceneMode.SCENE3D) { + throw new DeveloperError('Ray intersections are only supported in 3D mode.'); + } + //>>includeEnd('debug'); + var that = this; + ray = Ray.clone(ray); + objectsToExclude = defined(objectsToExclude) ? objectsToExclude.slice() : objectsToExclude; + return launchAsyncRayPick(this, ray, objectsToExclude, function() { + return drillPickFromRay(that, ray, limit, objectsToExclude, false, true); + }); }; var scratchSurfacePosition = new Cartesian3(); @@ -3837,19 +3991,47 @@ define([ return cartographic.height; } + function sampleHeightMostDetailed(scene, cartographic, objectsToExclude) { + var ray = getRayForSampleHeight(scene, cartographic); + return launchAsyncRayPick(scene, ray, objectsToExclude, function() { + var pickResult = pickFromRay(scene, ray, objectsToExclude, true, true); + if (defined(pickResult)) { + return getHeightFromCartesian(scene, pickResult.position); + } + }); + } + + function clampToHeightMostDetailed(scene, cartesian, objectsToExclude, result) { + var ray = getRayForClampToHeight(scene, cartesian); + return launchAsyncRayPick(scene, ray, objectsToExclude, function() { + var pickResult = pickFromRay(scene, ray, objectsToExclude, true, true); + if (defined(pickResult)) { + return Cartesian3.clone(pickResult.position, result); + } + }); + } + /** * Returns the height of scene geometry at the given cartographic position or undefined if there was no - * scene geometry to sample height from. May be used to clamp objects to the globe, 3D Tiles, or primitives in the scene. + * scene geometry to sample height from. The height of the input position is ignored. May be used to clamp objects to + * the globe, 3D Tiles, or primitives in the scene. *

* This function only samples height from globe tiles and 3D Tiles that are rendered in the current view. Samples height * from all other primitives regardless of their visibility. *

* * @param {Cartographic} position The cartographic position to sample height from. - * @param {Object[]} [objectsToExclude] A list of primitives, entities, or features to not sample height from. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to not sample height from. * @returns {Number} The height. This may be undefined if there was no scene geometry to sample height from. * + * @example + * var position = new Cesium.Cartographic(-1.31968, 0.698874); + * var height = viewer.scene.sampleHeight(position); + * console.log(height); + * * @see Scene#clampToHeight + * @see Scene#clampToHeightMostDetailed + * @see Scene#sampleHeightMostDetailed * * @exception {DeveloperError} sampleHeight is only supported in 3D mode. * @exception {DeveloperError} sampleHeight requires depth texture support. Check sampleHeightSupported. @@ -3865,7 +4047,7 @@ define([ } //>>includeEnd('debug'); var ray = getRayForSampleHeight(this, position); - var pickResult = pickFromRay(this, ray, objectsToExclude, true); + var pickResult = pickFromRay(this, ray, objectsToExclude, true, false); if (defined(pickResult)) { return getHeightFromCartesian(this, pickResult.position); } @@ -3881,11 +4063,18 @@ define([ *

* * @param {Cartesian3} cartesian The cartesian position. - * @param {Object[]} [objectsToExclude] A list of primitives, entities, or features to not clamp to. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to not clamp to. * @param {Cartesian3} [result] An optional object to return the clamped position. * @returns {Cartesian3} The modified result parameter or a new Cartesian3 instance if one was not provided. This may be undefined if there was no scene geometry to clamp to. * + * @example + * // Clamp an entity to the underlying scene geometry + * var position = entity.position.getValue(Cesium.JulianDate.now()); + * entity.position = viewer.scene.clampToHeight(position); + * * @see Scene#sampleHeight + * @see Scene#sampleHeightMostDetailed + * @see Scene#clampToHeightMostDetailed * * @exception {DeveloperError} clampToHeight is only supported in 3D mode. * @exception {DeveloperError} clampToHeight requires depth texture support. Check clampToHeightSupported. @@ -3901,12 +4090,115 @@ define([ } //>>includeEnd('debug'); var ray = getRayForClampToHeight(this, cartesian); - var pickResult = pickFromRay(this, ray, objectsToExclude, true); + var pickResult = pickFromRay(this, ray, objectsToExclude, true, false); if (defined(pickResult)) { return Cartesian3.clone(pickResult.position, result); } }; + /** + * Initiates an asynchronous {@link Scene#sampleHeight} query for an array of {@link Cartographic} positions + * using the maximum level of detail for 3D Tilesets in the scene. The height of the input positions is ignored. + * Returns a promise that is resolved when the query completes. Each point height is modified in place. + * If a height cannot be determined because no geometry can be sampled at that location, or another error occurs, + * the height is set to undefined. + * + * @param {Cartographic[]} positions The cartographic positions to update with sampled heights. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to not sample height from. + * @returns {Promise.} A promise that resolves to the provided list of positions when the query has completed. + * + * @example + * var positions = [ + * new Cesium.Cartographic(-1.31968, 0.69887), + * new Cesium.Cartographic(-1.10489, 0.83923) + * ]; + * var promise = viewer.scene.sampleHeightMostDetailed(positions); + * promise.then(function(updatedPosition) { + * // positions[0].height and positions[1].height have been updated. + * // updatedPositions is just a reference to positions. + * } + * + * @see Scene#sampleHeight + * + * @exception {DeveloperError} sampleHeightMostDetailed is only supported in 3D mode. + * @exception {DeveloperError} sampleHeightMostDetailed requires depth texture support. Check sampleHeightSupported. + */ + Scene.prototype.sampleHeightMostDetailed = function(positions, objectsToExclude) { + //>>includeStart('debug', pragmas.debug); + Check.defined('positions', positions); + if (this._mode !== SceneMode.SCENE3D) { + throw new DeveloperError('sampleHeightMostDetailed is only supported in 3D mode.'); + } + if (!this.sampleHeightSupported) { + throw new DeveloperError('sampleHeightMostDetailed requires depth texture support. Check sampleHeightSupported.'); + } + //>>includeEnd('debug'); + objectsToExclude = defined(objectsToExclude) ? objectsToExclude.slice() : objectsToExclude; + var length = positions.length; + var promises = new Array(length); + for (var i = 0; i < length; ++i) { + promises[i] = sampleHeightMostDetailed(this, positions[i], objectsToExclude); + } + return when.all(promises).then(function(heights) { + var length = heights.length; + for (var i = 0; i < length; ++i) { + positions[i].height = heights[i]; + } + return positions; + }); + }; + + /** + * Initiates an asynchronous {@link Scene#clampToHeight} query for an array of {@link Cartesian3} positions + * using the maximum level of detail for 3D Tilesets in the scene. Returns a promise that is resolved when + * the query completes. Each position is modified in place. If a position cannot be clamped because no geometry + * can be sampled at that location, or another error occurs, the element in the array is set to undefined. + * + * @param {Cartesian3[]} cartesians The cartesian positions to update with clamped positions. + * @param {Object[]} [objectsToExclude] A list of primitives, entities, or 3D Tiles features to not clamp to. + * @returns {Promise.} A promise that resolves to the provided list of positions when the query has completed. + * + * @example + * var cartesians = [ + * entities[0].position.getValue(Cesium.JulianDate.now()), + * entities[1].position.getValue(Cesium.JulianDate.now()) + * ]; + * var promise = viewer.scene.clampToHeightMostDetailed(cartesians); + * promise.then(function(updatedCartesians) { + * entities[0].position = updatedCartesians[0]; + * entities[1].position = updatedCartesians[1]; + * } + * + * @see Scene#clampToHeight + * + * @exception {DeveloperError} clampToHeightMostDetailed is only supported in 3D mode. + * @exception {DeveloperError} clampToHeightMostDetailed requires depth texture support. Check clampToHeightSupported. + */ + Scene.prototype.clampToHeightMostDetailed = function(cartesians, objectsToExclude) { + //>>includeStart('debug', pragmas.debug); + Check.defined('cartesians', cartesians); + if (this._mode !== SceneMode.SCENE3D) { + throw new DeveloperError('clampToHeightMostDetailed is only supported in 3D mode.'); + } + if (!this.clampToHeightSupported) { + throw new DeveloperError('clampToHeightMostDetailed requires depth texture support. Check clampToHeightSupported.'); + } + //>>includeEnd('debug'); + objectsToExclude = defined(objectsToExclude) ? objectsToExclude.slice() : objectsToExclude; + var length = cartesians.length; + var promises = new Array(length); + for (var i = 0; i < length; ++i) { + promises[i] = clampToHeightMostDetailed(this, cartesians[i], objectsToExclude, cartesians[i]); + } + return when.all(promises).then(function(clampedCartesians) { + var length = clampedCartesians.length; + for (var i = 0; i < length; ++i) { + cartesians[i] = clampedCartesians[i]; + } + return cartesians; + }); + }; + /** * Transforms a position in cartesian coordinates to canvas coordinates. This is commonly used to place an * HTML element at the same screen position as an object in the scene. diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/0_0_0.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/0_0_0.b3dm new file mode 100644 index 000000000000..00277d8462cb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/0_0_0.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_0.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_0.b3dm new file mode 100644 index 000000000000..3514e4cd2a2a Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_0.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_1.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_1.b3dm new file mode 100644 index 000000000000..99b4ac8569fa Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_1.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_2.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_2.b3dm new file mode 100644 index 000000000000..ec337a97d138 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_0_2.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_0.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_0.b3dm new file mode 100644 index 000000000000..205dbd0eee18 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_0.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_1.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_1.b3dm new file mode 100644 index 000000000000..f20b7dd20fd2 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_1.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_2.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_2.b3dm new file mode 100644 index 000000000000..e32032e27e1d Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_1_2.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_0.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_0.b3dm new file mode 100644 index 000000000000..8779e2254cfb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_0.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_1.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_1.b3dm new file mode 100644 index 000000000000..fc503c3b3a9f Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_1.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_2.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_2.b3dm new file mode 100644 index 000000000000..1775fc0f1d4b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/1_2_2.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_3.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_3.b3dm new file mode 100644 index 000000000000..b93e84028c3f Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_3.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_4.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_4.b3dm new file mode 100644 index 000000000000..2d5bc41455a0 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_4.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_5.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_5.b3dm new file mode 100644 index 000000000000..2e32e9129cc8 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_3_5.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_3.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_3.b3dm new file mode 100644 index 000000000000..31e75dfb7b04 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_3.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_4.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_4.b3dm new file mode 100644 index 000000000000..89cb54de483a Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_4.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_5.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_5.b3dm new file mode 100644 index 000000000000..b5f7696a731c Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_4_5.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_3.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_3.b3dm new file mode 100644 index 000000000000..d73f354f9b03 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_3.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_4.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_4.b3dm new file mode 100644 index 000000000000..66984df1e913 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_4.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_5.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_5.b3dm new file mode 100644 index 000000000000..4c970cc081da Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/2_5_5.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset.json new file mode 100644 index 000000000000..1cd90ab10b46 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset.json @@ -0,0 +1,77 @@ +{ + "asset": { + "version": "1.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 8 + }, + "Longitude": { + "minimum": -1.3197141326496755, + "maximum": -1.3196686224299798 + }, + "Latitude": { + "minimum": 0.6988476848333334, + "maximum": 0.6988827717222222 + }, + "Height": { + "minimum": 2.4691358024691357, + "maximum": 22.22222222222222 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 22.22222222222222 + ] + }, + "geometricError": 120, + "content": { + "uri": "0_0_0.b3dm" + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 22.22222222222222 + ] + }, + "geometricError": 120, + "content": { + "uri": "tileset2.json" + } + } + ], + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "REPLACE" + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset2.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset2.json new file mode 100644 index 000000000000..b6560b4bd5ae --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset2.json @@ -0,0 +1,311 @@ +{ + "asset": { + "version": "1.0" + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 22.22222222222222 + ] + }, + "geometricError": 120, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.319686826529935, + 0.6988687369666666, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_0_0.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988687369666666, + -1.319686826529935, + 0.6988792630333333, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_0_1.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988792630333334, + -1.319686826529935, + 0.6988897891, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_0_2.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.319686826529935, + 0.6988582109, + -1.3196731734700649, + 0.6988687369666666, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_1_0.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.319686826529935, + 0.6988687369666666, + -1.3196731734700649, + 0.6988792630333333, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_1_1.b3dm" + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.319686826529935, + 0.6988687369666666, + -1.3196822755099784, + 0.6988722456555555, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_3_3.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.319686826529935, + 0.6988722456555555, + -1.3196822755099784, + 0.6988757543444444, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_3_4.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.319686826529935, + 0.6988757543444445, + -1.3196822755099784, + 0.6988792630333334, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_3_5.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196822755099784, + 0.6988687369666666, + -1.3196777244900217, + 0.6988722456555555, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_4_3.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196822755099784, + 0.6988722456555555, + -1.3196777244900217, + 0.6988757543444444, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_4_4.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196822755099784, + 0.6988757543444445, + -1.3196777244900217, + 0.6988792630333334, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_4_5.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196777244900215, + 0.6988687369666666, + -1.3196731734700649, + 0.6988722456555555, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_5_3.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196777244900215, + 0.6988722456555555, + -1.3196731734700649, + 0.6988757543444444, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_5_4.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196777244900215, + 0.6988757543444445, + -1.3196731734700649, + 0.6988792630333334, + 0, + 2.4691358024691357 + ] + }, + "geometricError": 0, + "content": { + "uri": "2_5_5.b3dm" + } + } + ] + }, + { + "boundingVolume": { + "region": [ + -1.319686826529935, + 0.6988792630333334, + -1.3196731734700649, + 0.6988897891, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_1_2.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196731734700649, + 0.6988582109, + -1.3196595204101946, + 0.6988687369666666, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_2_0.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196731734700649, + 0.6988687369666666, + -1.3196595204101946, + 0.6988792630333333, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_2_1.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3196731734700649, + 0.6988792630333334, + -1.3196595204101946, + 0.6988897891, + 0, + 7.407407407407408 + ] + }, + "geometricError": 60, + "content": { + "uri": "1_2_2.b3dm" + } + } + ] + } +} diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 1db93a2124ca..6876b86da7f5 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -2,6 +2,7 @@ defineSuite([ 'Scene/Cesium3DTileset', 'Core/Cartesian2', 'Core/Cartesian3', + 'Core/Cartographic', 'Core/Color', 'Core/defined', 'Core/CullingVolume', @@ -14,6 +15,7 @@ defineSuite([ 'Core/Matrix4', 'Core/PerspectiveFrustum', 'Core/PrimitiveType', + 'Core/Ray', 'Core/RequestScheduler', 'Core/Resource', 'Core/Transforms', @@ -35,6 +37,7 @@ defineSuite([ Cesium3DTileset, Cartesian2, Cartesian3, + Cartographic, Color, defined, CullingVolume, @@ -47,6 +50,7 @@ defineSuite([ Matrix4, PerspectiveFrustum, PrimitiveType, + Ray, RequestScheduler, Resource, Transforms, @@ -66,6 +70,10 @@ defineSuite([ when) { 'use strict'; + // It's not easily possible to mock the asynchronous pick functions + // so don't run those tests when using the WebGL stub + var webglStub = !!window.webglStub; + var scene; var centerLongitude = -1.31968; var centerLatitude = 0.698874; @@ -76,6 +84,9 @@ defineSuite([ // Parent tile with no content and four child tiles with content var tilesetEmptyRootUrl = 'Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/tileset.json'; + // Tileset with 3 levels of uniform subdivision + var tilesetUniform = 'Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset.json'; + var tilesetReplacement1Url = 'Data/Cesium3DTiles/Tilesets/TilesetReplacement1/tileset.json'; var tilesetReplacement2Url = 'Data/Cesium3DTiles/Tilesets/TilesetReplacement2/tileset.json'; var tilesetReplacement3Url = 'Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset.json'; @@ -3297,4 +3308,185 @@ defineSuite([ expect(offsetMatrix).toEqualEpsilon(boundingSphereEastNorthUp, CesiumMath.EPSILON3); }); }); + + function sampleHeightMostDetailed(cartographics, objectsToExclude) { + var result; + var completed = false; + scene.sampleHeightMostDetailed(cartographics, objectsToExclude).then(function(pickResult) { + result = pickResult; + completed = true; + }); + return pollToPromise(function() { + // Scene requires manual updates in the tests to move along the promise + scene.render(); + return completed; + }).then(function() { + return result; + }); + } + + function drillPickFromRayMostDetailed(ray, limit, objectsToExclude) { + var result; + var completed = false; + scene.drillPickFromRayMostDetailed(ray, limit, objectsToExclude).then(function(pickResult) { + result = pickResult; + completed = true; + }); + return pollToPromise(function() { + // Scene requires manual updates in the tests to move along the promise + scene.render(); + return completed; + }).then(function() { + return result; + }); + } + + describe('most detailed height queries', function() { + it('tileset is offscreen', function() { + if (webglStub) { + return; + } + + viewNothing(); + + // Tileset uses replacement refinement so only one tile should be loaded and selected during most detailed picking + var centerCartographic = new Cartographic(-1.3196799798348215, 0.6988740001506679, 2.4683731133709323); + var cartographics = [centerCartographic]; + + return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(function(tileset) { + return sampleHeightMostDetailed(cartographics).then(function() { + expect(centerCartographic.height).toEqualEpsilon(2.47, CesiumMath.EPSILON1); + var statisticsAsync = tileset._statisticsLastAsync; + var statisticsRender = tileset._statisticsLastRender; + expect(statisticsAsync.numberOfCommands).toBe(1); + expect(statisticsAsync.numberOfTilesWithContentReady).toBe(1); + expect(statisticsAsync.selected).toBe(1); + expect(statisticsAsync.visited).toBeGreaterThan(1); + expect(statisticsAsync.numberOfTilesTotal).toBe(21); + expect(statisticsRender.selected).toBe(0); + }); + }); + }); + + it('tileset is onscreen', function() { + if (webglStub) { + return; + } + + viewAllTiles(); + + // Tileset uses replacement refinement so only one tile should be loaded and selected during most detailed picking + var centerCartographic = new Cartographic(-1.3196799798348215, 0.6988740001506679, 2.4683731133709323); + var cartographics = [centerCartographic]; + + return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(function(tileset) { + return sampleHeightMostDetailed(cartographics).then(function() { + expect(centerCartographic.height).toEqualEpsilon(2.47, CesiumMath.EPSILON1); + var statisticsAsync = tileset._statisticsLastAsync; + var statisticsRender = tileset._statisticsLastRender; + expect(statisticsAsync.numberOfCommands).toBe(1); + expect(statisticsAsync.numberOfTilesWithContentReady).toBeGreaterThan(1); + expect(statisticsAsync.selected).toBe(1); + expect(statisticsAsync.visited).toBeGreaterThan(1); + expect(statisticsAsync.numberOfTilesTotal).toBe(21); + expect(statisticsRender.selected).toBeGreaterThan(0); + }); + }); + }); + + it('tileset uses additive refinement', function() { + if (webglStub) { + return; + } + + viewNothing(); + + var originalLoadJson = Cesium3DTileset.loadJson; + spyOn(Cesium3DTileset, 'loadJson').and.callFake(function(tilesetUrl) { + return originalLoadJson(tilesetUrl).then(function(tilesetJson) { + tilesetJson.root.refine = 'ADD'; + return tilesetJson; + }); + }); + + var offcenterCartographic = new Cartographic(-1.3196754112739246, 0.6988705057695633, 2.467395745774971); + var cartographics = [offcenterCartographic]; + + return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(function(tileset) { + return sampleHeightMostDetailed(cartographics).then(function() { + var statistics = tileset._statisticsLastAsync; + expect(offcenterCartographic.height).toEqualEpsilon(7.407, CesiumMath.EPSILON1); + expect(statistics.numberOfCommands).toBe(3); // One for each level of the tree + expect(statistics.numberOfTilesWithContentReady).toBeGreaterThanOrEqualTo(3); + expect(statistics.selected).toBe(3); + expect(statistics.visited).toBeGreaterThan(3); + expect(statistics.numberOfTilesTotal).toBe(21); + }); + }); + }); + + it('drill picks multiple features when tileset uses additive refinement', function() { + if (webglStub) { + return; + } + + viewNothing(); + var ray = new Ray(scene.camera.positionWC, scene.camera.directionWC); + + var originalLoadJson = Cesium3DTileset.loadJson; + spyOn(Cesium3DTileset, 'loadJson').and.callFake(function(tilesetUrl) { + return originalLoadJson(tilesetUrl).then(function(tilesetJson) { + tilesetJson.root.refine = 'ADD'; + return tilesetJson; + }); + }); + + return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(function(tileset) { + return drillPickFromRayMostDetailed(ray).then(function(results) { + expect(results.length).toBe(3); + expect(results[0].object.content.url.indexOf('0_0_0.b3dm') > -1).toBe(true); + expect(results[1].object.content.url.indexOf('1_1_1.b3dm') > -1).toBe(true); + expect(results[2].object.content.url.indexOf('2_4_4.b3dm') > -1).toBe(true); + console.log(results); + }); + }); + }); + + it('works when tileset cache is disabled', function() { + if (webglStub) { + return; + } + viewNothing(); + var centerCartographic = new Cartographic(-1.3196799798348215, 0.6988740001506679, 2.4683731133709323); + var cartographics = [centerCartographic]; + return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(function(tileset) { + tileset.maximumMemoryUsage = 0; + return sampleHeightMostDetailed(cartographics).then(function() { + expect(centerCartographic.height).toEqualEpsilon(2.47, CesiumMath.EPSILON1); + }); + }); + }); + + it('multiple samples', function() { + if (webglStub) { + return; + } + + viewNothing(); + + var centerCartographic = new Cartographic(-1.3196799798348215, 0.6988740001506679); + var offcenterCartographic = new Cartographic(-1.3196754112739246, 0.6988705057695633); + var missCartographic = new Cartographic(-1.3196096042084076, 0.6988703290845706); + var cartographics = [centerCartographic, offcenterCartographic, missCartographic]; + + return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(function(tileset) { + return sampleHeightMostDetailed(cartographics).then(function() { + expect(centerCartographic.height).toEqualEpsilon(2.47, CesiumMath.EPSILON1); + expect(offcenterCartographic.height).toEqualEpsilon(2.47, CesiumMath.EPSILON1); + expect(missCartographic.height).toBeUndefined(); + expect(tileset._statisticsLastAsync.numberOfTilesWithContentReady).toBe(2); + }); + }); + }); + }); }, 'WebGL'); diff --git a/Specs/Scene/PickSpec.js b/Specs/Scene/PickSpec.js index 33de07c814fd..0ee351508385 100644 --- a/Specs/Scene/PickSpec.js +++ b/Specs/Scene/PickSpec.js @@ -56,6 +56,10 @@ defineSuite([ pollToPromise) { 'use strict'; + // It's not easily possible to mock the asynchronous pick functions + // so don't run those tests when using the WebGL stub + var webglStub = !!window.webglStub; + var scene; var primitives; var camera; @@ -1153,67 +1157,1077 @@ defineSuite([ }); }); - it('calls multiple picking functions within the same frame', function() { - if (!scene.clampToHeightSupported || !scene.pickPositionSupported) { - return; - } + function pickFromRayMostDetailed(ray, objectsToExclude) { + var result; + var completed = false; + scene.pickFromRayMostDetailed(ray, objectsToExclude).then(function(pickResult) { + result = pickResult; + completed = true; + }); + return pollToPromise(function() { + // Scene requires manual updates in the tests to move along the promise + scene.render(); + return completed; + }).then(function() { + return result; + }); + } - createSmallRectangle(0.0); - var offscreenRectanglePrimitive = createRectangle(0.0, offscreenRectangle); - offscreenRectanglePrimitive.appearance.material.uniforms.color = new Color(1.0, 0.0, 0.0, 1.0); + function drillPickFromRayMostDetailed(ray, limit, objectsToExclude) { + var result; + var completed = false; + scene.drillPickFromRayMostDetailed(ray, limit, objectsToExclude).then(function(pickResult) { + result = pickResult; + completed = true; + }); + return pollToPromise(function() { + // Scene requires manual updates in the tests to move along the promise + scene.render(); + return completed; + }).then(function() { + return result; + }); + } - scene.camera.setView({ destination : offscreenRectangle }); + function sampleHeightMostDetailed(cartographics, objectsToExclude) { + var result; + var completed = false; + scene.sampleHeightMostDetailed(cartographics, objectsToExclude).then(function(pickResult) { + result = pickResult; + completed = true; + }); + return pollToPromise(function() { + // Scene requires manual updates in the tests to move along the promise + scene.render(); + return completed; + }).then(function() { + return result; + }); + } - // Call render. Lays down depth for the pickPosition call - scene.renderForSpecs(); + function clampToHeightMostDetailed(cartesians, objectsToExclude) { + var result; + var completed = false; + scene.clampToHeightMostDetailed(cartesians, objectsToExclude).then(function(pickResult) { + result = pickResult; + completed = true; + }); + return pollToPromise(function() { + // Scene requires manual updates in the tests to move along the promise + scene.render(); + return completed; + }).then(function() { + return result; + }); + } - // Call clampToHeight - var cartesian = Cartesian3.fromRadians(0.0, 0.0, 100000.0); - expect(scene).toClampToHeightAndCall(function(cartesian) { - var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); - expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); - }, cartesian); + describe('pickFromRayMostDetailed', function() { + it('picks a tileset', function() { + if (webglStub) { + return; + } + scene.camera.setView({ destination : offscreenRectangle }); + return createTileset().then(function(tileset) { + return pickFromRayMostDetailed(primitiveRay).then(function(result) { + var primitive = result.object.primitive; + var position = result.position; + + expect(primitive).toBe(tileset); + + if (scene.context.depthTexture) { + var minimumHeight = Cartesian3.fromRadians(0.0, 0.0).x; + var maximumHeight = minimumHeight + 20.0; // Rough height of tile + expect(position.x).toBeGreaterThan(minimumHeight); + expect(position.x).toBeLessThan(maximumHeight); + expect(position.y).toEqualEpsilon(0.0, CesiumMath.EPSILON5); + expect(position.z).toEqualEpsilon(0.0, CesiumMath.EPSILON5); + } + }); + }); + }); - // Call pickPosition - expect(scene).toPickPositionAndCall(function(cartesian) { - var expectedCartesian = Cartographic.toCartesian(Rectangle.center(offscreenRectangle)); - expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + it('excludes tileset in objectsToExclude list', function() { + if (webglStub) { + return; + } + scene.camera.setView({ destination : offscreenRectangle }); + return createTileset().then(function(tileset) { + var objectsToExclude = [tileset]; + return pickFromRayMostDetailed(primitiveRay, objectsToExclude).then(function(result) { + expect(result).toBeUndefined(); + }); + }); }); - // Call clampToHeight again - expect(scene).toClampToHeightAndCall(function(cartesian) { - var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); - expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); - }, cartesian); + it('excludes tileset whose show is false', function() { + if (webglStub) { + return; + } + scene.camera.setView({ destination : offscreenRectangle }); + return createTileset().then(function(tileset) { + tileset.show = false; + return pickFromRayMostDetailed(primitiveRay).then(function(result) { + expect(result).toBeUndefined(); + }); + }); + }); - // Call pick - expect(scene).toPickPrimitive(offscreenRectanglePrimitive); + it('picks a primitive', function() { + if (webglStub) { + return; + } + var rectangle = createSmallRectangle(0.0); + scene.camera.setView({ destination : offscreenRectangle }); + return pickFromRayMostDetailed(primitiveRay).then(function(result) { + var primitive = result.object.primitive; + var position = result.position; - // Call clampToHeight again - expect(scene).toClampToHeightAndCall(function(cartesian) { - var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); - expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); - }, cartesian); + expect(primitive).toBe(rectangle); - // Call pickPosition on translucent primitive and returns undefined - offscreenRectanglePrimitive.appearance.material.uniforms.color = new Color(1.0, 0.0, 0.0, 0.5); - scene.renderForSpecs(); - expect(scene).toPickPositionAndCall(function(cartesian) { - expect(cartesian).toBeUndefined(); + if (scene.context.depthTexture) { + var expectedPosition = Cartesian3.fromRadians(0.0, 0.0); + expect(position).toEqualEpsilon(expectedPosition, CesiumMath.EPSILON5); + } + }); }); - // Call clampToHeight again - expect(scene).toClampToHeightAndCall(function(cartesian) { - var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); - expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); - }, cartesian); + it('returns undefined if no primitives are picked', function() { + if (webglStub) { + return; + } + createLargeRectangle(0.0); + scene.camera.setView({ destination : offscreenRectangle }); + return pickFromRayMostDetailed(offscreenRay).then(function(result) { + expect(result).toBeUndefined(); + }); + }); - // Call pickPosition on translucent primitive with pickTranslucentDepth - scene.pickTranslucentDepth = true; - scene.renderForSpecs(); - expect(scene).toPickPositionAndCall(function(cartesian) { - var expectedCartesian = Cartographic.toCartesian(Rectangle.center(offscreenRectangle)); - expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + it('picks the top primitive', function() { + if (webglStub) { + return; + } + createLargeRectangle(0.0); + var rectangle2 = createLargeRectangle(1.0); + scene.camera.setView({ destination : offscreenRectangle }); + return pickFromRayMostDetailed(primitiveRay).then(function(result) { + expect(result.object.primitive).toBe(rectangle2); + }); + }); + + it('excludes objects', function() { + if (webglStub) { + return; + } + var rectangle1 = createLargeRectangle(0.0); + var rectangle2 = createLargeRectangle(1.0); + var rectangle3 = createLargeRectangle(2.0); + var rectangle4 = createLargeRectangle(3.0); + rectangle4.show = false; + + scene.camera.setView({ destination : offscreenRectangle }); + return pickFromRayMostDetailed(primitiveRay, [rectangle2, rectangle3, rectangle4]).then(function(result) { + expect(result.object.primitive).toBe(rectangle1); + }).then(function() { + return pickFromRayMostDetailed(primitiveRay).then(function(result) { + expect(result.object.primitive).toBe(rectangle3); + }); + }); + }); + + it('picks primitive that doesn\'t write depth', function() { + if (webglStub) { + return; + } + var collection = scene.primitives.add(new PointPrimitiveCollection()); + var point = collection.add({ + position : Cartographic.fromRadians(0.0, 0.0, 100.0), + disableDepthTestDistance : Number.POSITIVE_INFINITY + }); + + scene.camera.setView({ destination : offscreenRectangle }); + return pickFromRayMostDetailed(primitiveRay).then(function(result) { + expect(result.object.primitive).toBe(point); + expect(result.position).toBeUndefined(); + }); + }); + + it('throws if ray is undefined', function() { + expect(function() { + scene.pickFromRayMostDetailed(undefined); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in 2D', function() { + scene.morphTo2D(0.0); + expect(function() { + scene.pickFromRayMostDetailed(undefined); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in CV', function() { + scene.morphToColumbusView(0.0); + expect(function() { + scene.pickFromRayMostDetailed(undefined); + }).toThrowDeveloperError(); + }); + }); + + describe('drillPickFromRayMostDetailed', function() { + it('drill picks a primitive', function() { + if (webglStub) { + return; + } + var rectangle = createSmallRectangle(0.0); + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay).then(function(results) { + expect(results.length).toBe(1); + + var primitive = results[0].object.primitive; + var position = results[0].position; + + expect(primitive).toBe(rectangle); + + if (scene.context.depthTexture) { + var expectedPosition = Cartesian3.fromRadians(0.0, 0.0); + expect(position).toEqualEpsilon(expectedPosition, CesiumMath.EPSILON5); + } else { + expect(position).toBeUndefined(); + } + }); + }); + + it('drill picks multiple primitives', function() { + if (webglStub) { + return; + } + var rectangle1 = createSmallRectangle(0.0); + var rectangle2 = createSmallRectangle(1.0); + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay).then(function(results) { + expect(results.length).toBe(2); + + // rectangle2 is picked before rectangle1 + expect(results[0].object.primitive).toBe(rectangle2); + expect(results[1].object.primitive).toBe(rectangle1); + + if (scene.context.depthTexture) { + var rectangleCenter1 = Cartesian3.fromRadians(0.0, 0.0, 0.0); + var rectangleCenter2 = Cartesian3.fromRadians(0.0, 0.0, 1.0); + expect(results[0].position).toEqualEpsilon(rectangleCenter2, CesiumMath.EPSILON5); + expect(results[1].position).toEqualEpsilon(rectangleCenter1, CesiumMath.EPSILON5); + } else { + expect(results[0].position).toBeUndefined(); + expect(results[1].position).toBeUndefined(); + } + }); + }); + + it('does not drill pick when show is false', function() { + if (webglStub) { + return; + } + var rectangle1 = createLargeRectangle(0.0); + var rectangle2 = createLargeRectangle(1.0); + rectangle2.show = false; + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay).then(function(results) { + expect(results.length).toEqual(1); + expect(results[0].object.primitive).toEqual(rectangle1); + }); + }); + + it('does not drill pick when alpha is zero', function() { + if (webglStub) { + return; + } + var rectangle1 = createLargeRectangle(0.0); + var rectangle2 = createLargeRectangle(1.0); + rectangle2.appearance.material.uniforms.color.alpha = 0.0; + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay).then(function(results) { + expect(results.length).toEqual(1); + expect(results[0].object.primitive).toEqual(rectangle1); + }); + }); + + it('returns empty array if no primitives are picked', function() { + if (webglStub) { + return; + } + createLargeRectangle(0.0); + createLargeRectangle(1.0); + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(offscreenRay).then(function(results) { + expect(results.length).toEqual(0); + }); + }); + + it('can drill pick batched Primitives with show attribute', function() { + if (webglStub) { + return; + } + var geometry = new RectangleGeometry({ + rectangle : Rectangle.fromDegrees(-50.0, -50.0, 50.0, 50.0), + granularity : CesiumMath.toRadians(20.0), + vertexFormat : EllipsoidSurfaceAppearance.VERTEX_FORMAT, + height : 0.0 + }); + + var geometryWithHeight = new RectangleGeometry({ + rectangle : Rectangle.fromDegrees(-50.0, -50.0, 50.0, 50.0), + granularity : CesiumMath.toRadians(20.0), + vertexFormat : EllipsoidSurfaceAppearance.VERTEX_FORMAT, + height : 1.0 + }); + + var instance1 = new GeometryInstance({ + id : 1, + geometry : geometry, + attributes : { + show : new ShowGeometryInstanceAttribute(true) + } + }); + + var instance2 = new GeometryInstance({ + id : 2, + geometry : geometry, + attributes : { + show : new ShowGeometryInstanceAttribute(false) + } + }); + + var instance3 = new GeometryInstance({ + id : 3, + geometry : geometryWithHeight, + attributes : { + show : new ShowGeometryInstanceAttribute(true) + } + }); + + var primitive = primitives.add(new Primitive({ + geometryInstances : [instance1, instance2, instance3], + asynchronous : false, + appearance : new EllipsoidSurfaceAppearance() + })); + + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay).then(function(results) { + expect(results.length).toEqual(2); + expect(results[0].object.primitive).toEqual(primitive); + expect(results[0].object.id).toEqual(3); + expect(results[1].object.primitive).toEqual(primitive); + expect(results[1].object.id).toEqual(1); + }); + }); + + it('can drill pick without ID', function() { + if (webglStub) { + return; + } + var geometry = new RectangleGeometry({ + rectangle : Rectangle.fromDegrees(-50.0, -50.0, 50.0, 50.0), + granularity : CesiumMath.toRadians(20.0), + vertexFormat : EllipsoidSurfaceAppearance.VERTEX_FORMAT + }); + + var instance1 = new GeometryInstance({ + geometry : geometry, + attributes : { + show : new ShowGeometryInstanceAttribute(true) + } + }); + + var instance2 = new GeometryInstance({ + geometry : geometry, + attributes : { + show : new ShowGeometryInstanceAttribute(true) + } + }); + + var primitive = primitives.add(new Primitive({ + geometryInstances : [instance1, instance2], + asynchronous : false, + appearance : new EllipsoidSurfaceAppearance() + })); + + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay).then(function(results) { + expect(results.length).toEqual(1); + expect(results[0].object.primitive).toEqual(primitive); + }); + }); + + it('can drill pick batched Primitives without show attribute', function() { + if (webglStub) { + return; + } + var geometry = new RectangleGeometry({ + rectangle : Rectangle.fromDegrees(-50.0, -50.0, 50.0, 50.0), + granularity : CesiumMath.toRadians(20.0), + vertexFormat : EllipsoidSurfaceAppearance.VERTEX_FORMAT, + height : 0.0 + }); + + var geometryWithHeight = new RectangleGeometry({ + rectangle : Rectangle.fromDegrees(-50.0, -50.0, 50.0, 50.0), + granularity : CesiumMath.toRadians(20.0), + vertexFormat : EllipsoidSurfaceAppearance.VERTEX_FORMAT, + height : 1.0 + }); + + var instance1 = new GeometryInstance({ + id : 1, + geometry : geometry + }); + + var instance2 = new GeometryInstance({ + id : 2, + geometry : geometry + }); + + var instance3 = new GeometryInstance({ + id : 3, + geometry : geometryWithHeight + }); + + var primitive = primitives.add(new Primitive({ + geometryInstances : [instance1, instance2, instance3], + asynchronous : false, + appearance : new EllipsoidSurfaceAppearance() + })); + + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay).then(function(results) { + expect(results.length).toEqual(1); + expect(results[0].object.primitive).toEqual(primitive); + expect(results[0].object.id).toEqual(3); + }); + }); + + it('stops drill picking when the limit is reached.', function() { + if (webglStub) { + return; + } + createLargeRectangle(0.0); + var rectangle2 = createLargeRectangle(1.0); + var rectangle3 = createLargeRectangle(2.0); + var rectangle4 = createLargeRectangle(3.0); + + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay, 3).then(function(results) { + expect(results.length).toEqual(3); + expect(results[0].object.primitive).toEqual(rectangle4); + expect(results[1].object.primitive).toEqual(rectangle3); + expect(results[2].object.primitive).toEqual(rectangle2); + }); + }); + + it('excludes objects', function() { + if (webglStub) { + return; + } + createLargeRectangle(0.0); + var rectangle2 = createLargeRectangle(1.0); + var rectangle3 = createLargeRectangle(2.0); + var rectangle4 = createLargeRectangle(3.0); + var rectangle5 = createLargeRectangle(4.0); + scene.camera.setView({ destination : offscreenRectangle }); + return drillPickFromRayMostDetailed(primitiveRay, 2, [rectangle5, rectangle3]).then(function(results) { + expect(results.length).toBe(2); + expect(results[0].object.primitive).toBe(rectangle4); + expect(results[1].object.primitive).toBe(rectangle2); + }); + }); + + it('throws if ray is undefined', function() { + expect(function() { + scene.drillPickFromRayMostDetailed(undefined); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in 2D', function() { + scene.morphTo2D(0.0); + expect(function() { + scene.drillPickFromRayMostDetailed(primitiveRay); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in CV', function() { + scene.morphToColumbusView(0.0); + expect(function() { + scene.drillPickFromRayMostDetailed(primitiveRay); + }).toThrowDeveloperError(); + }); + }); + + describe('sampleHeightMostDetailed', function() { + it('samples height from tileset', function() { + if (!scene.sampleHeightSupported) { + return; + } + + var cartographics = [new Cartographic(0.0, 0.0)]; + return createTileset().then(function() { + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + var height = updatedCartographics[0].height; + expect(height).toBeGreaterThan(0.0); + expect(height).toBeLessThan(20.0); // Rough height of tile + }); + }); + }); + + it('samples height from the globe', function() { + if (!scene.sampleHeightSupported) { + return; + } + + var cartographics = [ + new Cartographic(0.0, 0.0), + new Cartographic(0.0001, 0.0001), + new Cartographic(0.0002, 0.0002) + ]; + var clonedCartographics = [new Cartographic(0.0, 0.0), new Cartographic(0.0001, 0.0001), new Cartographic(0.0002, 0.0002)]; + return createGlobe().then(function() { + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics).toBe(cartographics); + expect(updatedCartographics.length).toBe(3); + var previousHeight; + for (var i = 0; i < 3; ++i) { + var longitude = updatedCartographics[i].longitude; + var latitude = updatedCartographics[i].latitude; + var height = updatedCartographics[i].height; + expect(longitude).toBe(clonedCartographics[i].longitude); + expect(latitude).toBe(clonedCartographics[i].latitude); + expect(height).toBeDefined(); + expect(height).not.toBe(previousHeight); + previousHeight = height; + } + }); + }); + }); + + it('does not sample offscreen globe tiles', function() { + if (!scene.sampleHeightSupported) { + return; + } + + var cartographics = [new Cartographic(0.0, 0.0)]; + scene.camera.setView({ destination : offscreenRectangle }); + return createGlobe().then(function() { + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics[0].height).toBeUndefined(); + }); + }); + }); + + it('samples height from multiple primitives', function() { + if (!scene.sampleHeightSupported) { + return; + } + + createRectangle(0.0, smallRectangle); + createRectangle(0.0, offscreenRectangle); + + var cartographics = [ + Rectangle.center(smallRectangle), + Rectangle.center(offscreenRectangle), + new Cartographic(-2.0, -2.0) + ]; + scene.camera.setView({ destination : offscreenRectangle }); + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics[0].height).toBeDefined(); + expect(updatedCartographics[1].height).toBeDefined(); + expect(updatedCartographics[2].height).toBeUndefined(); // No primitive occupies this space + }); + }); + + it('samples multiple heights from primitive', function() { + if (!scene.sampleHeightSupported) { + return; + } + + createSmallRectangle(0.0); + var cartographics = [ + new Cartographic(0.0, 0.0), + new Cartographic(-0.000001, -0.000001), + new Cartographic(0.0000005, 0.0000005) + ]; + scene.camera.setView({ destination : offscreenRectangle }); + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + var previousHeight; + for (var i = 0; i < 3; ++i) { + var height = updatedCartographics[i].height; + expect(height).toEqualEpsilon(0.0, CesiumMath.EPSILON3); + expect(height).not.toBe(previousHeight); + previousHeight = height; + } + }); + }); + + it('samples height from the top primitive', function() { + if (!scene.sampleHeightSupported) { + return; + } + createSmallRectangle(0.0); + createSmallRectangle(1.0); + var cartographics = [new Cartographic(0.0, 0.0)]; + scene.camera.setView({ destination : offscreenRectangle }); + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics[0].height).toEqualEpsilon(1.0, CesiumMath.EPSILON3); + }); + }); + + it('excludes objects', function() { + if (!scene.sampleHeightSupported) { + return; + } + + var rectangle1 = createRectangle(0.0, smallRectangle); + createRectangle(0.0, offscreenRectangle); + var rectangle3 = createRectangle(1.0, offscreenRectangle); + + var cartographics = [ + Rectangle.center(smallRectangle), + Rectangle.center(offscreenRectangle), + new Cartographic(-2.0, -2.0) + ]; + scene.camera.setView({ destination : offscreenRectangle }); + return sampleHeightMostDetailed(cartographics, [rectangle1, rectangle3]).then(function(updatedCartographics) { + expect(updatedCartographics[0].height).toBeUndefined(); // This rectangle was excluded + expect(updatedCartographics[1].height).toEqualEpsilon(0.0, CesiumMath.EPSILON2); + expect(updatedCartographics[2].height).toBeUndefined(); // No primitive occupies this space + }); + }); + + it('excludes primitive that doesn\'t write depth', function() { + if (!scene.sampleHeightSupported) { + return; + } + + var rectangle = createSmallRectangle(0.0); + + var height = 100.0; + var cartographics = [new Cartographic(0.0, 0.0, height)]; + var collection = scene.primitives.add(new PointPrimitiveCollection()); + var point = collection.add({ + position : Cartographic.toCartesian(cartographics[0]) + }); + + scene.camera.setView({ destination : offscreenRectangle }); + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics[0].height).toEqualEpsilon(height, CesiumMath.EPSILON3); + }).then(function() { + point.disableDepthTestDistance = Number.POSITIVE_INFINITY; + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics[0].height).toEqualEpsilon(0.0, CesiumMath.EPSILON3); + }); + }).then(function() { + rectangle.show = false; + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics[0].height).toBeUndefined(); + }); + }); + }); + + it('handles empty array', function() { + if (!scene.sampleHeightSupported) { + return; + } + + var cartographics = []; + return sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + expect(updatedCartographics.length).toBe(0); + }); + }); + + it('throws if positions is undefined', function() { + if (!scene.sampleHeightSupported) { + return; + } + + expect(function() { + scene.sampleHeightMostDetailed(undefined); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in 2D', function() { + if (!scene.sampleHeightSupported) { + return; + } + + scene.morphTo2D(0.0); + var cartographics = [new Cartographic(0.0, 0.0)]; + expect(function() { + scene.sampleHeightMostDetailed(cartographics); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in CV', function() { + if (!scene.sampleHeightSupported) { + return; + } + + scene.morphToColumbusView(0.0); + var cartographics = [new Cartographic(0.0, 0.0)]; + expect(function() { + scene.sampleHeightMostDetailed(cartographics); + }).toThrowDeveloperError(); + }); + + it('throws if sampleHeight is not supported', function() { + if (!scene.sampleHeightSupported) { + return; + } + // Disable extension + var depthTexture = scene.context._depthTexture; + scene.context._depthTexture = false; + + var cartographics = [new Cartographic(0.0, 0.0)]; + expect(function() { + scene.sampleHeightMostDetailed(cartographics); + }).toThrowDeveloperError(); + + // Re-enable extension + scene.context._depthTexture = depthTexture; + }); + }); + + describe('clampToHeightMostDetailed', function() { + it('clamps to tileset', function() { + if (!scene.clampToHeightSupported) { + return; + } + + var cartesians = [Cartesian3.fromRadians(0.0, 0.0, 100000.0)]; + return createTileset().then(function() { + return clampToHeightMostDetailed(cartesians).then(function(updatedCartesians) { + var minimumHeight = Cartesian3.fromRadians(0.0, 0.0).x; + var maximumHeight = minimumHeight + 20.0; // Rough height of tile + var position = updatedCartesians[0]; + expect(position.x).toBeGreaterThan(minimumHeight); + expect(position.x).toBeLessThan(maximumHeight); + expect(position.y).toEqualEpsilon(0.0, CesiumMath.EPSILON5); + expect(position.z).toEqualEpsilon(0.0, CesiumMath.EPSILON5); + }); + }); + }); + + it('clamps to the globe', function() { + if (!scene.clampToHeightSupported) { + return; + } + + var cartesians = [ + Cartesian3.fromRadians(0.0, 0.0, 100000.0), + Cartesian3.fromRadians(0.0001, 0.0001, 100000.0), + Cartesian3.fromRadians(0.0002, 0.0002, 100000.0) + ]; + var clonedCartesians = [ + Cartesian3.fromRadians(0.0, 0.0, 100000.0), + Cartesian3.fromRadians(0.0001, 0.0001, 100000.0), + Cartesian3.fromRadians(0.0002, 0.0002, 100000.0) + ]; + return createGlobe().then(function() { + return clampToHeightMostDetailed(cartesians).then(function(updatedCartesians) { + expect(updatedCartesians).toBe(cartesians); + expect(updatedCartesians.length).toBe(3); + var previousCartesian; + for (var i = 0; i < 3; ++i) { + expect(updatedCartesians[i]).not.toEqual(clonedCartesians[i]); + expect(updatedCartesians[i]).not.toEqual(previousCartesian); + previousCartesian = updatedCartesians[i]; + } + }); + }); + }); + + it('does not clamp to offscreen globe tiles', function() { + if (!scene.clampToHeightSupported) { + return; + } + + var cartesians = [Cartesian3.fromRadians(0.0, 0.0, 100000.0)]; + scene.camera.setView({ destination : offscreenRectangle }); + return createGlobe().then(function() { + return clampToHeightMostDetailed(cartesians).then(function(updatedCartesians) { + expect(updatedCartesians[0]).toBeUndefined(); + }); + }); + }); + + it('clamps to multiple primitives', function() { + if (!scene.clampToHeightSupported) { + return; + } + + createRectangle(0.0, smallRectangle); + createRectangle(0.0, offscreenRectangle); + + var cartesians = [ + Cartographic.toCartesian(Rectangle.center(smallRectangle)), + Cartographic.toCartesian(Rectangle.center(offscreenRectangle)), + Cartesian3.fromRadians(-2.0, -2.0) + ]; + scene.camera.setView({ destination : offscreenRectangle }); + return clampToHeightMostDetailed(cartesians).then(function(updatedCartesians) { + expect(updatedCartesians[0]).toBeDefined(); + expect(updatedCartesians[1]).toBeDefined(); + expect(updatedCartesians[2]).toBeUndefined(); // No primitive occupies this space + }); + }); + + it('clamps to primitive', function() { + if (!scene.clampToHeightSupported) { + return; + } + + createSmallRectangle(0.0); + var cartesians = [ + Cartesian3.fromRadians(0.0, 0.0, 100000.0), + Cartesian3.fromRadians(-0.000001, -0.000001, 100000.0), + Cartesian3.fromRadians(0.0000005, 0.0000005, 100000.0) + ]; + var expectedCartesians = [ + Cartesian3.fromRadians(0.0, 0.0, 0.0), + Cartesian3.fromRadians(-0.000001, -0.000001, 0.0), + Cartesian3.fromRadians(0.0000005, 0.0000005, 0.0) + ]; + scene.camera.setView({ destination : offscreenRectangle }); + return clampToHeightMostDetailed(cartesians).then(function(updatedCartesians) { + var previousCartesian; + for (var i = 0; i < 3; ++i) { + expect(updatedCartesians[i]).toEqualEpsilon(expectedCartesians[i], CesiumMath.EPSILON5); + expect(updatedCartesians[i]).not.toEqual(previousCartesian); + previousCartesian = updatedCartesians[i]; + } + }); + }); + + it('clamps to top primitive', function() { + if (!scene.clampToHeightSupported) { + return; + } + createSmallRectangle(0.0); + createSmallRectangle(1.0); + var cartesians = [Cartesian3.fromRadians(0.0, 0.0)]; + scene.camera.setView({ destination : offscreenRectangle }); + return clampToHeightMostDetailed(cartesians).then(function(updatedCartesians) { + var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0, 1.0); + expect(updatedCartesians[0]).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }); + }); + + it('excludes objects', function() { + if (!scene.clampToHeightSupported) { + return; + } + + var rectangle1 = createRectangle(0.0, smallRectangle); + createRectangle(0.0, offscreenRectangle); + var rectangle3 = createRectangle(1.0, offscreenRectangle); + + var cartesians = [ + Cartographic.toCartesian(Rectangle.center(smallRectangle)), + Cartographic.toCartesian(Rectangle.center(offscreenRectangle)), + Cartesian3.fromRadians(-2.0, -2.0) + ]; + scene.camera.setView({ destination : offscreenRectangle }); + return clampToHeightMostDetailed(cartesians, [rectangle1, rectangle3]).then(function(updatedCartesians) { + var expectedCartesian = Cartographic.toCartesian(Rectangle.center(offscreenRectangle)); + expect(updatedCartesians[0]).toBeUndefined(); // This rectangle was excluded + expect(updatedCartesians[1]).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON2); + expect(updatedCartesians[2]).toBeUndefined(); // No primitive occupies this space + }); + }); + + it('excludes primitive that doesn\'t write depth', function() { + if (!scene.clampToHeightSupported) { + return; + } + + var rectangle = createSmallRectangle(0.0); + + var height = 100.0; + var cartesian = Cartesian3.fromRadians(0.0, 0.0, height); + var cartesians1 = [Cartesian3.clone(cartesian)]; + var cartesians2 = [Cartesian3.clone(cartesian)]; + var cartesians3 = [Cartesian3.clone(cartesian)]; + var collection = scene.primitives.add(new PointPrimitiveCollection()); + var point = collection.add({ + position : cartesian + }); + + scene.camera.setView({ destination : offscreenRectangle }); + return clampToHeightMostDetailed(cartesians1).then(function(updatedCartesians) { + expect(updatedCartesians[0]).toEqualEpsilon(cartesian, CesiumMath.EPSILON3); + }).then(function() { + point.disableDepthTestDistance = Number.POSITIVE_INFINITY; + return clampToHeightMostDetailed(cartesians2).then(function(updatedCartesians) { + expect(updatedCartesians[0]).toEqualEpsilon(cartesian, CesiumMath.EPSILON3); + }); + }).then(function() { + rectangle.show = false; + return clampToHeightMostDetailed(cartesians3).then(function(updatedCartesians) { + expect(updatedCartesians[0]).toBeUndefined(); + }); + }); + }); + + it('handles empty array', function() { + if (!scene.clampToHeightSupported) { + return; + } + + var cartesians = []; + return sampleHeightMostDetailed(cartesians).then(function(updatedCartesians) { + expect(updatedCartesians.length).toBe(0); + }); + }); + + it('throws if cartesians is undefined', function() { + if (!scene.clampToHeightSupported) { + return; + } + + expect(function() { + scene.clampToHeightMostDetailed(undefined); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in 2D', function() { + if (!scene.clampToHeightSupported) { + return; + } + + scene.morphTo2D(0.0); + var cartesians = [Cartesian3.fromRadians(0.0, 0.0)]; + expect(function() { + scene.clampToHeightMostDetailed(cartesians); + }).toThrowDeveloperError(); + }); + + it('throws if scene camera is in CV', function() { + if (!scene.clampToHeightSupported) { + return; + } + + scene.morphToColumbusView(0.0); + var cartesians = [Cartesian3.fromRadians(0.0, 0.0)]; + expect(function() { + scene.clampToHeightMostDetailed(cartesians); + }).toThrowDeveloperError(); + }); + + it('throws if clampToHeight is not supported', function() { + if (!scene.clampToHeightSupported) { + return; + } + // Disable extension + var depthTexture = scene.context._depthTexture; + scene.context._depthTexture = false; + + var cartesians = [Cartesian3.fromRadians(0.0, 0.0)]; + expect(function() { + scene.clampToHeightMostDetailed(cartesians); + }).toThrowDeveloperError(); + + // Re-enable extension + scene.context._depthTexture = depthTexture; + }); + }); + + it('calls multiple picking functions within the same frame', function() { + if (!scene.clampToHeightSupported || !scene.pickPositionSupported) { + return; + } + + createSmallRectangle(0.0); + var offscreenRectanglePrimitive = createRectangle(0.0, offscreenRectangle); + offscreenRectanglePrimitive.appearance.material.uniforms.color = new Color(1.0, 0.0, 0.0, 1.0); + + scene.camera.setView({ destination : offscreenRectangle }); + + // Call render. Lays down depth for the pickPosition call + scene.renderForSpecs(); + + var cartographic = Cartographic.fromRadians(0.0, 0.0, 100000.0); + var cartesian = Cartographic.toCartesian(cartographic); + var cartesians = [Cartesian3.clone(cartesian)]; + var cartographics = [cartographic]; + + // Call clampToHeight + expect(scene).toClampToHeightAndCall(function(cartesian) { + var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); + expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }, cartesian); + + // Call pickPosition + expect(scene).toPickPositionAndCall(function(cartesian) { + var expectedCartesian = Cartographic.toCartesian(Rectangle.center(offscreenRectangle)); + expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }); + + // Call clampToHeight again + expect(scene).toClampToHeightAndCall(function(cartesian) { + var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); + expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }, cartesian); + + // Call pick + expect(scene).toPickPrimitive(offscreenRectanglePrimitive); + + // Call clampToHeight again + expect(scene).toClampToHeightAndCall(function(cartesian) { + var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); + expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }, cartesian); + + // Call pickPosition on translucent primitive and returns undefined + offscreenRectanglePrimitive.appearance.material.uniforms.color = new Color(1.0, 0.0, 0.0, 0.5); + scene.renderForSpecs(); + expect(scene).toPickPositionAndCall(function(cartesian) { + expect(cartesian).toBeUndefined(); + }); + + // Call clampToHeight again + expect(scene).toClampToHeightAndCall(function(cartesian) { + var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); + expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }, cartesian); + + // Call pickPosition on translucent primitive with pickTranslucentDepth + scene.pickTranslucentDepth = true; + scene.renderForSpecs(); + expect(scene).toPickPositionAndCall(function(cartesian) { + var expectedCartesian = Cartographic.toCartesian(Rectangle.center(offscreenRectangle)); + expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }); + + // Mix async and sync requests + var results = []; + var completed = 0; + scene.clampToHeightMostDetailed(cartesians).then(function(updatedCartesians) { + results.push(updatedCartesians); + completed++; + }); + scene.sampleHeightMostDetailed(cartographics).then(function(updatedCartographics) { + results.push(updatedCartographics); + completed++; + }); + + // Call clampToHeight again + expect(scene).toClampToHeightAndCall(function(cartesian) { + var expectedCartesian = Cartesian3.fromRadians(0.0, 0.0); + expect(cartesian).toEqualEpsilon(expectedCartesian, CesiumMath.EPSILON5); + }, cartesian); + + return pollToPromise(function() { + // Scene requires manual updates in the tests to move along the promise + scene.render(); + return completed === 2; + }).then(function() { + expect(results[0][0]).toBeDefined(); + expect(results[1][0].height).toBeDefined(); }); });