diff --git a/Apps/Sandcastle/gallery/development/BillboardClampToGround.html b/Apps/Sandcastle/gallery/development/BillboardClampToGround.html new file mode 100644 index 000000000000..b3db0abd654e --- /dev/null +++ b/Apps/Sandcastle/gallery/development/BillboardClampToGround.html @@ -0,0 +1,158 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+
+
+
+
+
+ + + diff --git a/CHANGES.md b/CHANGES.md index 0dc284704372..9cf5ce0688dc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Change Log ### 1.10 - 2015-06-01 * Breaking changes * +* Added `Billboard.heightReference` and `Label.heightReference` to clamp billboards and labels to terrain. * Added new `PointPrimitive` and `PointPrimitiveCollection`, which are faster and use less memory than billboards with circles. * Changed `Entity.point` back-end graphics to use the new `PointPrimitive` instead of billboards. No change to the `Entity.point` API. * Upgraded Autolinker from version 0.15.2 to 0.17.1. diff --git a/Source/Scene/Billboard.js b/Source/Scene/Billboard.js index fcfcd0ff5a71..cbfe9e34b7e9 100644 --- a/Source/Scene/Billboard.js +++ b/Source/Scene/Billboard.js @@ -4,6 +4,7 @@ define([ '../Core/Cartesian2', '../Core/Cartesian3', '../Core/Cartesian4', + '../Core/Cartographic', '../Core/Color', '../Core/createGuid', '../Core/defaultValue', @@ -12,6 +13,7 @@ define([ '../Core/DeveloperError', '../Core/Matrix4', '../Core/NearFarScalar', + './HeightReference', './HorizontalOrigin', './SceneMode', './SceneTransforms', @@ -21,6 +23,7 @@ define([ Cartesian2, Cartesian3, Cartesian4, + Cartographic, Color, createGuid, defaultValue, @@ -29,6 +32,7 @@ define([ DeveloperError, Matrix4, NearFarScalar, + HeightReference, HorizontalOrigin, SceneMode, SceneTransforms, @@ -98,6 +102,8 @@ define([ this._scaleByDistance = options.scaleByDistance; this._translucencyByDistance = options.translucencyByDistance; this._pixelOffsetScaleByDistance = options.pixelOffsetScaleByDistance; + this._heightReference = defaultValue(options.heightReference, HeightReference.NONE); + this._ownerSize = new Cartesian2(); // used by labels this._id = options.id; this._collection = defaultValue(options.collection, billboardCollection); @@ -140,6 +146,12 @@ define([ if (defined(this._billboardCollection._textureAtlas)) { this._loadImage(); } + + this._actualClampedPosition = undefined; + this._removeCallbackFunc = undefined; + this._mode = SceneMode.SCENE3D; + + this._updateClamping(); }; var SHOW_INDEX = Billboard.SHOW_INDEX = 0; @@ -156,7 +168,8 @@ define([ var SCALE_BY_DISTANCE_INDEX = Billboard.SCALE_BY_DISTANCE_INDEX = 11; var TRANSLUCENCY_BY_DISTANCE_INDEX = Billboard.TRANSLUCENCY_BY_DISTANCE_INDEX = 12; var PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX = Billboard.PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX = 13; - Billboard.NUMBER_OF_PROPERTIES = 14; + var OWNER_SIZE_INDEX = Billboard.OWNER_SIZE_INDEX = 14; + Billboard.NUMBER_OF_PROPERTIES = 15; function makeDirty(billboard, propertyChanged) { var billboardCollection = billboard._billboardCollection; @@ -212,6 +225,33 @@ define([ Cartesian3.clone(value, position); Cartesian3.clone(value, this._actualPosition); + this._updateClamping(); + makeDirty(this, POSITION_INDEX); + } + } + }, + + /** + * Gets or sets the height reference of this billboard. + * @memberof Billboard.prototype + * @type {HeightReference} + * @default HeightReference.NONE + */ + heightReference : { + get : function() { + return this._heightReference; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug) + if (!defined(value)) { + throw new DeveloperError('value is required.'); + } + //>>includeEnd('debug'); + + var heightReference = this._heightReference; + if (value !== heightReference) { + this._heightReference = value; + this._updateClamping(); makeDirty(this, POSITION_INDEX); } } @@ -747,6 +787,22 @@ define([ get : function() { return this._imageIndex !== -1; } + }, + + /** + * Keeps track of the position of the billboard based on the height reference. + * @memberof Billboard.prototype + * @type {Cartesian3} + * @private + */ + _clampedPosition : { + get : function() { + return this._actualClampedPosition; + }, + set : function(value) { + this._actualClampedPosition = Cartesian3.clone(value, this._actualClampedPosition); + makeDirty(this, POSITION_INDEX); + } } }); @@ -762,6 +818,81 @@ define([ return this._pickId; }; + Billboard.prototype._updateClamping = function() { + Billboard._updateClamping(this._billboardCollection, this); + }; + + var scratchCartographic = new Cartographic(); + var scratchPosition = new Cartesian3(); + + Billboard._updateClamping = function(collection, owner) { + var scene = collection._scene; + if (!defined(scene)) { + if (owner._heightReference !== HeightReference.NONE) { + throw new DeveloperError('Height reference is not supported.'); + } + return; + } + + var globe = scene.globe; + var ellipsoid = globe.ellipsoid; + var surface = globe._surface; + + var mode = scene.frameState.mode; + var projection = scene.frameState.mapProjection; + + var modeChanged = mode !== owner._mode; + owner._mode = mode; + + if ((owner._heightReference === HeightReference.NONE || modeChanged) && defined(owner._removeCallbackFunc)) { + owner._removeCallbackFunc(); + owner._removeCallbackFunc = undefined; + owner._clampedPosition = undefined; + } + + if (owner._heightReference === HeightReference.NONE || !defined(owner._position)) { + return; + } + + var position = ellipsoid.cartesianToCartographic(owner._position); + if (!defined(position)) { + return; + } + + if (defined(owner._removeCallbackFunc)) { + owner._removeCallbackFunc(); + } + + var updateFunction = function(clampedPosition) { + if (owner._heightReference === HeightReference.RELATIVE_TO_GROUND) { + if (owner._mode === SceneMode.SCENE3D) { + var clampedCart = ellipsoid.cartesianToCartographic(clampedPosition, scratchCartographic); + clampedCart.height += position.height; + ellipsoid.cartographicToCartesian(clampedCart, clampedPosition); + } else { + clampedPosition.x += position.height; + } + } + owner._clampedPosition = Cartesian3.clone(clampedPosition, owner._clampedPosition); + }; + + owner._removeCallbackFunc = surface.updateHeight(position, updateFunction); + + var height = globe.getHeight(position); + if (defined(height)) { + Cartographic.clone(position, scratchCartographic); + scratchCartographic.height = height; + if (owner._mode === SceneMode.SCENE3D) { + ellipsoid.cartographicToCartesian(scratchCartographic, scratchPosition); + } else { + projection.project(scratchCartographic, scratchPosition); + Cartesian3.fromElements(scratchPosition.z, scratchPosition.x, scratchPosition.y, scratchPosition); + } + + updateFunction(scratchPosition); + } + }; + Billboard.prototype._loadImage = function() { var atlas = this._billboardCollection._textureAtlas; @@ -910,18 +1041,39 @@ define([ } }; + Billboard.prototype._setOwnerSize = function(value) { + //>>includeStart('debug', pragmas.debug); + if (!defined(value)) { + throw new DeveloperError('value is required.'); + } + //>>includeEnd('debug'); + + var size = this._ownerSize; + if (!Cartesian2.equals(size, value)) { + Cartesian2.clone(value, size); + makeDirty(this, OWNER_SIZE_INDEX); + } + }; + Billboard.prototype._getActualPosition = function() { - return this._actualPosition; + return defined(this._clampedPosition) ? this._clampedPosition : this._actualPosition; }; Billboard.prototype._setActualPosition = function(value) { - Cartesian3.clone(value, this._actualPosition); + if (!(defined(this._clampedPosition))) { + Cartesian3.clone(value, this._actualPosition); + } makeDirty(this, POSITION_INDEX); }; var tempCartesian3 = new Cartesian4(); - Billboard._computeActualPosition = function(position, frameState, modelMatrix) { - if (frameState.mode === SceneMode.SCENE3D) { + Billboard._computeActualPosition = function(billboard, position, frameState, modelMatrix) { + if (defined(billboard._clampedPosition)) { + if (frameState.mode !== billboard._mode) { + billboard._updateClamping(); + } + return billboard._clampedPosition; + } else if (frameState.mode === SceneMode.SCENE3D) { return position; } @@ -1003,7 +1155,9 @@ define([ Cartesian2.add(scratchPixelOffset, this._translate, scratchPixelOffset); var modelMatrix = billboardCollection.modelMatrix; - var windowCoordinates = Billboard._computeScreenSpacePosition(modelMatrix, this._actualPosition, + var actualPosition = this._getActualPosition(); + + var windowCoordinates = Billboard._computeScreenSpacePosition(modelMatrix, actualPosition, this._eyeOffset, scratchPixelOffset, scene, result); windowCoordinates.y = scene.canvas.clientHeight - windowCoordinates.y; return windowCoordinates; @@ -1037,6 +1191,11 @@ define([ }; Billboard.prototype._destroy = function() { + if (defined(this._customData)) { + this._billboardCollection._scene.globe._surface.removeTileCustomData(this._customData); + this._customData = undefined; + } + this.image = undefined; this._pickId = this._pickId && this._pickId.destroy(); this._billboardCollection = undefined; diff --git a/Source/Scene/BillboardCollection.js b/Source/Scene/BillboardCollection.js index f210f5a09dab..e7b3a5b86f69 100644 --- a/Source/Scene/BillboardCollection.js +++ b/Source/Scene/BillboardCollection.js @@ -71,6 +71,7 @@ define([ var SCALE_BY_DISTANCE_INDEX = Billboard.SCALE_BY_DISTANCE_INDEX; var TRANSLUCENCY_BY_DISTANCE_INDEX = Billboard.TRANSLUCENCY_BY_DISTANCE_INDEX; var PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX = Billboard.PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX; + var OWNER_SIZE_INDEX = Billboard.OWNER_SIZE_INDEX; var NUMBER_OF_PROPERTIES = Billboard.NUMBER_OF_PROPERTIES; var attributeLocations = { @@ -81,7 +82,8 @@ define([ compressedAttribute2 : 4, // image height, color, pick color, 2 bytes free eyeOffset : 5, scaleByDistance : 6, - pixelOffsetScaleByDistance : 7 + pixelOffsetScaleByDistance : 7, + ownerSize : 8 }; /** @@ -103,6 +105,7 @@ define([ * @param {Object} [options] Object with the following properties: * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms each billboard from model to world coordinates. * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown. + * @param {Scene} [options.scene] Must be passed in for billboards that use the height reference property or will be depth tested against the globe. * * @performance For best performance, prefer a few collections, each with many billboards, to * many collections with only a few billboards each. Organize collections so that billboards @@ -132,6 +135,8 @@ define([ var BillboardCollection = function(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this._scene = options.scene; + this._textureAtlas = undefined; this._textureAtlasGUID = undefined; this._destroyTextureAtlas = true; @@ -246,7 +251,8 @@ define([ BufferUsage.STATIC_DRAW, // ALIGNED_AXIS_INDEX BufferUsage.STATIC_DRAW, // SCALE_BY_DISTANCE_INDEX BufferUsage.STATIC_DRAW, // TRANSLUCENCY_BY_DISTANCE_INDEX - BufferUsage.STATIC_DRAW // PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX + BufferUsage.STATIC_DRAW, // PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX + BufferUsage.STATIC_DRAW // OWNER_SIZE_INDEX ]; var that = this; @@ -600,6 +606,11 @@ define([ componentsPerAttribute : 4, componentDatatype : ComponentDatatype.FLOAT, usage : buffersUsage[PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX] + }, { + index : attributeLocations.ownerSize, + componentsPerAttribute : 2, + componentDatatype : ComponentDatatype.FLOAT, + usage : buffersUsage[OWNER_SIZE_INDEX] }], 4 * numberOfBillboards); // 4 vertices per billboard } @@ -922,6 +933,17 @@ define([ writer(i + 3, near, nearValue, far, farValue); } + function writeOwnerSize(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard) { + var i = billboard._index * 4; + var size = billboard._ownerSize; + + var writer = vafWriters[attributeLocations.ownerSize]; + writer(i + 0, size.x, size.y); + writer(i + 1, size.x, size.y); + writer(i + 2, size.x, size.y); + writer(i + 3, size.x, size.y); + } + function writeBillboard(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard) { writePositionScaleAndRotation(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard); writeCompressedAttrib0(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard); @@ -930,6 +952,7 @@ define([ writeEyeOffset(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard); writeScaleByDistance(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard); writePixelOffsetScaleByDistance(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard); + writeOwnerSize(billboardCollection, context, textureAtlasCoordinates, vafWriters, billboard); } function recomputeActualPositions(billboardCollection, billboards, length, frameState, modelMatrix, recomputeBoundingVolume) { @@ -945,7 +968,7 @@ define([ for ( var i = 0; i < length; ++i) { var billboard = billboards[i]; var position = billboard.position; - var actualPosition = Billboard._computeActualPosition(position, frameState, modelMatrix); + var actualPosition = Billboard._computeActualPosition(billboard, position, frameState, modelMatrix); if (defined(actualPosition)) { billboard._setActualPosition(actualPosition); @@ -1026,6 +1049,11 @@ define([ * @exception {RuntimeError} image with id must be in the atlas. */ BillboardCollection.prototype.update = function(context, frameState, commandList) { + var scene = this._scene; + if (defined(scene) && (!scene._globeDepth.supported || context.maximumVertexTextureImageUnits === 0)) { + throw new DeveloperError('Bilboards with a height reference are not supported.'); + } + removeBillboards(this); var billboards = this._billboards; @@ -1127,8 +1155,11 @@ define([ writers.push(writePixelOffsetScaleByDistance); } - var numWriters = writers.length; + if (properties[OWNER_SIZE_INDEX]) { + writers.push(writeOwnerSize); + } + var numWriters = writers.length; vafWriters = this._vaf.writers; if ((billboardsToUpdateLength / billboardsLength) > 0.1) { @@ -1191,9 +1222,9 @@ define([ var va; var vaLength; var command; - var j; var vs; var fs; + var j; if (pass.render) { var colorList = this._colorCommands; @@ -1232,6 +1263,9 @@ define([ if (this._shaderPixelOffsetScaleByDistance) { vs.defines.push('EYE_DISTANCE_PIXEL_OFFSET'); } + if (defined(this._scene)) { + vs.defines.push('TEST_GLOBE_DEPTH'); + } this._sp = context.replaceShaderProgram(this._sp, vs, BillboardCollectionFS, attributeLocations); this._compiledShaderRotation = this._shaderRotation; @@ -1297,6 +1331,9 @@ define([ if (this._shaderPixelOffsetScaleByDistance) { vs.defines.push('EYE_DISTANCE_PIXEL_OFFSET'); } + if (defined(this._scene)) { + vs.defines.push('TEST_GLOBE_DEPTH'); + } fs = new ShaderSource({ defines : ['RENDER_FOR_PICK'], diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index d4c97243abcf..4e1fb885ab6c 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -409,7 +409,7 @@ define([ var intersection; length = sphereIntersections.length; for (i = 0; i < length; ++i) { - intersection = sphereIntersections[i].pick(ray, scene, true, result); + intersection = sphereIntersections[i].pick(ray, scene.mode, scene.mapProjection, true, result); if (defined(intersection)) { break; } @@ -482,7 +482,7 @@ define([ var ray = scratchGetHeightRay; Cartesian3.normalize(cartesian, ray.direction); - var intersection = tile.data.pick(ray, undefined, false, scratchGetHeightIntersection); + var intersection = tile.data.pick(ray, undefined, undefined, false, scratchGetHeightIntersection); if (!defined(intersection)) { return undefined; } diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index ba6039493603..a9a8eb8ebc1d 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -170,12 +170,11 @@ define([ } }); - function getPosition(tile, scene, vertices, stride, index, result) { + function getPosition(tile, mode, projection, vertices, stride, index, result) { Cartesian3.unpack(vertices, index * stride, result); Cartesian3.add(tile.center, result, result); - if (defined(scene) && scene.mode !== SceneMode.SCENE3D) { - var projection = scene.mapProjection; + if (defined(mode) && mode !== SceneMode.SCENE3D) { var ellipsoid = projection.ellipsoid; var positionCart = ellipsoid.cartesianToCartographic(result); projection.project(positionCart, result); @@ -190,7 +189,7 @@ define([ var scratchV2 = new Cartesian3(); var scratchResult = new Cartesian3(); - GlobeSurfaceTile.prototype.pick = function(ray, scene, cullBackFaces, result) { + GlobeSurfaceTile.prototype.pick = function(ray, mode, projection, cullBackFaces, result) { var terrain = this.pickTerrain; if (!defined(terrain)) { return undefined; @@ -211,9 +210,9 @@ define([ var i1 = indices[i + 1]; var i2 = indices[i + 2]; - var v0 = getPosition(this, scene, vertices, stride, i0, scratchV0); - var v1 = getPosition(this, scene, vertices, stride, i1, scratchV1); - var v2 = getPosition(this, scene, vertices, stride, i2, scratchV2); + var v0 = getPosition(this, mode, projection, vertices, stride, i0, scratchV0); + var v1 = getPosition(this, mode, projection, vertices, stride, i1, scratchV1); + var v2 = getPosition(this, mode, projection, vertices, stride, i2, scratchV2); var intersection = IntersectionTests.rayTriangle(ray, v0, v1, v2, cullBackFaces, scratchResult); if (defined(intersection)) { diff --git a/Source/Scene/HeightReference.js b/Source/Scene/HeightReference.js new file mode 100644 index 000000000000..50522c3a2143 --- /dev/null +++ b/Source/Scene/HeightReference.js @@ -0,0 +1,38 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + "use strict"; + + /** + * Represents the position relative to the terrain. + * + * @namespace + * @alias HeightReference + */ + var HeightReference = { + /** + * The position is absolute. + * @type {Number} + * @constant + */ + NONE : 0, + + /** + * The position is clamped to the terrain. + * @type {Number} + * @constant + */ + CLAMP_TO_GROUND : 1, + + /** + * The position height is the height above the terrain. + * @type {Number} + * @constant + */ + RELATIVE_TO_GROUND : 2 + }; + + return freezeObject(HeightReference); +}); diff --git a/Source/Scene/Label.js b/Source/Scene/Label.js index a0bdc910e03c..ef165523f0b9 100644 --- a/Source/Scene/Label.js +++ b/Source/Scene/Label.js @@ -9,6 +9,7 @@ define([ '../Core/DeveloperError', '../Core/NearFarScalar', './Billboard', + './HeightReference', './HorizontalOrigin', './LabelStyle', './VerticalOrigin' @@ -22,6 +23,7 @@ define([ DeveloperError, NearFarScalar, Billboard, + HeightReference, HorizontalOrigin, LabelStyle, VerticalOrigin) { @@ -86,12 +88,19 @@ define([ this._id = options.id; this._translucencyByDistance = options.translucencyByDistance; this._pixelOffsetScaleByDistance = options.pixelOffsetScaleByDistance; + this._heightReference = defaultValue(options.heightReference, HeightReference.NONE); this._labelCollection = labelCollection; this._glyphs = []; this._rebindAllGlyphs = true; this._repositionAllGlyphs = true; + + this._actualClampedPosition = undefined; + this._removeCallbackFunc = undefined; + this._mode = undefined; + + this._updateClamping(); }; defineProperties(Label.prototype, { @@ -146,17 +155,44 @@ define([ if (!Cartesian3.equals(position, value)) { Cartesian3.clone(value, position); - var glyphs = this._glyphs; - for (var i = 0, len = glyphs.length; i < len; i++) { - var glyph = glyphs[i]; - if (defined(glyph.billboard)) { - glyph.billboard.position = value; + if (this._heightReference === HeightReference.NONE) { + var glyphs = this._glyphs; + for (var i = 0, len = glyphs.length; i < len; i++) { + var glyph = glyphs[i]; + if (defined(glyph.billboard)) { + glyph.billboard.position = value; + } } + } else { + this._updateClamping(); } } } }, + /** + * Gets or sets the height reference of this billboard. + * @memberof Label.prototype + * @type {HeightReference} + */ + heightReference : { + get : function() { + return this._heightReference; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + if (!defined(value)) { + throw new DeveloperError('value is required.'); + } + //>>includeEnd('debug'); + + if (value !== this._heightReference) { + this._heightReference = value; + this._updateClamping(); + } + } + }, + /** * Gets or sets the text of this label. * @memberof Label.prototype @@ -627,9 +663,36 @@ define([ } } } + }, + + /** + * Keeps track of the position of the label based on the height reference. + * @memberof Label.prototype + * @type {Cartesian3} + * @private + */ + _clampedPosition : { + get : function() { + return this._actualClampedPosition; + }, + set : function(value) { + this._actualClampedPosition = Cartesian3.clone(value, this._actualClampedPosition); + + var glyphs = this._glyphs; + for (var i = 0, len = glyphs.length; i < len; i++) { + var glyph = glyphs[i]; + if (defined(glyph.billboard)) { + glyph.billboard.position = value; + } + } + } } }); + Label.prototype._updateClamping = function() { + Billboard._updateClamping(this._labelCollection, this); + }; + /** * Computes the screen-space position of the label's origin, taking into account eye and pixel offsets. * The screen space origin is the top, left corner of the canvas; x increases from @@ -658,7 +721,7 @@ define([ var labelCollection = this._labelCollection; var modelMatrix = labelCollection.modelMatrix; - var actualPosition = Billboard._computeActualPosition(this._position, scene.frameState, modelMatrix); + var actualPosition = Billboard._computeActualPosition(this, this._position, scene.frameState, modelMatrix); var windowCoordinates = Billboard._computeScreenSpacePosition(modelMatrix, actualPosition, this._eyeOffset, this._pixelOffset, scene, result); diff --git a/Source/Scene/LabelCollection.js b/Source/Scene/LabelCollection.js index cceeb26f9e18..40ccad433034 100644 --- a/Source/Scene/LabelCollection.js +++ b/Source/Scene/LabelCollection.js @@ -210,6 +210,7 @@ define([ // reusable Cartesian2 instance var glyphPixelOffset = new Cartesian2(); + var ownerSize = new Cartesian2(); function repositionAllGlyphs(label, resolutionScale) { var glyphs = label._glyphs; @@ -217,6 +218,7 @@ define([ var dimensions; var totalWidth = 0; var maxHeight = 0; + var maxWidth = 0; var glyphIndex = 0; var glyphLength = glyphs.length; @@ -225,6 +227,7 @@ define([ dimensions = glyph.dimensions; totalWidth += dimensions.computedWidth; maxHeight = Math.max(maxHeight, dimensions.height); + maxWidth = Math.max(maxWidth, dimensions.computedWidth); } var scale = label._scale; @@ -239,6 +242,9 @@ define([ glyphPixelOffset.x = widthOffset * resolutionScale; glyphPixelOffset.y = 0; + ownerSize.x = maxWidth; + ownerSize.y = maxHeight; + var verticalOrigin = label._verticalOrigin; for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) { glyph = glyphs[glyphIndex]; @@ -256,6 +262,7 @@ define([ if (defined(glyph.billboard)) { glyph.billboard._setTranslate(glyphPixelOffset); + glyph.billboard._setOwnerSize(ownerSize); } glyphPixelOffset.x += dimensions.computedWidth * scale * resolutionScale; @@ -268,6 +275,11 @@ define([ unbindGlyph(labelCollection, glyphs[i]); } label._labelCollection = undefined; + + if (defined(label._removeCallbackFunc)) { + label._removeCallbackFunc(); + } + destroyObject(label); } @@ -289,6 +301,7 @@ define([ * @param {Object} [options] Object with the following properties: * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms each label from model to world coordinates. * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown. + * @param {Scene} [options.scene] Must be passed in for labels that use the height reference property or will be depth tested against the globe. * * @performance For best performance, prefer a few collections, each with many labels, to * many collections with only a few labels each. Avoid having collections where some @@ -317,9 +330,13 @@ define([ var LabelCollection = function(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this._scene = options.scene; + this._textureAtlas = undefined; - this._billboardCollection = new BillboardCollection(); + this._billboardCollection = new BillboardCollection({ + scene : this._scene + }); this._billboardCollection.destroyTextureAtlas = false; this._spareBillboards = []; @@ -577,7 +594,8 @@ define([ labelsToUpdate = this._labelsToUpdate; } - for (var i = 0, len = labelsToUpdate.length; i < len; ++i) { + var len = labelsToUpdate.length; + for (var i = 0; i < len; ++i) { var label = labelsToUpdate[i]; if (label.isDestroyed()) { continue; @@ -638,6 +656,7 @@ define([ this.removeAll(); this._billboardCollection = this._billboardCollection.destroy(); this._textureAtlas = this._textureAtlas && this._textureAtlas.destroy(); + return destroyObject(this); }; diff --git a/Source/Scene/PolylineCollection.js b/Source/Scene/PolylineCollection.js index 65c30b3ca70f..c06f5d47736d 100644 --- a/Source/Scene/PolylineCollection.js +++ b/Source/Scene/PolylineCollection.js @@ -312,7 +312,7 @@ define([ * Determines if this collection contains the specified polyline. * * @param {Polyline} polyline The polyline to check for. - * @returns {Boolean} true if this collection contains the billboard, false otherwise. + * @returns {Boolean} true if this collection contains the polyline, false otherwise. * * @see PolylineCollection#get */ diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 2cfd8ab8373e..6a7b67d8adeb 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -1,11 +1,16 @@ /*global define*/ define([ + '../Core/Cartesian3', + '../Core/Cartographic', '../Core/defaultValue', '../Core/defined', '../Core/defineProperties', '../Core/DeveloperError', + '../Core/Event', '../Core/getTimestamp', '../Core/Queue', + '../Core/Ray', + '../Core/Rectangle', '../Core/Visibility', './QuadtreeOccluders', './QuadtreeTile', @@ -13,12 +18,17 @@ define([ './SceneMode', './TileReplacementQueue' ], function( + Cartesian3, + Cartographic, defaultValue, defined, defineProperties, DeveloperError, + Event, getTimestamp, Queue, + Ray, + Rectangle, Visibility, QuadtreeOccluders, QuadtreeTile, @@ -90,6 +100,13 @@ define([ this._levelZeroTilesReady = false; this._loadQueueTimeSlice = 5.0; + this._addHeightCallbacks = []; + this._removeHeightCallbacks = []; + + this._tileToUpdateHeights = []; + this._lastTileIndex = 0; + this._updateHeightsTimeSlice = 2.0; + /** * Gets or sets the maximum screen-space error, in pixels, that is allowed. * A higher maximum error will render fewer tiles and improve performance, while a lower @@ -144,6 +161,16 @@ define([ var levelZeroTiles = this._levelZeroTiles; if (defined(levelZeroTiles)) { for (var i = 0; i < levelZeroTiles.length; ++i) { + var tile = levelZeroTiles[i]; + var customData = tile.customData; + var customDataLength = customData.length; + + for (var j = 0; j < customDataLength; ++j) { + var data = customData[j]; + data.level = 0; + this._addHeightCallbacks.push(data); + } + levelZeroTiles[i].freeResources(); } } @@ -182,6 +209,31 @@ define([ } }; + /** + * Calls the callback when a new tile is rendered that contains the given cartographic. The only parameter + * is the cartesian position on the tile. + * + * @param {Cartographic} cartographic The cartographic position. + * @param {Function} callback The function to be called when a new tile is loaded containing cartographic. + * @returns {Function} The function to remove this callback from the quadtree. + */ + QuadtreePrimitive.prototype.updateHeight = function(cartographic, callback) { + var primitive = this; + var object = { + position : undefined, + positionCartographic : cartographic, + level : -1, + callback : callback + }; + + object.removeFunc = function() { + primitive._removeHeightCallbacks.push(object); + }; + + primitive._addHeightCallbacks.push(object); + return object.removeFunc; + }; + /** * Updates the primitive. * @@ -247,6 +299,7 @@ define([ } var i; + var j; var len; // Clear the render list. @@ -282,9 +335,23 @@ define([ var occluders = primitive._occluders; var tile; + var levelZeroTiles = primitive._levelZeroTiles; + + var customDataAdded = primitive._addHeightCallbacks; + var customDataRemoved = primitive._removeHeightCallbacks; + var frameNumber = frameState.frameNumber; + + if (customDataAdded.length > 0 || customDataRemoved.length > 0) { + for (i = 0, len = levelZeroTiles.length; i < len; ++i) { + tile = levelZeroTiles[i]; + tile._updateCustomData(frameNumber, customDataAdded, customDataRemoved); + } + + customDataAdded.length = 0; + customDataRemoved.length = 0; + } // Enqueue the root tiles that are renderable and visible. - var levelZeroTiles = primitive._levelZeroTiles; for (i = 0, len = levelZeroTiles.length; i < len; ++i) { tile = levelZeroTiles[i]; primitive._tileReplacementQueue.markTileRendered(tile); @@ -309,6 +376,7 @@ define([ ++debug.tilesVisited; primitive._tileReplacementQueue.markTileRendered(tile); + tile._updateCustomData(frameNumber); if (tile.level > debug.maxDepth) { debug.maxDepth = tile.level; @@ -450,6 +518,96 @@ define([ } } + var scratchRay = new Ray(); + var scratchCartographic = new Cartographic(); + var scratchPosition = new Cartesian3(); + + function updateHeights(primitive, frameState) { + var tilesToUpdateHeights = primitive._tileToUpdateHeights; + var terrainProvider = primitive._tileProvider.terrainProvider; + + var startTime = getTimestamp(); + var timeSlice = primitive._updateHeightsTimeSlice; + var endTime = startTime + timeSlice; + + var mode = frameState.mode; + var projection = frameState.mapProjection; + var ellipsoid = projection.ellipsoid; + + while (tilesToUpdateHeights.length > 0) { + var tile = tilesToUpdateHeights[tilesToUpdateHeights.length - 1]; + if (tile !== primitive._lastTileUpdated) { + primitive._lastTileIndex = 0; + } + + var customData = tile.customData; + var customDataLength = customData.length; + + var timeSliceMax = false; + for (var i = primitive._lastTileIndex; i < customDataLength; ++i) { + var data = customData[i]; + + if (tile.level > data.level) { + if (!defined(data.position)) { + data.position = ellipsoid.cartographicToCartesian(data.positionCartographic); + } + + if (mode === SceneMode.SCENE3D) { + Cartesian3.clone(Cartesian3.ZERO, scratchRay.origin); + Cartesian3.normalize(data.position, scratchRay.direction); + } else { + Cartographic.clone(data.positionCartographic, scratchCartographic); + + // minimum height for the terrain set, need to get this information from the terrain provider + scratchCartographic.height = -11500.0; + projection.project(scratchCartographic, scratchPosition); + Cartesian3.fromElements(scratchPosition.z, scratchPosition.x, scratchPosition.y, scratchPosition); + Cartesian3.clone(scratchPosition, scratchRay.origin); + Cartesian3.clone(Cartesian3.UNIT_X, scratchRay.direction); + } + + var position = tile.data.pick(scratchRay, mode, projection, false, scratchPosition); + if (defined(position)) { + data.callback(position); + } + + data.level = tile.level; + } else if (tile.level === data.level) { + var children = tile.children; + var childrenLength = children.length; + + var child; + for (var j = 0; j < childrenLength; ++j) { + child = children[j]; + if (Rectangle.contains(child.rectangle, data.positionCartographic)) { + break; + } + } + + var tileDataAvailable = terrainProvider.getTileDataAvailable(child.x, child.y, child.level); + if ((defined(tileDataAvailable) && !tileDataAvailable) || + (defined(parent) && defined(parent.data) && defined(parent.data.terrainData) && + !parent.data.terrainData.isChildAvailable(parent.x, parent.y, child.x, child.y))) { + data.removeFunc(); + } + } + + if (getTimestamp() >= endTime) { + timeSliceMax = true; + break; + } + } + + if (timeSliceMax) { + primitive._lastTileUpdated = tile; + primitive._lastTileIndex = i; + break; + } else { + tilesToUpdateHeights.pop(); + } + } + } + function tileDistanceSortFunction(a, b) { return a._distance - b._distance; } @@ -457,12 +615,21 @@ define([ function createRenderCommandsForSelectedTiles(primitive, context, frameState, commandList) { var tileProvider = primitive._tileProvider; var tilesToRender = primitive._tilesToRender; + var tilesToUpdateHeights = primitive._tileToUpdateHeights; tilesToRender.sort(tileDistanceSortFunction); for (var i = 0, len = tilesToRender.length; i < len; ++i) { - tileProvider.showTileThisFrame(tilesToRender[i], context, frameState, commandList); + var tile = tilesToRender[i]; + tileProvider.showTileThisFrame(tile, context, frameState, commandList); + + if (tile._frameRendered !== frameState.frameNumber - 1) { + tilesToUpdateHeights.push(tile); + } + tile._frameRendered = frameState.frameNumber; } + + updateHeights(primitive, frameState); } return QuadtreePrimitive; diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 0f799f79b007..abc1e0231c66 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -3,11 +3,13 @@ define([ '../Core/defined', '../Core/defineProperties', '../Core/DeveloperError', + '../Core/Rectangle', './QuadtreeTileLoadState' ], function( defined, defineProperties, DeveloperError, + Rectangle, QuadtreeTileLoadState) { "use strict"; @@ -62,6 +64,10 @@ define([ // QuadtreePrimitive gets/sets this private property. this._distance = 0.0; + this._customData = []; + this._frameUpdated = undefined; + this._frameRendered = undefined; + /** * Gets or sets the current state of the tile in the tile load pipeline. * @type {QuadtreeTileLoadState} @@ -129,6 +135,54 @@ define([ return result; }; + QuadtreeTile.prototype._updateCustomData = function(frameNumber, added, removed) { + var customData = this.customData; + + var i; + var data; + var rectangle; + + if (defined(added) && defined(removed)) { + // level zero tile + for (i = 0; i < removed.length; ++i) { + data = removed[i]; + for (var j = 0; j < customData.length; ++j) { + if (customData[j] === data) { + customData.splice(j, 1); + break; + } + } + } + + rectangle = this._rectangle; + for (i = 0; i < added.length; ++i) { + data = added[i]; + if (Rectangle.contains(rectangle, data.positionCartographic)) { + customData.push(data); + } + } + + this._frameUpdated = frameNumber; + } else { + // interior or leaf tile, update from parent + var parent = this._parent; + if (defined(parent) && this._frameUpdated !== parent._frameUpdated) { + customData.length = 0; + + rectangle = this._rectangle; + var parentCustomData = parent.customData; + for (i = 0; i < parentCustomData.length; ++i) { + data = parentCustomData[i]; + if (Rectangle.contains(rectangle, data.positionCartographic)) { + customData.push(data); + } + } + + this._frameUpdated = parent._frameUpdated; + } + } + }; + defineProperties(QuadtreeTile.prototype, { /** * Gets the tiling scheme used to tile the surface. @@ -240,6 +294,17 @@ define([ } }, + /** + * An array of objects associated with this tile. + * @memberof QuadtreeTile.prototype + * @type {Array} + */ + customData : { + get : function() { + return this._customData; + } + }, + /** * Gets a value indicating whether or not this tile needs further loading. * This property will return true if the {@link QuadtreeTile#state} is diff --git a/Source/Shaders/BillboardCollectionVS.glsl b/Source/Shaders/BillboardCollectionVS.glsl index 7cf499bb2fae..780e69181e41 100644 --- a/Source/Shaders/BillboardCollectionVS.glsl +++ b/Source/Shaders/BillboardCollectionVS.glsl @@ -6,6 +6,7 @@ attribute vec4 compressedAttribute2; // image height, color, pick color, attribute vec3 eyeOffset; // eye offset in meters attribute vec4 scaleByDistance; // near, nearScale, far, farScale attribute vec4 pixelOffsetScaleByDistance; // near, nearScale, far, farScale +attribute vec2 ownerSize; varying vec2 v_textureCoordinates; @@ -32,6 +33,42 @@ const float SHIFT_RIGHT3 = 1.0 / 8.0; const float SHIFT_RIGHT2 = 1.0 / 4.0; const float SHIFT_RIGHT1 = 1.0 / 2.0; +vec4 computePositionWindowCoordinates(vec4 positionEC, vec2 imageSize, float scale, vec2 direction, vec2 origin, vec2 translate, vec2 pixelOffset, vec3 alignedAxis, float rotation) +{ + vec4 positionWC = czm_eyeToWindowCoordinates(positionEC); + + vec2 halfSize = imageSize * scale * czm_resolutionScale; + halfSize *= ((direction * 2.0) - 1.0); + + positionWC.xy += (origin * abs(halfSize)); + +#if defined(ROTATION) || defined(ALIGNED_AXIS) + if (!all(equal(alignedAxis, vec3(0.0))) || rotation != 0.0) + { + float angle = rotation; + if (!all(equal(alignedAxis, vec3(0.0)))) + { + vec3 pos = positionEC.xyz + czm_encodedCameraPositionMCHigh + czm_encodedCameraPositionMCLow; + vec3 normal = normalize(cross(alignedAxis, pos)); + vec4 tangent = vec4(normalize(cross(pos, normal)), 0.0); + tangent = czm_modelViewProjection * tangent; + angle += sign(-tangent.x) * acos(tangent.y / length(tangent.xy)); + } + + float cosTheta = cos(angle); + float sinTheta = sin(angle); + mat2 rotationMatrix = mat2(cosTheta, sinTheta, -sinTheta, cosTheta); + halfSize = rotationMatrix * halfSize; + } +#endif + + positionWC.xy += halfSize; + positionWC.xy += translate; + positionWC.xy += (pixelOffset * czm_resolutionScale); + + return positionWC; +} + void main() { // Modifying this shader may also require modifications to Billboard._computeScreenSpacePosition @@ -43,6 +80,8 @@ void main() #if defined(ROTATION) || defined(ALIGNED_AXIS) float rotation = positionLowAndRotation.w; +#else + float rotation = 0.0; #endif float compressed = compressedAttribute0.x; @@ -172,38 +211,43 @@ void main() float pixelOffsetScale = czm_nearFarScalar(pixelOffsetScaleByDistance, lengthSq); pixelOffset *= pixelOffsetScale; #endif - - vec4 positionWC = czm_eyeToWindowCoordinates(positionEC); - vec2 halfSize = imageSize * scale * czm_resolutionScale; - halfSize *= ((direction * 2.0) - 1.0); - - positionWC.xy += (origin * abs(halfSize)); - -#if defined(ROTATION) || defined(ALIGNED_AXIS) - if (!all(equal(alignedAxis, vec3(0.0))) || rotation != 0.0) +#ifdef TEST_GLOBE_DEPTH + if (-positionEC.z < 50000.0) { - float angle = rotation; - if (!all(equal(alignedAxis, vec3(0.0)))) - { - vec3 pos = positionEC.xyz + czm_encodedCameraPositionMCHigh + czm_encodedCameraPositionMCLow; - vec3 normal = normalize(cross(alignedAxis, pos)); - vec4 tangent = vec4(normalize(cross(pos, normal)), 0.0); - tangent = czm_modelViewProjection * tangent; - angle += sign(-tangent.x) * acos(tangent.y / length(tangent.xy)); - } + vec4 offsetPosition = positionEC; + offsetPosition.z *= 0.99; - float cosTheta = cos(angle); - float sinTheta = sin(angle); - mat2 rotationMatrix = mat2(cosTheta, sinTheta, -sinTheta, cosTheta); - halfSize = rotationMatrix * halfSize; + vec2 directions[4]; + directions[0] = vec2(0.0, 0.0); + directions[1] = vec2(0.0, 1.0); + directions[2] = vec2(1.0, 0.0); + directions[3] = vec2(1.0, 1.0); + + vec2 invSize = 1.0 / czm_viewport.zw; + vec2 size = all(equal(vec2(0.0), ownerSize)) ? imageSize : ownerSize; + + bool visible = false; + for (int i = 0; i < 4; ++i) + { + vec4 wc = computePositionWindowCoordinates(offsetPosition, size, scale, directions[i], vec2(0.0, 0.0), vec2(0.0), pixelOffset, alignedAxis, rotation); + float d = texture2D(czm_globeDepthTexture, wc.xy * invSize).r; + if (wc.z < d) + { + visible = true; + break; + } + } + + if (!visible) + { + gl_Position = czm_projection[3]; + return; + } } #endif - positionWC.xy += halfSize; - positionWC.xy += translate; - positionWC.xy += (pixelOffset * czm_resolutionScale); - + vec4 positionWC = computePositionWindowCoordinates(positionEC, imageSize, scale, direction, origin, translate, pixelOffset, alignedAxis, rotation); gl_Position = czm_viewportOrthographic * vec4(positionWC.xy, -positionWC.z, 1.0); v_textureCoordinates = textureCoordinates; diff --git a/Specs/Scene/BillboardCollectionSpec.js b/Specs/Scene/BillboardCollectionSpec.js index 8de04fec60e6..4a5525d7a9df 100644 --- a/Specs/Scene/BillboardCollectionSpec.js +++ b/Specs/Scene/BillboardCollectionSpec.js @@ -6,9 +6,11 @@ defineSuite([ 'Core/Cartesian2', 'Core/Cartesian3', 'Core/Color', + 'Core/Ellipsoid', 'Core/loadImage', 'Core/Math', 'Core/NearFarScalar', + 'Scene/HeightReference', 'Scene/HorizontalOrigin', 'Scene/OrthographicFrustum', 'Scene/TextureAtlas', @@ -23,9 +25,11 @@ defineSuite([ Cartesian2, Cartesian3, Color, + Ellipsoid, loadImage, CesiumMath, NearFarScalar, + HeightReference, HorizontalOrigin, OrthographicFrustum, TextureAtlas, @@ -39,6 +43,8 @@ defineSuite([ var scene; var camera; var billboards; + var heightReferenceSupported; + var billboardsWithHeight; var greenImage; var blueImage; @@ -49,6 +55,8 @@ defineSuite([ scene = createScene(); camera = scene.camera; + heightReferenceSupported = scene._globeDepth.supported && scene.context.maximumVertexTextureImageUnits > 0; + return when.join( loadImage('./Data/Images/Green.png').then(function(result) { greenImage = result; @@ -70,11 +78,20 @@ defineSuite([ beforeEach(function() { scene.morphTo3D(0); + camera.position = new Cartesian3(10.0, 0.0, 0.0); camera.direction = Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()); camera.up = Cartesian3.clone(Cartesian3.UNIT_Z); + billboards = new BillboardCollection(); scene.primitives.add(billboards); + + if (heightReferenceSupported) { + billboardsWithHeight = new BillboardCollection({ + scene : scene + }); + scene.primitives.add(billboardsWithHeight); + } }); afterEach(function() { @@ -104,6 +121,7 @@ defineSuite([ expect(b.width).not.toBeDefined(); expect(b.height).not.toBeDefined(); expect(b.id).not.toBeDefined(); + expect(b.heightReference).toEqual(HeightReference.NONE); }); it('can add and remove before first update.', function() { @@ -1458,4 +1476,140 @@ defineSuite([ return deferred.promise; }); + + describe('height referenced billboards', function() { + function createMockGlobe() { + var globe = { + callback : undefined, + removedCallback : false, + ellipsoid : Ellipsoid.WGS84, + update : function() {}, + getHeight : function() { + return 0.0; + }, + _surface : {}, + destroy : function() {} + }; + + globe._surface.updateHeight = function(position, callback) { + globe.callback = callback; + return function() { + globe.removedCallback = true; + globe.callback = undefined; + }; + }; + + return globe; + } + + it('explicitly constructs a billboard with height reference', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var b = billboardsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND + }); + + expect(b.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + }); + + it('set billboard height reference property', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var b = billboardsWithHeight.add(); + b.heightReference = HeightReference.CLAMP_TO_GROUND; + + expect(b.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + }); + + it('creating with a height reference creates a height update callback', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var b = billboardsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + }); + + it('set height reference property creates a height update callback', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var b = billboardsWithHeight.add({ + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + b.heightReference = HeightReference.CLAMP_TO_GROUND; + expect(scene.globe.callback).toBeDefined(); + }); + + it('updates the callback when the height reference changes', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var b = billboardsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + + b.heightReference = HeightReference.RELATIVE_TO_GROUND; + expect(scene.globe.removedCallback).toEqual(true); + expect(scene.globe.callback).toBeDefined(); + + scene.globe.removedCallback = false; + b.heightReference = HeightReference.NONE; + expect(scene.globe.removedCallback).toEqual(true); + expect(scene.globe.callback).not.toBeDefined(); + }); + + it('changing the position updates the callback', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var b = billboardsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + + b.position = Cartesian3.fromDegrees(-73.0, 40.0); + expect(scene.globe.removedCallback).toEqual(true); + expect(scene.globe.callback).toBeDefined(); + }); + + it('callback updates the position', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var b = billboardsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + + var cartographic = scene.globe.ellipsoid.cartesianToCartographic(b._clampedPosition); + expect(cartographic.height).toEqual(0.0); + + scene.globe.callback(Cartesian3.fromDegrees(-72.0, 40.0, 100.0)); + cartographic = scene.globe.ellipsoid.cartesianToCartographic(b._clampedPosition); + expect(cartographic.height).toEqualEpsilon(100.0, CesiumMath.EPSILON9); + }); + }); }, 'WebGL'); diff --git a/Specs/Scene/LabelCollectionSpec.js b/Specs/Scene/LabelCollectionSpec.js index a0f740e96c85..b7d56a0bd9d8 100644 --- a/Specs/Scene/LabelCollectionSpec.js +++ b/Specs/Scene/LabelCollectionSpec.js @@ -5,8 +5,10 @@ defineSuite([ 'Core/Cartesian2', 'Core/Cartesian3', 'Core/Color', + 'Core/Ellipsoid', 'Core/Math', 'Core/NearFarScalar', + 'Scene/HeightReference', 'Scene/HorizontalOrigin', 'Scene/LabelStyle', 'Scene/OrthographicFrustum', @@ -18,8 +20,10 @@ defineSuite([ Cartesian2, Cartesian3, Color, + Ellipsoid, CesiumMath, NearFarScalar, + HeightReference, HorizontalOrigin, LabelStyle, OrthographicFrustum, @@ -33,10 +37,14 @@ defineSuite([ var scene; var camera; var labels; + var heightReferenceSupported; + var labelsWithHeight; beforeAll(function() { scene = createScene(); camera = scene.camera; + + heightReferenceSupported = scene._globeDepth.supported && scene.context.maximumVertexTextureImageUnits > 0; }); afterAll(function() { @@ -45,11 +53,20 @@ defineSuite([ beforeEach(function() { scene.morphTo3D(0); + camera.position = new Cartesian3(10.0, 0.0, 0.0); camera.direction = Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()); camera.up = Cartesian3.clone(Cartesian3.UNIT_Z); + labels = new LabelCollection(); scene.primitives.add(labels); + + if (heightReferenceSupported) { + labelsWithHeight = new LabelCollection({ + scene : scene + }); + scene.primitives.add(labelsWithHeight); + } }); afterEach(function() { @@ -1617,4 +1634,140 @@ defineSuite([ expect(textureAtlas.isDestroyed()).toBe(true); }); + describe('height referenced labels', function() { + function createMockGlobe() { + var globe = { + callback : undefined, + removedCallback : false, + ellipsoid : Ellipsoid.WGS84, + update : function() {}, + getHeight : function() { + return 0.0; + }, + _surface : {}, + destroy : function() {} + }; + + globe._surface.updateHeight = function(position, callback) { + globe.callback = callback; + return function() { + globe.removedCallback = true; + globe.callback = undefined; + }; + }; + + return globe; + } + + it('explicitly constructs a label with height reference', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var l = labelsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND + }); + + expect(l.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + }); + + it('set label height reference property', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var l = labelsWithHeight.add(); + l.heightReference = HeightReference.CLAMP_TO_GROUND; + + expect(l.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + }); + + it('creating with a height reference creates a height update callback', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var l = labelsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + }); + + it('set height reference property creates a height update callback', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var l = labelsWithHeight.add({ + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + l.heightReference = HeightReference.CLAMP_TO_GROUND; + expect(scene.globe.callback).toBeDefined(); + }); + + it('updates the callback when the height reference changes', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var l = labelsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + + l.heightReference = HeightReference.RELATIVE_TO_GROUND; + expect(scene.globe.removedCallback).toEqual(true); + expect(scene.globe.callback).toBeDefined(); + + scene.globe.removedCallback = false; + l.heightReference = HeightReference.NONE; + expect(scene.globe.removedCallback).toEqual(true); + expect(scene.globe.callback).not.toBeDefined(); + }); + + it('changing the position updates the callback', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var l = labelsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + + l.position = Cartesian3.fromDegrees(-73.0, 40.0); + expect(scene.globe.removedCallback).toEqual(true); + expect(scene.globe.callback).toBeDefined(); + }); + + it('callback updates the position', function() { + if (!heightReferenceSupported) { + return; + } + + scene.globe = createMockGlobe(); + var l = labelsWithHeight.add({ + heightReference : HeightReference.CLAMP_TO_GROUND, + position : Cartesian3.fromDegrees(-72.0, 40.0) + }); + expect(scene.globe.callback).toBeDefined(); + + var cartographic = scene.globe.ellipsoid.cartesianToCartographic(l._clampedPosition); + expect(cartographic.height).toEqual(0.0); + + scene.globe.callback(Cartesian3.fromDegrees(-72.0, 40.0, 100.0)); + cartographic = scene.globe.ellipsoid.cartesianToCartographic(l._clampedPosition); + expect(cartographic.height).toEqualEpsilon(100.0, CesiumMath.EPSILON9); + }); + }); + }, 'WebGL'); \ No newline at end of file diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index dcee05216c89..3787fd46b6fd 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -1,6 +1,8 @@ /*global defineSuite*/ defineSuite([ 'Scene/QuadtreePrimitive', + 'Core/Cartesian3', + 'Core/Cartographic', 'Core/defineProperties', 'Core/GeographicTilingScheme', 'Core/Visibility', @@ -9,6 +11,8 @@ defineSuite([ 'Specs/createFrameState' ], function( QuadtreePrimitive, + Cartesian3, + Cartographic, defineProperties, GeographicTilingScheme, Visibility, @@ -158,4 +162,87 @@ defineSuite([ expect(tile.state).not.toBe(QuadtreeTileLoadState.START); }); }); + + it('add and remove callbacks to tiles', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + tileProvider.computeDistanceToTile.and.returnValue(1e-15); + + // Load the root tiles. + tileProvider.loadTile.and.callFake(function(context, frameState, tile) { + tile.state = QuadtreeTileLoadState.DONE; + tile.renderable = true; + }); + + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); + + var removeFunc = quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(position) {}); + + quadtree.update(context, frameState, []); + + var addedCallback = false; + quadtree.forEachLoadedTile(function (tile) { + addedCallback = addedCallback || tile.customData.length > 0; + }); + + expect(addedCallback).toEqual(true); + + removeFunc(); + quadtree.update(context, frameState, []); + + var removedCallback = true; + quadtree.forEachLoadedTile(function (tile) { + removedCallback = removedCallback && tile.customData.length === 0; + }); + + expect(removedCallback).toEqual(true); + }); + + it('updates heights', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + tileProvider.computeDistanceToTile.and.returnValue(1e-15); + + tileProvider.terrainProvider = { + getTileDataAvailable : function() { + return true; + } + }; + + // Load the root tiles. + tileProvider.loadTile.and.callFake(function(context, frameState, tile) { + tile.state = QuadtreeTileLoadState.DONE; + tile.renderable = true; + }); + + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); + + var position = Cartesian3.clone(Cartesian3.ZERO); + var updatedPosition = Cartesian3.clone(Cartesian3.UNIT_X); + + quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(p) { + Cartesian3.clone(p, position); + }); + + quadtree.update(context, frameState, []); + expect(position).toEqual(Cartesian3.ZERO); + + quadtree.forEachLoadedTile(function (tile) { + tile.data = { + pick : function() { + return updatedPosition; + } + }; + }); + + quadtree.update(context, frameState, []); + + expect(position).toEqual(updatedPosition); + }); }); \ No newline at end of file