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 @@ + + +
+ + + + + +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