From 834aea0dd6726a2351eb5c7069f36f167fc3f2a0 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 6 Jun 2018 10:07:24 +1000 Subject: [PATCH 001/131] Start of BVH work. --- Source/Core/CesiumTerrainProvider.js | 43 +++++++++++++++++++++++-- Source/Core/QuantizedMeshTerrainData.js | 2 ++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index 6418e649a3c..eb9e1d475ed 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -56,6 +56,7 @@ define([ this.availability = layer.availability; this.hasVertexNormals = layer.hasVertexNormals; this.hasWaterMask = layer.hasWaterMask; + this.hasBvh = layer.hasBvh; this.littleEndianExtensionSize = layer.littleEndianExtensionSize; } @@ -71,6 +72,7 @@ define([ * @param {Resource|String|Promise|Promise} options.url The URL of the Cesium terrain server. * @param {Boolean} [options.requestVertexNormals=false] Flag that indicates if the client should request additional lighting information from the server, in the form of per vertex normals if available. * @param {Boolean} [options.requestWaterMask=false] Flag that indicates if the client should request per tile water masks from the server, if available. + * @param {Boolean} [options.requestBvh=true] Flag that indicates if the client should request bounding-volume hierarchy information along with tiles, if available. Using volume hierarchy information should significantly improve performance; there is little reason to disable it. * @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If not specified, the WGS84 ellipsoid is used. * @param {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas. * @@ -106,6 +108,7 @@ define([ this._heightmapStructure = undefined; this._hasWaterMask = false; this._hasVertexNormals = false; + this._hasBvh = false; /** * Boolean flag that indicates if the client should request vertex normals from the server. @@ -123,6 +126,15 @@ define([ */ this._requestWaterMask = defaultValue(options.requestWaterMask, false); + /** + * Boolean flag that indicates if the client should request tile bounding-volume hierarchy information + * from the server. + * @type {Boolean} + * @default true + * @private + */ + this._requestBvh = defaultValue(options.requestBvh, true); + this._errorEvent = new Event(); var credit = options.credit; @@ -192,6 +204,7 @@ define([ var hasVertexNormals = false; var hasWaterMask = false; + var hasBvh = false; var littleEndianExtensionSize = true; var isHeightmap = false; if (data.format === 'heightmap-1.0') { @@ -255,9 +268,13 @@ define([ if (defined(data.extensions) && data.extensions.indexOf('watermask') !== -1) { hasWaterMask = true; } + if (defined(data.extensions) && data.extensions.indexOf('bvh') !== -1) { + hasBvh = true; + } that._hasWaterMask = that._hasWaterMask || hasWaterMask; that._hasVertexNormals = that._hasVertexNormals || hasVertexNormals; + that._hasBvh = that._hasBvh || hasBvh; if (defined(data.attribution)) { if (attribution.length > 0) { attribution += ' '; @@ -273,6 +290,7 @@ define([ availability: availability, hasVertexNormals: hasVertexNormals, hasWaterMask: hasWaterMask, + hasBvh: hasBvh, littleEndianExtensionSize: littleEndianExtensionSize })); @@ -381,7 +399,15 @@ define([ * @constant * @default 2 */ - WATER_MASK: 2 + WATER_MASK: 2, + /** + * A bounding-volume hierarchy is included as an extension to the tile mesh + * + * @type {Number} + * @constant + * @default 3 + */ + BVH: 3 }; function getRequestHeader(extensionsList) { @@ -501,6 +527,8 @@ define([ var encodedNormalBuffer; var waterMaskBuffer; + var minimumHeights; + var maximumHeights; while (pos < view.byteLength) { var extensionId = view.getUint8(pos, true); pos += Uint8Array.BYTES_PER_ELEMENT; @@ -511,6 +539,12 @@ define([ encodedNormalBuffer = new Uint8Array(buffer, pos, vertexCount * 2); } else if (extensionId === QuantizedMeshExtensionIds.WATER_MASK && provider._requestWaterMask) { waterMaskBuffer = new Uint8Array(buffer, pos, extensionLength); + } else if (extensionId === QuantizedMeshExtensionIds.BVH && provider._requestBvh) { + var numberOfHeights = view.getUint32(pos, true); + pos += Uint32Array.BYTES_PER_ELEMENT; + minimumHeights = new Float32Array(buffer, pos, numberOfHeights); + pos += Float32Array.BYTES_PER_ELEMENT * numberOfHeights; + maximumHeights = new Float32Array(buffer, pos, numberOfHeights); } pos += extensionLength; } @@ -551,7 +585,9 @@ define([ northSkirtHeight : skirtHeight, childTileMask: provider.availability.computeChildMaskForTile(level, x, y), waterMask: waterMaskBuffer, - credits: provider._tileCredits + credits: provider._tileCredits, + minimumHeights: minimumHeights, + maximumHeights: maximumHeights }); } @@ -615,6 +651,9 @@ define([ if (this._requestWaterMask && layerToUse.hasWaterMask) { extensionList.push('watermask'); } + if (this._requestBvh && layerToUse.hasBvh) { + extensionList.push('bvh'); + } var headers; var query; diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index ed582ce1364..234faabc18f 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -77,6 +77,8 @@ define([ * @param {Uint8Array} [options.encodedNormals] The buffer containing per vertex normals, encoded using 'oct' encoding * @param {Uint8Array} [options.waterMask] The buffer containing the watermask. * @param {Credit[]} [options.credits] Array of credits for this tile. + * @param {Float32Array} [options.minimumHeights] The minimum heights of this tile and its descendants. + * @param {Float32Array} [options.maximumHeights] The maximum heights of this tile and its descendants. * * * @example From 34bbcc6551b67e0ec8841132718110172033774c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 14 Jun 2018 16:18:01 +1000 Subject: [PATCH 002/131] Start removing unnecessary upsampling. --- Source/Core/CesiumTerrainProvider.js | 23 ++- Source/Core/QuantizedMeshTerrainData.js | 4 +- Source/Scene/GlobeSurfaceTile.js | 214 ++---------------------- Source/Scene/QuadtreeTile.js | 6 +- Source/Scene/TileTerrain.js | 34 +++- terrainnotes.md | 76 +++++++++ 6 files changed, 141 insertions(+), 216 deletions(-) create mode 100644 terrainnotes.md diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index eb9e1d475ed..e4965f5f438 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -527,8 +527,7 @@ define([ var encodedNormalBuffer; var waterMaskBuffer; - var minimumHeights; - var maximumHeights; + var bvh; while (pos < view.byteLength) { var extensionId = view.getUint8(pos, true); pos += Uint8Array.BYTES_PER_ELEMENT; @@ -540,11 +539,18 @@ define([ } else if (extensionId === QuantizedMeshExtensionIds.WATER_MASK && provider._requestWaterMask) { waterMaskBuffer = new Uint8Array(buffer, pos, extensionLength); } else if (extensionId === QuantizedMeshExtensionIds.BVH && provider._requestBvh) { - var numberOfHeights = view.getUint32(pos, true); - pos += Uint32Array.BYTES_PER_ELEMENT; - minimumHeights = new Float32Array(buffer, pos, numberOfHeights); - pos += Float32Array.BYTES_PER_ELEMENT * numberOfHeights; - maximumHeights = new Float32Array(buffer, pos, numberOfHeights); + var extensionPos = pos; + + // Align to 4 bytes. + if (extensionPos % 4 !== 0) { + extensionPos += (4 - (extensionPos % 4)); + } + + var numberOfHeights = view.getUint32(extensionPos, true); + extensionPos += Uint32Array.BYTES_PER_ELEMENT; + + bvh = new Float32Array(buffer, extensionPos, numberOfHeights); + extensionPos += Float32Array.BYTES_PER_ELEMENT * numberOfHeights; } pos += extensionLength; } @@ -586,8 +592,7 @@ define([ childTileMask: provider.availability.computeChildMaskForTile(level, x, y), waterMask: waterMaskBuffer, credits: provider._tileCredits, - minimumHeights: minimumHeights, - maximumHeights: maximumHeights + bvh: bvh }); } diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index 234faabc18f..f86cc8c6816 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -77,8 +77,7 @@ define([ * @param {Uint8Array} [options.encodedNormals] The buffer containing per vertex normals, encoded using 'oct' encoding * @param {Uint8Array} [options.waterMask] The buffer containing the watermask. * @param {Credit[]} [options.credits] Array of credits for this tile. - * @param {Float32Array} [options.minimumHeights] The minimum heights of this tile and its descendants. - * @param {Float32Array} [options.maximumHeights] The maximum heights of this tile and its descendants. + * @param {Float32Array} [options.bvh] The bounding-volume hierarchy for this tile and its descendents. TODO: describe its structure * * * @example @@ -168,6 +167,7 @@ define([ this._orientedBoundingBox = options.orientedBoundingBox; this._horizonOcclusionPoint = options.horizonOcclusionPoint; this._credits = options.credits; + this._bvh = options.bvh; var vertexCount = this._quantizedVertices.length / 3; var uValues = this._uValues = this._quantizedVertices.subarray(0, vertexCount); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 46bba1be510..498ac4a054c 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -68,13 +68,11 @@ define([ this.minimumHeight = 0.0; this.maximumHeight = 0.0; this.boundingSphere3D = new BoundingSphere(); - this.boundingSphere2D = new BoundingSphere(); this.orientedBoundingBox = undefined; this.tileBoundingRegion = undefined; this.occludeePointInScaledSpace = new Cartesian3(); this.loadedTerrain = undefined; - this.upsampledTerrain = undefined; this.pickBoundingSphere = new BoundingSphere(); this.pickTerrain = undefined; @@ -101,11 +99,7 @@ define([ var loadingIsTransitioning = defined(loadedTerrain) && (loadedTerrain.state === TerrainState.RECEIVING || loadedTerrain.state === TerrainState.TRANSFORMING); - var upsampledTerrain = this.upsampledTerrain; - var upsamplingIsTransitioning = defined(upsampledTerrain) && - (upsampledTerrain.state === TerrainState.RECEIVING || upsampledTerrain.state === TerrainState.TRANSFORMING); - - var shouldRemoveTile = !loadingIsTransitioning && !upsamplingIsTransitioning; + var shouldRemoveTile = !loadingIsTransitioning; var imagery = this.imagery; for (var i = 0, len = imagery.length; shouldRemoveTile && i < len; ++i) { @@ -186,11 +180,6 @@ define([ this.loadedTerrain = undefined; } - if (defined(this.upsampledTerrain)) { - this.upsampledTerrain.freeResources(); - this.upsampledTerrain = undefined; - } - if (defined(this.pickTerrain)) { this.pickTerrain.freeResources(); this.pickTerrain = undefined; @@ -285,7 +274,7 @@ define([ var isRenderable = defined(surfaceTile.vertexArray); // But it's not done loading until our two state machines are terminated. - var isDoneLoading = !defined(surfaceTile.loadedTerrain) && !defined(surfaceTile.upsampledTerrain); + var isDoneLoading = !defined(surfaceTile.loadedTerrain); // If this tile's terrain and imagery are just upsampled from its parent, mark the tile as // upsampled only. We won't refine a tile if its four children are upsampled only. @@ -356,13 +345,16 @@ define([ function prepareNewTile(tile, terrainProvider, imageryLayerCollection) { var surfaceTile = tile.data; - var upsampleTileDetails = getUpsampleTileDetails(tile); - if (defined(upsampleTileDetails)) { - surfaceTile.upsampledTerrain = new TileTerrain(upsampleTileDetails); - } + surfaceTile.loadedTerrain = new TileTerrain(); - if (isDataAvailable(tile, terrainProvider)) { - surfaceTile.loadedTerrain = new TileTerrain(); + if (tile.parent && tile.parent.terrainData && !tile.parent.terrainData.isChildAvailable(tile.parent.x, tile.parent.y, tile.x, tile.y)) { + // Start upsampling right away. + surfaceTile.loadedTerrain.state = TerrainState.FAILED; + } else if (terrainProvider.getTileDataAvailable) { + if (!terrainProvider.getTileDataAvailable(tile.x, tile.y, tile.level)) { + // Start upsampling right away. + surfaceTile.loadedTerrain.state = TerrainState.FAILED; + } } // Map imagery tiles to this terrain tile @@ -377,11 +369,9 @@ define([ function processTerrainStateMachine(tile, frameState, terrainProvider, vertexArraysToDestroy) { var surfaceTile = tile.data; var loaded = surfaceTile.loadedTerrain; - var upsampled = surfaceTile.upsampledTerrain; - var suspendUpsampling = false; if (defined(loaded)) { - loaded.processLoadStateMachine(frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); + loaded.processLoadStateMachine(tile, frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); // Publish the terrain data on the tile as soon as it is available. // We'll potentially need it to upsample child tiles. @@ -392,10 +382,7 @@ define([ // If there's a water mask included in the terrain data, create a // texture for it. createWaterMaskTextureIfNeeded(frameState.context, surfaceTile); - - propagateNewLoadedDataToChildren(tile); } - suspendUpsampling = true; } if (loaded.state === TerrainState.READY) { @@ -411,185 +398,10 @@ define([ loaded.vertexArray = undefined; // No further loading or upsampling is necessary. - surfaceTile.pickTerrain = defaultValue(surfaceTile.loadedTerrain, surfaceTile.upsampledTerrain); - surfaceTile.loadedTerrain = undefined; - surfaceTile.upsampledTerrain = undefined; - } else if (loaded.state === TerrainState.FAILED) { - // Loading failed for some reason, or data is simply not available, - // so no need to continue trying to load. Any retrying will happen before we - // reach this point. + surfaceTile.pickTerrain = surfaceTile.loadedTerrain; surfaceTile.loadedTerrain = undefined; } } - - if (!suspendUpsampling && defined(upsampled)) { - upsampled.processUpsampleStateMachine(frameState, terrainProvider, tile.x, tile.y, tile.level); - - // Publish the terrain data on the tile as soon as it is available. - // We'll potentially need it to upsample child tiles. - // It's safe to overwrite terrainData because we won't get here after - // loaded terrain data has been received. - if (upsampled.state >= TerrainState.RECEIVED) { - if (surfaceTile.terrainData !== upsampled.data) { - surfaceTile.terrainData = upsampled.data; - - // If the terrain provider has a water mask, "upsample" that as well - // by computing texture translation and scale. - if (terrainProvider.hasWaterMask) { - upsampleWaterMask(tile); - } - - propagateNewUpsampledDataToChildren(tile); - } - } - - if (upsampled.state === TerrainState.READY) { - upsampled.publishToTile(tile); - - if (defined(tile.data.vertexArray)) { - // Free the tiles existing vertex array on next render. - vertexArraysToDestroy.push(tile.data.vertexArray); - } - - // Transfer ownership of the vertex array to the tile itself. - tile.data.vertexArray = upsampled.vertexArray; - upsampled.vertexArray = undefined; - - // No further upsampling is necessary. We need to continue loading, though. - surfaceTile.pickTerrain = surfaceTile.upsampledTerrain; - surfaceTile.upsampledTerrain = undefined; - } else if (upsampled.state === TerrainState.FAILED) { - // Upsampling failed for some reason. This is pretty much a catastrophic failure, - // but maybe we'll be saved by loading. - surfaceTile.upsampledTerrain = undefined; - } - } - } - - function getUpsampleTileDetails(tile) { - // Find the nearest ancestor with loaded terrain. - var sourceTile = tile.parent; - while (defined(sourceTile) && defined(sourceTile.data) && !defined(sourceTile.data.terrainData)) { - sourceTile = sourceTile.parent; - } - - if (!defined(sourceTile) || !defined(sourceTile.data)) { - // No ancestors have loaded terrain - try again later. - return undefined; - } - - return { - data : sourceTile.data.terrainData, - x : sourceTile.x, - y : sourceTile.y, - level : sourceTile.level - }; - } - - function propagateNewUpsampledDataToChildren(tile) { - // Now that there's new data for this tile: - // - child tiles that were previously upsampled need to be re-upsampled based on the new data. - - // Generally this is only necessary when a child tile is upsampled, and then one - // of its ancestors receives new (better) data and we want to re-upsample from the - // new data. - - propagateNewUpsampledDataToChild(tile, tile._southwestChild); - propagateNewUpsampledDataToChild(tile, tile._southeastChild); - propagateNewUpsampledDataToChild(tile, tile._northwestChild); - propagateNewUpsampledDataToChild(tile, tile._northeastChild); - } - - function propagateNewUpsampledDataToChild(tile, childTile) { - if (defined(childTile) && childTile.state !== QuadtreeTileLoadState.START) { - var childSurfaceTile = childTile.data; - if (defined(childSurfaceTile.terrainData) && !childSurfaceTile.terrainData.wasCreatedByUpsampling()) { - // Data for the child tile has already been loaded. - return; - } - - // Restart the upsampling process, no matter its current state. - // We create a new instance rather than just restarting the existing one - // because there could be an asynchronous operation pending on the existing one. - if (defined(childSurfaceTile.upsampledTerrain)) { - childSurfaceTile.upsampledTerrain.freeResources(); - } - childSurfaceTile.upsampledTerrain = new TileTerrain({ - data : tile.data.terrainData, - x : tile.x, - y : tile.y, - level : tile.level - }); - - childTile.state = QuadtreeTileLoadState.LOADING; - } - } - - function propagateNewLoadedDataToChildren(tile) { - var surfaceTile = tile.data; - - // Now that there's new data for this tile: - // - child tiles that were previously upsampled need to be re-upsampled based on the new data. - // - child tiles that were previously deemed unavailable may now be available. - - propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.southwestChild); - propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.southeastChild); - propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.northwestChild); - propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.northeastChild); - } - - function propagateNewLoadedDataToChildTile(tile, surfaceTile, childTile) { - if (childTile.state !== QuadtreeTileLoadState.START) { - var childSurfaceTile = childTile.data; - if (defined(childSurfaceTile.terrainData) && !childSurfaceTile.terrainData.wasCreatedByUpsampling()) { - // Data for the child tile has already been loaded. - return; - } - - // Restart the upsampling process, no matter its current state. - // We create a new instance rather than just restarting the existing one - // because there could be an asynchronous operation pending on the existing one. - if (defined(childSurfaceTile.upsampledTerrain)) { - childSurfaceTile.upsampledTerrain.freeResources(); - } - childSurfaceTile.upsampledTerrain = new TileTerrain({ - data : surfaceTile.terrainData, - x : tile.x, - y : tile.y, - level : tile.level - }); - - if (surfaceTile.terrainData.isChildAvailable(tile.x, tile.y, childTile.x, childTile.y)) { - // Data is available for the child now. It might have been before, too. - if (!defined(childSurfaceTile.loadedTerrain)) { - // No load process is in progress, so start one. - childSurfaceTile.loadedTerrain = new TileTerrain(); - } - } - - childTile.state = QuadtreeTileLoadState.LOADING; - } - } - - function isDataAvailable(tile, terrainProvider) { - var tileDataAvailable = terrainProvider.getTileDataAvailable(tile.x, tile.y, tile.level); - if (defined(tileDataAvailable)) { - return tileDataAvailable; - } - - var parent = tile.parent; - if (!defined(parent)) { - // Data is assumed to be available for root tiles. - return true; - } - - if (!defined(parent.data) || !defined(parent.data.terrainData)) { - // Parent tile data is not yet received or upsampled, so assume (for now) that this - // child tile is not available. - return false; - } - - return parent.data.terrainData.isChildAvailable(parent.x, parent.y, tile.x, tile.y); } function getContextWaterMaskData(context) { diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index d3fe85700bd..8bf5e88b065 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -57,9 +57,9 @@ define([ this._northwestChild = undefined; this._northeastChild = undefined; - // QuadtreeTileReplacementQueue gets/sets these private properties. - this._replacementPrevious = undefined; - this._replacementNext = undefined; + // TileReplacementQueue gets/sets these private properties. + this.replacementPrevious = undefined; + this.replacementNext = undefined; // The distance from the camera to this tile, updated when the tile is selected // for rendering. We can get rid of this if we have a better way to sort by diff --git a/Source/Scene/TileTerrain.js b/Source/Scene/TileTerrain.js index 450590d70c7..425fb575e44 100644 --- a/Source/Scene/TileTerrain.js +++ b/Source/Scene/TileTerrain.js @@ -92,7 +92,11 @@ define([ tile.data.occludeePointInScaledSpace = Cartesian3.clone(mesh.occludeePointInScaledSpace, surfaceTile.occludeePointInScaledSpace); }; - TileTerrain.prototype.processLoadStateMachine = function(frameState, terrainProvider, x, y, level, priorityFunction) { + TileTerrain.prototype.processLoadStateMachine = function(tile, frameState, terrainProvider, x, y, level, priorityFunction) { + if (this.state === TerrainState.FAILED) { + upsample(this, tile, frameState, terrainProvider, x, y, level, priorityFunction) + } + if (this.state === TerrainState.UNLOADED) { requestTileGeometry(this, terrainProvider, x, y, level, priorityFunction); } @@ -106,6 +110,34 @@ define([ } }; + function upsample(tileTerrain, tile, frameState, terrainProvider, x, y, level, priorityFunction) { + var parent = tile.parent; + if (!parent) { + // Trying to upsample from a root tile. No can do. + return; + } + + var sourceData = parent.data.terrainData; + var sourceX = parent.x; + var sourceY = parent.y; + var sourceLevel = parent.level; + + tileTerrain.data = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level); + if (!defined(tileTerrain.data)) { + // The upsample request has been deferred - try again later. + return; + } + + tileTerrain.state = TerrainState.RECEIVING; + + when(tileTerrain.data, function(terrainData) { + tileTerrain.data = terrainData; + tileTerrain.state = TerrainState.RECEIVED; + }, function() { + tileTerrain.state = TerrainState.FAILED; + }); + } + function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, priorityFunction) { function success(terrainData) { tileTerrain.data = terrainData; diff --git a/terrainnotes.md b/terrainnotes.md new file mode 100644 index 00000000000..4c8a337eb3c --- /dev/null +++ b/terrainnotes.md @@ -0,0 +1,76 @@ +To refine past a tile, one of the following conditions must be met: + + * The tile is loaded. + * The tile has a SSE that is _known_ (not just suspected) to be too high AND we know which of the tile's children exist. We'll know the SSE is too high if: + * The distance to the tile is known accurately (e.g. we know the precise min/max height), or + * The bounding volume for the tile is known to be X or _closer_, and X is sufficient to necessitate refinement. For example, if we have no min/max height information for this tile, but the parent tile has a min of A and a max of B, we know the child must fall within these bounds as well. Therefore, the child tile can be no farther away than max(distance to A, distance to B). + + +GlobeSurfaceTile properties: + + * `tileBoundingRegion` is used to estimate the distance to the tile. It is _not_ used for culling. + * `orientedBoundingBox` is used for culling, _not_ for distance estimation. + * `minimumHeight` and `maximumheight` are used for picking, for culling in 2D, and for `updateHeights` in `QuadtreePrimitive`. + + +A tile conceptually has three sets of min/max heights: + + * The spatially coherent min/max of the real terrain in this horizontal extent, i.e. the min/max of this tile and all its descendants. + * Distance estimation: Great! + * Culling for rendering: Great! + * Skipping loading: Great! + * The min/max of the geometry, which may not reflect the full range of heights of descendants. + * Distance estimation: Not perfect, but should be good enough. + * Culling for rendering: Great! + * Skipping loading: Not perfect, but should be good enough. + * The (spatially coherent) min/max of the parent tile, applied to this tile. + * Distance estimation: Tile is guaranteed to be closer than the farther-away height surface (min or max). + * Culling for rendering: If test says its invisible, it definitely is. If test says its visible, it may or may not be. + * Skipping loading: We can skip loading if the tile is culled. + + + +## Tile properties + +* Available from construction: + * level + * x + * y + * rectangle + * tilingScheme + * parent + * southwestChild + * southeastChild + * northwestChild + * northeastChild + * state +* Populated by loading or upsampling + * vertexArray (rendering) + * center (rendering vertexArray RTC) + * imagery (rendering) + * waterMaskTexture (rendering) + * waterMaskTranslationAndScale (rendering) + * orientedBoundingBox (culling) + * occludeePointInScaledSpace (culling) + * boundingSphere3D (culling, when orientedBoundingBox isn't available) + * terrainData (used to upsample children, determine child tile mask) + * upsampledFromParent (true if terrain and all imagery are just upsampled from the parent) + * renderable (true if the tile is renderable at all) +* Populated by non-tile loading + * childTileMask / getTileDataAvailable (not stored on tile directly, comes from terrainData.childTileMask or TerrainProvider.getTileDataAvailable, determines if we need to upsample) + * tileBoundingRegion (distance estimation for SSE, load priority) +* Caching / per-frame intermediates + * surfaceShader (shader used to render this tile last frame) + * _distance (distance from the camera to this tile, set as a side-effect of GlobeSurfaceTileProvider#computeTileVisibility) + * isClipped (true if the tile is clipped by a custom clipping plane, set as a side-effect of GlobeSurfaceTileProvider#computeTileVisibility) + * replacementPrevious / replacementNext (This tile's position in the TileReplacementQueue linked list) +* Probably not needed + * pickBoundingSphere (assigned and used in Globe#pick, not clear why this needs to be a tile property) + * pickTerrain (points to either the loaded or upsampled TileTerrain instance) + * minimumHeight / maximumHeight (same as the min/max height in tileBoundingRegion?) +* Not sure + * _priorityFunction (used to prioritize requests, returns the tile's distance to the tileBoundingRegion) + * _customData (custom data that is inside the bounds of this tile) + * _frameUpdated (used in QuadtreeTile#_updateCustomData to determine when a parent tile was updated more (?) recently than this tile) + * _frameRendered (used in QuadtreePrimitive#createRenderCommandsForSelectedTiles to determine which tiles need updated heights) + * _loadedCallbacks (used for WMTS (?) to remove old imagery once new imagery is loaded. I think.) From 88d0fdcd2958adf4524fb2be8c80aa514b7bbad6 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 16 Jun 2018 22:35:39 +1000 Subject: [PATCH 003/131] Compute the bounding volume (for culling) as needed. --- Source/Core/QuantizedMeshTerrainData.js | 11 ++ Source/Scene/GlobeSurfaceTile.js | 34 ++++++ Source/Scene/GlobeSurfaceTileProvider.js | 141 +++++++++++++++++++++++ Source/Scene/QuadtreePrimitive.js | 6 +- Source/Scene/TileBoundingRegion.js | 10 +- terrainnotes.md | 5 + 6 files changed, 201 insertions(+), 6 deletions(-) diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index f86cc8c6816..ecf9d65fc60 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -223,6 +223,17 @@ define([ get : function() { return this._waterMask; } + }, + + /** + * Gets the bounding-volume hierarchy (BVH) starting with this tile. + * @memberof QuantizedMeshTerrainData.prototype + * @type {Float32Array} + */ + bvh : { + get : function() { + return this._bvh; + } } }); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 498ac4a054c..0a584fff31e 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -69,6 +69,16 @@ define([ this.maximumHeight = 0.0; this.boundingSphere3D = new BoundingSphere(); this.orientedBoundingBox = undefined; + this.boundingVolumeSourceTile = undefined; + this._bvh = undefined; + + /** + * A bounding region used to estimate distance to the tile. The horizontal bounds are always tight-fitting, + * but the `minimumHeight` and `maximumHeight` properties may be derived from the min/max of an ancestor tile + * and be quite loose-fitting and thus very poor for estimating distance. The {@link TileBoundingRegion#boundingVolume} + * and {@link TileBoundingRegion#boundingSphere} will always be undefined; tiles store these separately. + * @type {TileBoundingRegion} + */ this.tileBoundingRegion = undefined; this.occludeePointInScaledSpace = new Cartesian3(); @@ -112,6 +122,29 @@ define([ } }); + GlobeSurfaceTile.prototype.getBvh = function(tile, terrainProvider) { + if (this._bvh === undefined) { + var terrainData = this.terrainData; + if (terrainData !== undefined && terrainData.bvh !== undefined) { + this._bvh = terrainData.bvh; + } + + var parent = tile.parent; + if (parent !== undefined && parent.data !== undefined) { + var parentBvh = parent.data.getBvh(parent, terrainProvider); + if (parentBvh !== undefined && parentBvh.length > 2) { + var subsetLength = (parentBvh.length - 2) / 4; + // TODO: Cesium tiles are numbered from the North aren't there? This code is assuming from the South. + var childIndex = ((tile.y % 2) === 0 ? 0 : 2) + ((tile.x % 2) === 0 ? 0 : 1); + var start = 2 + subsetLength * childIndex; + this._bvh = parentBvh.subarray(start, start + subsetLength); + } + } + } + + return this._bvh; +}; + function getPosition(encoding, mode, projection, vertices, index, result) { encoding.decodePosition(vertices, index, result); @@ -234,6 +267,7 @@ define([ maximumHeight = tile.parent.data.maximumHeight; } return new TileBoundingRegion({ + computeBoundingVolumes : false, rectangle : tile.rectangle, ellipsoid : tile.tilingScheme.ellipsoid, minimumHeight : minimumHeight, diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 0d194c54572..ee168a5b7e3 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -38,6 +38,7 @@ define([ '../Scene/DepthFunction', '../Scene/PerInstanceColorAppearance', '../Scene/Primitive', + '../Scene/TileBoundingRegion', './ClippingPlaneCollection', './GlobeSurfaceTile', './ImageryLayer', @@ -84,6 +85,7 @@ define([ DepthFunction, PerInstanceColorAppearance, Primitive, + TileBoundingRegion, ClippingPlaneCollection, GlobeSurfaceTile, ImageryLayer, @@ -522,6 +524,9 @@ define([ * @returns {Visibility} The visibility of the tile. */ GlobeSurfaceTileProvider.prototype.computeTileVisibility = function(tile, frameState, occluders) { + // TODO: this function is now called before the bounding volume and occludee point + // are initialized, so the visibility test is rubbish. 😳 + var distance = this.computeDistanceToTile(tile, frameState); tile._distance = distance; @@ -533,6 +538,30 @@ define([ } var surfaceTile = tile.data; + + // We now need a bounding volume in order to determine visibility, but the tile + // may not be loaded yet. + if (surfaceTile.boundingVolumeSourceTile !== tile) { + var heightSource = updateTileBoundingRegion(tile, this.terrainProvider); + if (heightSource === undefined) { + // We have no idea where this tile is, so let's just call it visible. + return Visibility.PARTIAL; + } + + if (heightSource !== surfaceTile.boundingVolumeSourceTile) { + // Heights are from a new source tile, so update the bounding volume. + surfaceTile.boundingVolumeSourceTile = heightSource; + var tileBoundingRegion = surfaceTile.tileBoundingRegion; + surfaceTile.orientedBoundingBox = OrientedBoundingBox.fromRectangle( + tile.rectangle, + tileBoundingRegion.minimumHeight, + tileBoundingRegion.maximumHeight, + tile.tilingScheme.ellipsoid, + surfaceTile.orientedBoundingBox); + surfaceTile.boundingSphere3D = undefined; // TODO: compute the bounding sphere for real, or get rid of it entirely. + } + } + var cullingVolume = frameState.cullingVolume; var boundingVolume = defaultValue(surfaceTile.orientedBoundingBox, surfaceTile.boundingSphere3D); @@ -577,6 +606,18 @@ define([ return intersection; }; + /** + * Determines if the given tile can be refined + * @param {QuadtreeTile} tile The tile to check. + * @returns {boolean} True if the tile can be refined, false if it cannot. + */ + GlobeSurfaceTileProvider.prototype.canRefine = function(tile) { + //return tile.southwestChild.renderable && tile.southeastChild.renderable && tile.northwestChild.renderable && tile.northeastChild.renderable; + // TODO: only allow refinement if we know if we know whether or not the children are available. + // TODO: do we need to do anything to limit the upsampled depth? + return true; + }; + var modifiedModelViewScratch = new Matrix4(); var modifiedModelViewProjectionScratch = new Matrix4(); var tileRectangleScratch = new Cartesian4(); @@ -625,11 +666,111 @@ define([ * @returns {Number} The distance from the camera to the closest point on the tile, in meters. */ GlobeSurfaceTileProvider.prototype.computeDistanceToTile = function(tile, frameState) { + // The distance should be: + // 1. the actual distance to the tight-fitting bounding volume, or + // 2. a distance that is equal to or greater than the actual distance to the tight-fitting bounding volume. + // + // When we don't know the min/max heights for a tile, but we do know the min/max of an ancestor tile, we can + // build a tight-fitting bounding volume horizontally, but not vertically. The min/max heights from the + // ancestor will likely form a volume that is much bigger than it needs to be. This means that the volume may + // be deemed to be much closer to the camera than it really is, causing us to select tiles that are too detailed. + // Loading too-detailed tiles is super expensive, so we don't want to do that. We don't know where the child + // tile really lies within the parent range of heights, but we _do_ know the child tile can't be any closer than + // the ancestor height surface (min or max) that is _farthest away_ from the camera. So if we computed distance + // based that conservative metric, we may end up loading tiles that are not detailed enough, but that's much + // better (faster) than loading tiles that are too detailed. + + var heightSource = updateTileBoundingRegion(tile, this.terrainProvider); + var surfaceTile = tile.data; var tileBoundingRegion = surfaceTile.tileBoundingRegion; + + if (heightSource !== tile) { + if (heightSource === undefined) { + // Can't find any min/max heights anywhere? Ok, let's just say the + // tile is really far away so we'll load and render it rather than + // refining. + return 9999999999.0; + } + + // Make the bounding region a pancake located at the farthest-away + // ancestor height surface. + var ancestorMin = tileBoundingRegion.minimumHeight; + var ancestorMax = tileBoundingRegion.maximumHeight; + + var cameraHeight = frameState.camera.positionCartographic.height; + if (Math.abs(cameraHeight - ancestorMin) < Math.abs(cameraHeight - ancestorMax)) { + tileBoundingRegion.minimumHeight = ancestorMin; + tileBoundingRegion.maximumHeight = ancestorMin; + } else { + tileBoundingRegion.minimumHeight = ancestorMax; + tileBoundingRegion.maximumHeight = ancestorMax; + } + } + return tileBoundingRegion.distanceToCamera(frameState); }; + function updateTileBoundingRegion(tile, terrainProvider) { + var surfaceTile = tile.data; + if (surfaceTile === undefined) { + surfaceTile = tile.data = new GlobeSurfaceTile(); + surfaceTile.tileBoundingRegion = new TileBoundingRegion({ + computeBoundingVolumes : false, + rectangle : tile.rectangle, + ellipsoid : tile.tilingScheme.ellipsoid, + minimumHeight : 0.0, + maximumHeight : 0.0 + }); + } + + var terrainData = surfaceTile.terrainData; + var tileBoundingRegion = surfaceTile.tileBoundingRegion; + + if (terrainData !== undefined && terrainData._minimumHeight !== undefined && terrainData._maximumHeight !== undefined) { + // We have tight-fitting min/max heights from the geometry. + // TODO: HeightmapTerrainData won't have min/max properties, but the TerrainMesh will. + tileBoundingRegion.minimumHeight = terrainData._minimumHeight; + tileBoundingRegion.maximumHeight = terrainData._maximumHeight; + return tile; + } + + var bvh = surfaceTile.getBvh(tile, terrainProvider.terrainProvider); + if (bvh !== undefined && bvh[0] === bvh[0] && bvh[1] === bvh[1]) { + // Have a BVH that covers this tile and the heights are not NaN. + tileBoundingRegion.minimumHeight = bvh[0]; + tileBoundingRegion.maximumHeight = bvh[1]; + return tile; + } + + // No accurate BVH data available, so we're stuck with min/max heights from an ancestor tile. + tileBoundingRegion.minimumHeight = Number.NaN; + tileBoundingRegion.maximumHeight = Number.NaN; + + var ancestor = tile.parent; + while (ancestor !== undefined) { + var ancestorSurfaceTile = ancestor.data; + if (ancestorSurfaceTile !== undefined) { + var ancestorTerrainData = ancestorSurfaceTile.terrainData; + if (ancestorTerrainData !== undefined && ancestorTerrainData._minimumHeight !== undefined && ancestorTerrainData._maximumHeight !== undefined) { + // TODO: HeightmapTerrainData won't have min/max properties, but the TerrainMesh will. + tileBoundingRegion.minimumHeight = ancestorTerrainData._minimumHeight; + tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight; + return ancestor; + } + + var ancestorBvh = ancestorSurfaceTile._bvh; + if (ancestorBvh !== undefined) { + tileBoundingRegion.minimumHeight = bvh[0]; + tileBoundingRegion.maximumHeight = bvh[1]; + return ancestor; + } + } + } + + return undefined; + } + /** * Returns true if this object was destroyed; otherwise, false. *

diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index c0b44e083d0..1457a5d63cd 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -558,12 +558,12 @@ define([ var southeastChild = tile.southeastChild; var northwestChild = tile.northwestChild; var northeastChild = tile.northeastChild; - var allAreRenderable = southwestChild.renderable && southeastChild.renderable && - northwestChild.renderable && northeastChild.renderable; var allAreUpsampled = southwestChild.upsampledFromParent && southeastChild.upsampledFromParent && northwestChild.upsampledFromParent && northeastChild.upsampledFromParent; - if (allAreRenderable) { + var tileProvider = primitive.tileProvider; + + if (tileProvider.canRefine(tile)) { if (allAreUpsampled) { // No point in rendering the children because they're all upsampled. Render this tile instead. addTileToRenderList(primitive, tile); diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js index 48ac2003f72..aa7a070c119 100644 --- a/Source/Scene/TileBoundingRegion.js +++ b/Source/Scene/TileBoundingRegion.js @@ -50,6 +50,8 @@ define([ * @param {Number} [options.minimumHeight=0.0] The minimum height of the region. * @param {Number} [options.maximumHeight=0.0] The maximum height of the region. * @param {Ellipsoid} [options.ellipsoid=Cesium.Ellipsoid.WGS84] The ellipsoid. + * @param {Boolean} [options.computeBoundingVolumes=true] True to compute the {@link TileBoundingRegion#boundingVolume} and + * {@link TileBoundingVolume#boundingSphere}. If false, these properties will be undefined. * * @private */ @@ -122,10 +124,12 @@ define([ var ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); computeBox(this, options.rectangle, ellipsoid); - // An oriented bounding box that encloses this tile's region. This is used to calculate tile visibility. - this._orientedBoundingBox = OrientedBoundingBox.fromRectangle(this.rectangle, this.minimumHeight, this.maximumHeight, ellipsoid); + if (defaultValue(options.computeBoundingVolumes, true)) { + // An oriented bounding box that encloses this tile's region. This is used to calculate tile visibility. + this._orientedBoundingBox = OrientedBoundingBox.fromRectangle(this.rectangle, this.minimumHeight, this.maximumHeight, ellipsoid); - this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(this._orientedBoundingBox); + this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(this._orientedBoundingBox); + } } defineProperties(TileBoundingRegion.prototype, { diff --git a/terrainnotes.md b/terrainnotes.md index 4c8a337eb3c..2ce36ffb28c 100644 --- a/terrainnotes.md +++ b/terrainnotes.md @@ -74,3 +74,8 @@ A tile conceptually has three sets of min/max heights: * _frameUpdated (used in QuadtreeTile#_updateCustomData to determine when a parent tile was updated more (?) recently than this tile) * _frameRendered (used in QuadtreePrimitive#createRenderCommandsForSelectedTiles to determine which tiles need updated heights) * _loadedCallbacks (used for WMTS (?) to remove old imagery once new imagery is loaded. I think.) + + +TileBoundingRegion has a both an oriented bounding box and a bounding sphere, but we don't use either of them for terrain. +Instead, tiles store separate copies of these things (orientingBoundingBox and boundingSphere3D). That's probably good because we update the min/max height in publishToTile but we don't update either of the other two. + From d51bc735a66e45e7ba365a948e5809cb002b25ff Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 18 Jun 2018 18:08:58 +1000 Subject: [PATCH 004/131] Make terrain rendering mostly work again. --- Source/Scene/GlobeSurfaceShaderSet.js | 9 ++++- Source/Scene/GlobeSurfaceTileProvider.js | 49 +++++++++++++++++++++--- Source/Scene/TileTerrain.js | 5 +++ Source/Shaders/GlobeFS.glsl | 14 +++++++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 04756584f28..c53c421911f 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -66,7 +66,7 @@ define([ return useWebMercatorProjection ? get2DYPositionFractionMercatorProjection : get2DYPositionFractionGeographicProjection; } - GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, clippingPlanes) { + GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, clippingPlanes, renderPartialTile) { var quantization = 0; var quantizationDefine = ''; @@ -93,7 +93,8 @@ define([ (enableFog << 13) | (quantization << 14) | (applySplit << 15) | - (enableClippingPlanes << 16); + (enableClippingPlanes << 16) | + (renderPartialTile << 17); var currentClippingShaderState = 0; if (defined(clippingPlanes)) { @@ -180,6 +181,10 @@ define([ fs.defines.push('ENABLE_CLIPPING_PLANES'); } + if (renderPartialTile) { + fs.defines.push('RENDER_PARTIAL_TILE'); + } + var computeDayColor = '\ vec4 computeDayColor(vec4 initialColor, vec3 textureCoordinates)\n\ {\n\ diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index ee168a5b7e3..9a2d6cb1a12 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -559,6 +559,7 @@ define([ tile.tilingScheme.ellipsoid, surfaceTile.orientedBoundingBox); surfaceTile.boundingSphere3D = undefined; // TODO: compute the bounding sphere for real, or get rid of it entirely. + surfaceTile.occludeePointInScaledSpace = undefined; // TODO } } @@ -760,12 +761,13 @@ define([ } var ancestorBvh = ancestorSurfaceTile._bvh; - if (ancestorBvh !== undefined) { - tileBoundingRegion.minimumHeight = bvh[0]; - tileBoundingRegion.maximumHeight = bvh[1]; + if (ancestorBvh !== undefined && ancestorBvh[0] === ancestorBvh[0] && ancestorBvh[1] === ancestorBvh[1]) { + tileBoundingRegion.minimumHeight = ancestorBvh[0]; + tileBoundingRegion.maximumHeight = ancestorBvh[1]; return ancestor; } } + ancestor = ancestor.parent; } return undefined; @@ -1074,6 +1076,9 @@ define([ u_minimumBrightness : function() { return frameState.fog.minimumBrightness; }, + u_textureCoordinateSubset : function() { + return this.textureCoordinateSubset; + }, // make a separate object so that changes to the properties are seen on // derived commands that combine another uniform map with this one. @@ -1110,7 +1115,8 @@ define([ minMaxHeight : new Cartesian2(), scaleAndBias : new Matrix4(), clippingPlanesEdgeColor : Color.clone(Color.WHITE), - clippingPlanesEdgeWidth : 0.0 + clippingPlanesEdgeWidth : 0.0, + textureCoordinateSubset : new Cartesian4() } }; @@ -1233,8 +1239,35 @@ define([ })(); var otherPassesInitialColor = new Cartesian4(0.0, 0.0, 0.0, 0.0); + var ancestorSubsetScratch = new Cartesian4(); + + function addDrawCommandsForTile(tileProvider, tile, frameState, subset) { + if (!tile.renderable) { + // We can't render this tile yet, so instead render a subset of our closest renderable ancestor. + var ancestor = tile.parent; + while (ancestor !== undefined && !ancestor.renderable) { + ancestor = ancestor.parent; + } + + if (ancestor === undefined) { + // Can't find any renderable ancestor. This shouldn't happen because we + // don't start tile selection at all until the root tiles are loaded. + return; + } + + var myRectangle = tile.rectangle; + var ancestorRectangle = ancestor.rectangle; + var ancestorSubset = ancestorSubsetScratch; + + ancestorSubset.x = (myRectangle.west - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); + ancestorSubset.y = (myRectangle.south - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); + ancestorSubset.z = (myRectangle.east - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); + ancestorSubset.w = (myRectangle.north - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); + + addDrawCommandsForTile(tileProvider, ancestor, frameState, ancestorSubset); + return; + } - function addDrawCommandsForTile(tileProvider, tile, frameState) { var surfaceTile = tile.data; var creditDisplay = frameState.creditDisplay; @@ -1396,6 +1429,10 @@ define([ uniformMapProperties.southMercatorYAndOneOverHeight.x = southMercatorY; uniformMapProperties.southMercatorYAndOneOverHeight.y = oneOverMercatorHeight; + if (subset !== undefined) { + Cartesian4.clone(subset, uniformMap.textureCoordinateSubset); + } + // For performance, use fog in the shader only when the tile is in fog. var applyFog = enableFog && CesiumMath.fog(tile._distance, frameState.fog.density) > CesiumMath.EPSILON3; @@ -1498,7 +1535,7 @@ define([ uniformMap = combine(uniformMap, tileProvider.uniformMap); } - command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, clippingPlanes); + command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, clippingPlanes, subset !== undefined); command.castShadows = castShadows; command.receiveShadows = receiveShadows; command.renderState = renderState; diff --git a/Source/Scene/TileTerrain.js b/Source/Scene/TileTerrain.js index 425fb575e44..aab3a0dc180 100644 --- a/Source/Scene/TileTerrain.js +++ b/Source/Scene/TileTerrain.js @@ -122,6 +122,11 @@ define([ var sourceY = parent.y; var sourceLevel = parent.level; + if (sourceData === undefined) { + // Parent is not available, so we can't upsample this tile yet. + return; + } + tileTerrain.data = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level); if (!defined(tileTerrain.data)) { // The upsample request has been deferred - try again later. diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index 8364c94ad00..b11715a13e9 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -61,6 +61,10 @@ uniform vec4 u_clippingPlanesEdgeStyle; uniform float u_minimumBrightness; #endif +#ifdef RENDER_PARTIAL_TILE +uniform vec4 u_textureCoordinateSubset; +#endif + varying vec3 v_positionMC; varying vec3 v_positionEC; varying vec3 v_textureCoordinates; @@ -155,6 +159,16 @@ vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat void main() { +#ifdef RENDER_PARTIAL_TILE + if (v_textureCoordinates.x < u_textureCoordinateSubset.x || + v_textureCoordinates.y < u_textureCoordinateSubset.y || + v_textureCoordinates.x > u_textureCoordinateSubset.z || + v_textureCoordinates.y > u_textureCoordinateSubset.w) + { + discard; + } +#endif + #ifdef ENABLE_CLIPPING_PLANES float clipDistance = clip(gl_FragCoord, u_clippingPlanes, u_clippingPlanesMatrix); #endif From 763976d661503130cc8b2bb072963f65e079871f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 19 Jun 2018 16:33:49 +1000 Subject: [PATCH 005/131] Get to upsampling more quickly. --- Source/Core/QuantizedMeshTerrainData.js | 28 ++++++++++++ Source/Scene/GlobeSurfaceTile.js | 43 ++++++++++++++++++- Source/Scene/TileTerrain.js | 18 +++++++- .../createVerticesFromQuantizedTerrainMesh.js | 5 +++ .../Workers/upsampleQuantizedTerrainMesh.js | 16 +++++++ 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index ecf9d65fc60..c3aaae2caad 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -369,6 +369,10 @@ define([ var upsampleTaskProcessor = new TaskProcessor('upsampleQuantizedTerrainMesh'); + // var startTime; + // var notDeferredTime; + // var stopTime; + /** * Upsamples this terrain data for use by a descendant tile. The resulting instance will contain a subset of the * vertices in this instance, interpolated if necessary. @@ -418,6 +422,13 @@ define([ return undefined; } + // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { + // if (startTime !== undefined) { + // console.log('**************** wut'); + // } + // startTime = Date.now(); + // } + var isEastChild = thisX * 2 !== descendantX; var isNorthChild = thisY * 2 === descendantY; @@ -425,6 +436,10 @@ define([ var childRectangle = tilingScheme.tileXYToRectangle(descendantX, descendantY, descendantLevel); var upsamplePromise = upsampleTaskProcessor.scheduleTask({ + // startTime : Date.now(), + // level : descendantLevel, + // x : descendantX, + // y : descendantY, vertices : mesh.vertices, vertexCountWithoutSkirts : this._vertexCountWithoutSkirts, indices : mesh.indices, @@ -440,10 +455,18 @@ define([ }); if (!defined(upsamplePromise)) { + // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { + // console.log('woe'); + // } // Postponed return undefined; } + // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { + // console.log('Scheduled'); + // notDeferredTime = Date.now(); + // } + var shortestSkirt = Math.min(this._westSkirtHeight, this._eastSkirtHeight); shortestSkirt = Math.min(shortestSkirt, this._southSkirtHeight); shortestSkirt = Math.min(shortestSkirt, this._northSkirtHeight); @@ -455,6 +478,11 @@ define([ var credits = this._credits; return when(upsamplePromise).then(function(result) { + // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { + // stopTime = Date.now(); + // console.log('core upsample: ' + (stopTime - startTime) + ' / ' + (stopTime - notDeferredTime)); + // } + var quantizedVertices = new Uint16Array(result.vertices); var indicesTypedArray = IndexDatatype.createTypedArray(quantizedVertices.length / 3, result.indices); var encodedNormals; diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 0a584fff31e..459ff629d32 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -301,7 +301,7 @@ define([ } if (tile.state === QuadtreeTileLoadState.LOADING) { - processTerrainStateMachine(tile, frameState, terrainProvider, vertexArraysToDestroy); + processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); } // The terrain is renderable as soon as we have a valid vertex array. @@ -400,13 +400,52 @@ define([ } } - function processTerrainStateMachine(tile, frameState, terrainProvider, vertexArraysToDestroy) { + // var startTime; + // var stopTime; + + function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { var surfaceTile = tile.data; var loaded = surfaceTile.loadedTerrain; if (defined(loaded)) { + // If this tile is FAILED, we'll need to upsample from the parent. If the parent isn't + // ready for that, let's push it along. + var parent = tile.parent; + if (loaded.state === TerrainState.FAILED && parent !== undefined) { + var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined && parent.data.terrainData._mesh !== undefined; + if (!parentReady) { + //console.log('Waiting on L' + parent.level + 'X' + parent.x + 'Y' + parent.y); + GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); + } + } + + // if (tile.level === 11 && tile.x === 698 && tile.y === 603 || + // tile.level === 10 && tile.x === 349 && tile.y === 301 || + // tile.level === 9 && tile.x === 174 && tile.y === 150) { + // //console.log('Before L' + tile.level + 'X' + tile.x + 'Y' + tile.y + ': ' + loaded.state); + // } + + // if (tile.level === 10 && tile.x === 349 && tile.y === 301) { + // if (parent.data && parent.data.terrainData && parent.data.terrainData._mesh && startTime === undefined) { + // startTime = performance.now(); + // } + // } + loaded.processLoadStateMachine(tile, frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); + // if (tile.level === 10 && tile.x === 349 && tile.y === 301) { + // if (tile.data && tile.data.terrainData && stopTime === undefined) { + // stopTime = performance.now(); + // console.log('Full upsample: ' + (stopTime - startTime)); + // } + // } + + // if (tile.level === 11 && tile.x === 698 && tile.y === 603 || + // tile.level === 10 && tile.x === 349 && tile.y === 301 || + // tile.level === 9 && tile.x === 174 && tile.y === 150) { + // //console.log('After L' + tile.level + 'X' + tile.x + 'Y' + tile.y + ': ' + loaded.state); + // } + // Publish the terrain data on the tile as soon as it is available. // We'll potentially need it to upsample child tiles. if (loaded.state >= TerrainState.RECEIVED) { diff --git a/Source/Scene/TileTerrain.js b/Source/Scene/TileTerrain.js index aab3a0dc180..6517f75151d 100644 --- a/Source/Scene/TileTerrain.js +++ b/Source/Scene/TileTerrain.js @@ -110,6 +110,9 @@ define([ } }; + // var startTime; + // var stopTime; + function upsample(tileTerrain, tile, frameState, terrainProvider, x, y, level, priorityFunction) { var parent = tile.parent; if (!parent) { @@ -122,20 +125,33 @@ define([ var sourceY = parent.y; var sourceLevel = parent.level; - if (sourceData === undefined) { + if (sourceData === undefined || sourceData._mesh === undefined) { // Parent is not available, so we can't upsample this tile yet. return; } + // if (tile.level === 10 && tile.x === 349 && tile.y === 301 && startTime === undefined) { + // startTime = performance.now(); + // } + tileTerrain.data = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level); if (!defined(tileTerrain.data)) { // The upsample request has been deferred - try again later. return; } + // if (tile.level === 10 && tile.x === 349 && tile.y === 301 && stopTime === undefined) { + // stopTime = performance.now(); + // console.log('Waiting for a slot: ' + (stopTime - startTime)); + // } + tileTerrain.state = TerrainState.RECEIVING; when(tileTerrain.data, function(terrainData) { + // if (tile.level === 10 && tile.x === 349 && tile.y === 301) { + // var stopTime = performance.now(); + // console.log('Full upsample2: ' + (stopTime - startTime)); + // } tileTerrain.data = terrainData; tileTerrain.state = TerrainState.RECEIVED; }, function() { diff --git a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js index bbd328bdd0f..edd2620b35c 100644 --- a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js +++ b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js @@ -46,6 +46,8 @@ define([ var scratchFromENU = new Matrix4(); function createVerticesFromQuantizedTerrainMesh(parameters, transferableObjects) { + // var startTime = performance.now(); + var quantizedVertices = parameters.quantizedVertices; var quantizedVertexCount = quantizedVertices.length / 3; var octEncodedNormals = parameters.octEncodedNormals; @@ -208,6 +210,9 @@ define([ transferableObjects.push(vertexBuffer.buffer, indexBuffer.buffer); + // var stopTime = performance.now(); + // console.log('createVerticesFromQuantizedTerrainMesh time: ' + (stopTime - startTime)); + return { vertices : vertexBuffer.buffer, indices : indexBuffer.buffer, diff --git a/Source/Workers/upsampleQuantizedTerrainMesh.js b/Source/Workers/upsampleQuantizedTerrainMesh.js index 1d4b737e5a8..9921c0bbaaa 100644 --- a/Source/Workers/upsampleQuantizedTerrainMesh.js +++ b/Source/Workers/upsampleQuantizedTerrainMesh.js @@ -49,7 +49,18 @@ define([ var decodeTexCoordsScratch = new Cartesian2(); var octEncodedNormalScratch = new Cartesian3(); + // var startTime; + function upsampleQuantizedTerrainMesh(parameters, transferableObjects) { + // console.log('L' + parameters.level + 'X' + parameters.x + 'Y' + parameters.y); + // if (parameters.level === 10 && parameters.x === 349 && parameters.y === 301) { + // if (startTime !== undefined) { + // console.log('****** wut'); + // } + // startTime = Date.now(); + // console.log('time until start: ' + (startTime - parameters.startTime)); + // } + var isEastChild = parameters.isEastChild; var isNorthChild = parameters.isNorthChild; @@ -320,6 +331,11 @@ define([ transferableObjects.push(vertices.buffer, indicesTypedArray.buffer); } + // if (parameters.level === 10 && parameters.x === 349 && parameters.y === 301) { + // var stopTime = Date.now(); + // console.log('upsample time: ' + (stopTime - startTime)); + // } + return { vertices : vertices.buffer, encodedNormals : encodedNormals, From 8d47f39581cbfc11e1d4c249c5f460d25a2ea10b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 19 Jun 2018 17:05:30 +1000 Subject: [PATCH 006/131] Correctly account for Cesium's tile numbering (from the North). --- Source/Scene/GlobeSurfaceTile.js | 3 +-- Source/Scene/QuadtreePrimitive.js | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 459ff629d32..be26a430820 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -134,8 +134,7 @@ define([ var parentBvh = parent.data.getBvh(parent, terrainProvider); if (parentBvh !== undefined && parentBvh.length > 2) { var subsetLength = (parentBvh.length - 2) / 4; - // TODO: Cesium tiles are numbered from the North aren't there? This code is assuming from the South. - var childIndex = ((tile.y % 2) === 0 ? 0 : 2) + ((tile.x % 2) === 0 ? 0 : 1); + var childIndex = (tile.y === parent.y * 2 ? 2 : 0) + (tile.x === parent.x * 2 ? 0 : 1); var start = 2 + subsetLength * childIndex; this._bvh = parentBvh.subarray(start, start + subsetLength); } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 1457a5d63cd..42269f0cdf1 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -582,10 +582,10 @@ define([ // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState); - if (tile.needsLoading) { - // Tile is not rendered, so load it with low priority. - primitive._tileLoadQueueLow.push(tile); - } + // if (tile.needsLoading) { + // // Tile is not rendered, so load it with low priority. + // primitive._tileLoadQueueLow.push(tile); + // } } } else { // We'd like to refine but can't because not all of our children are renderable. Load the refinement blockers with high priority and @@ -633,12 +633,12 @@ define([ function queueChildTileLoad(primitive, childTile) { primitive._tileReplacementQueue.markTileRendered(childTile); if (childTile.needsLoading) { - if (childTile.renderable) { - primitive._tileLoadQueueLow.push(childTile); - } else { - // A tile blocking refine loads with high priority - primitive._tileLoadQueueHigh.push(childTile); - } + // if (childTile.renderable) { + // primitive._tileLoadQueueLow.push(childTile); + // } else { + // // A tile blocking refine loads with high priority + // primitive._tileLoadQueueHigh.push(childTile); + // } } } @@ -686,9 +686,9 @@ define([ // We've decided this tile is not visible, but if it's not fully loaded yet, we've made // this determination based on possibly-incorrect information. We need to load this // culled tile with low priority just in case it turns out to be visible after all. - if (tile.needsLoading) { - primitive._tileLoadQueueLow.push(tile); - } + // if (tile.needsLoading) { + // primitive._tileLoadQueueLow.push(tile); + // } } } From 902052a886d3f1fbf1759427ee3d68f760a5d013 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 20 Jun 2018 16:52:50 +1000 Subject: [PATCH 007/131] Remove TileTerrain. --- Source/Scene/Globe.js | 2 +- Source/Scene/GlobeSurfaceShaderSet.js | 2 +- Source/Scene/GlobeSurfaceTile.js | 297 ++++++++++++++------- Source/Scene/GlobeSurfaceTileProvider.js | 9 +- Source/Scene/QuadtreeTile.js | 2 +- Source/Scene/ShadowMap.js | 2 +- Source/Scene/TileTerrain.js | 319 ----------------------- 7 files changed, 216 insertions(+), 417 deletions(-) delete mode 100644 Source/Scene/TileTerrain.js diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 29b77bbac8f..b04cc7762ba 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -521,7 +521,7 @@ define([ tile.northeastChild; } - while (defined(tile) && (!defined(tile.data) || !defined(tile.data.pickTerrain))) { + while (defined(tile) && (!defined(tile.data) || !defined(tile.data.mesh))) { tile = tile.parent; } diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index c53c421911f..12b03791cb4 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -70,7 +70,7 @@ define([ var quantization = 0; var quantizationDefine = ''; - var terrainEncoding = surfaceTile.pickTerrain.mesh.encoding; + var terrainEncoding = surfaceTile.mesh.encoding; var quantizationMode = terrainEncoding.quantization; if (quantizationMode === TerrainQuantization.BITS12) { quantization = 1; diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index be26a430820..bfbaae0db25 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -2,44 +2,58 @@ define([ '../Core/BoundingSphere', '../Core/Cartesian3', '../Core/Cartesian4', - '../Core/defaultValue', '../Core/defined', '../Core/defineProperties', + '../Core/IndexDatatype', '../Core/IntersectionTests', '../Core/PixelFormat', + '../Core/Request', + '../Core/RequestState', + '../Core/RequestType', + '../Core/TileProviderError', + '../Renderer/Buffer', + '../Renderer/BufferUsage', '../Renderer/PixelDatatype', '../Renderer/Sampler', '../Renderer/Texture', '../Renderer/TextureMagnificationFilter', '../Renderer/TextureMinificationFilter', '../Renderer/TextureWrap', + '../Renderer/VertexArray', './ImageryState', './QuadtreeTileLoadState', './SceneMode', './TerrainState', './TileBoundingRegion', - './TileTerrain' + '../ThirdParty/when' ], function( BoundingSphere, Cartesian3, Cartesian4, - defaultValue, defined, defineProperties, + IndexDatatype, IntersectionTests, PixelFormat, + Request, + RequestState, + RequestType, + TileProviderError, + Buffer, + BufferUsage, PixelDatatype, Sampler, Texture, TextureMagnificationFilter, TextureMinificationFilter, TextureWrap, + VertexArray, ImageryState, QuadtreeTileLoadState, SceneMode, TerrainState, TileBoundingRegion, - TileTerrain) { + when) { 'use strict'; /** @@ -82,10 +96,11 @@ define([ this.tileBoundingRegion = undefined; this.occludeePointInScaledSpace = new Cartesian3(); - this.loadedTerrain = undefined; + this.terrainState = TerrainState.UNLOADED; + this.mesh = undefined; + this.vertexArray = undefined; this.pickBoundingSphere = new BoundingSphere(); - this.pickTerrain = undefined; this.surfaceShader = undefined; this.isClipped = true; @@ -105,9 +120,8 @@ define([ get : function() { // Do not remove tiles that are transitioning or that have // imagery that is transitioning. - var loadedTerrain = this.loadedTerrain; - var loadingIsTransitioning = defined(loadedTerrain) && - (loadedTerrain.state === TerrainState.RECEIVING || loadedTerrain.state === TerrainState.TRANSFORMING); + var terrainState = this.terrainState; + var loadingIsTransitioning = terrainState === TerrainState.RECEIVING || terrainState === TerrainState.TRANSFORMING; var shouldRemoveTile = !loadingIsTransitioning; @@ -163,12 +177,7 @@ define([ var scratchResult = new Cartesian3(); GlobeSurfaceTile.prototype.pick = function(ray, mode, projection, cullBackFaces, result) { - var terrain = this.pickTerrain; - if (!defined(terrain)) { - return undefined; - } - - var mesh = terrain.mesh; + var mesh = this.mesh; if (!defined(mesh)) { return undefined; } @@ -207,14 +216,21 @@ define([ this.terrainData = undefined; - if (defined(this.loadedTerrain)) { - this.loadedTerrain.freeResources(); - this.loadedTerrain = undefined; - } + this.terrainState = TerrainState.UNLOADED; + this.mesh = undefined; - if (defined(this.pickTerrain)) { - this.pickTerrain.freeResources(); - this.pickTerrain = undefined; + if (defined(this.vertexArray)) { + var indexBuffer = this.vertexArray.indexBuffer; + + this.vertexArray.destroy(); + this.vertexArray = undefined; + + if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { + --indexBuffer.referenceCount; + if (indexBuffer.referenceCount === 0) { + indexBuffer.destroy(); + } + } } var i, len; @@ -307,7 +323,7 @@ define([ var isRenderable = defined(surfaceTile.vertexArray); // But it's not done loading until our two state machines are terminated. - var isDoneLoading = !defined(surfaceTile.loadedTerrain); + var isDoneLoading = surfaceTile.terrainState === TerrainState.READY; // If this tile's terrain and imagery are just upsampled from its parent, mark the tile as // upsampled only. We won't refine a tile if its four children are upsampled only. @@ -378,15 +394,13 @@ define([ function prepareNewTile(tile, terrainProvider, imageryLayerCollection) { var surfaceTile = tile.data; - surfaceTile.loadedTerrain = new TileTerrain(); - if (tile.parent && tile.parent.terrainData && !tile.parent.terrainData.isChildAvailable(tile.parent.x, tile.parent.y, tile.x, tile.y)) { // Start upsampling right away. - surfaceTile.loadedTerrain.state = TerrainState.FAILED; + surfaceTile.terrainState = TerrainState.FAILED; } else if (terrainProvider.getTileDataAvailable) { if (!terrainProvider.getTileDataAvailable(tile.x, tile.y, tile.level)) { // Start upsampling right away. - surfaceTile.loadedTerrain.state = TerrainState.FAILED; + surfaceTile.terrainState = TerrainState.FAILED; } } @@ -404,76 +418,183 @@ define([ function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { var surfaceTile = tile.data; - var loaded = surfaceTile.loadedTerrain; - if (defined(loaded)) { - // If this tile is FAILED, we'll need to upsample from the parent. If the parent isn't - // ready for that, let's push it along. - var parent = tile.parent; - if (loaded.state === TerrainState.FAILED && parent !== undefined) { - var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined && parent.data.terrainData._mesh !== undefined; - if (!parentReady) { - //console.log('Waiting on L' + parent.level + 'X' + parent.x + 'Y' + parent.y); - GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); - } + // If this tile is FAILED, we'll need to upsample from the parent. If the parent isn't + // ready for that, let's push it along. + var parent = tile.parent; + if (surfaceTile.terrainState === TerrainState.FAILED && parent !== undefined) { + var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined && parent.data.terrainData._mesh !== undefined; + if (!parentReady) { + //console.log('Waiting on L' + parent.level + 'X' + parent.x + 'Y' + parent.y); + GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); } + } - // if (tile.level === 11 && tile.x === 698 && tile.y === 603 || - // tile.level === 10 && tile.x === 349 && tile.y === 301 || - // tile.level === 9 && tile.x === 174 && tile.y === 150) { - // //console.log('Before L' + tile.level + 'X' + tile.x + 'Y' + tile.y + ': ' + loaded.state); - // } - - // if (tile.level === 10 && tile.x === 349 && tile.y === 301) { - // if (parent.data && parent.data.terrainData && parent.data.terrainData._mesh && startTime === undefined) { - // startTime = performance.now(); - // } - // } - - loaded.processLoadStateMachine(tile, frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); - - // if (tile.level === 10 && tile.x === 349 && tile.y === 301) { - // if (tile.data && tile.data.terrainData && stopTime === undefined) { - // stopTime = performance.now(); - // console.log('Full upsample: ' + (stopTime - startTime)); - // } - // } - - // if (tile.level === 11 && tile.x === 698 && tile.y === 603 || - // tile.level === 10 && tile.x === 349 && tile.y === 301 || - // tile.level === 9 && tile.x === 174 && tile.y === 150) { - // //console.log('After L' + tile.level + 'X' + tile.x + 'Y' + tile.y + ': ' + loaded.state); - // } - - // Publish the terrain data on the tile as soon as it is available. - // We'll potentially need it to upsample child tiles. - if (loaded.state >= TerrainState.RECEIVED) { - if (surfaceTile.terrainData !== loaded.data) { - surfaceTile.terrainData = loaded.data; - - // If there's a water mask included in the terrain data, create a - // texture for it. - createWaterMaskTextureIfNeeded(frameState.context, surfaceTile); - } - } + if (surfaceTile.terrainState === TerrainState.FAILED) { + upsample(surfaceTile, tile, frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); + } - if (loaded.state === TerrainState.READY) { - loaded.publishToTile(tile); + if (surfaceTile.terrainState === TerrainState.UNLOADED) { + requestTileGeometry(surfaceTile, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); + } - if (defined(tile.data.vertexArray)) { - // Free the tiles existing vertex array on next render. - vertexArraysToDestroy.push(tile.data.vertexArray); - } + if (surfaceTile.terrainState === TerrainState.RECEIVED) { + transform(surfaceTile, frameState, terrainProvider, tile.x, tile.y, tile.level); + } + + if (surfaceTile.terrainState === TerrainState.TRANSFORMED) { + createResources(surfaceTile, frameState.context, terrainProvider, tile.x, tile.y, tile.level); + } + } + + function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level, priorityFunction) { + var parent = tile.parent; + if (!parent) { + // Trying to upsample from a root tile. No can do. + return; + } + + var sourceData = parent.data.terrainData; + var sourceX = parent.x; + var sourceY = parent.y; + var sourceLevel = parent.level; + + if (sourceData === undefined || sourceData._mesh === undefined) { + // Parent is not available, so we can't upsample this tile yet. + return; + } + + var terrainDataPromise = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level); + if (!defined(terrainDataPromise)) { + // The upsample request has been deferred - try again later. + return; + } + + surfaceTile.terrainState = TerrainState.RECEIVING; + + when(terrainDataPromise, function(terrainData) { + surfaceTile.terrainData = terrainData; + surfaceTile.terrainState = TerrainState.RECEIVED; + }, function() { + surfaceTile.terrainState = TerrainState.FAILED; + }); + } + + function requestTileGeometry(surfaceTile, terrainProvider, x, y, level, priorityFunction) { + function success(terrainData) { + surfaceTile.terrainData = terrainData; + surfaceTile.terrainState = TerrainState.RECEIVED; + surfaceTile.request = undefined; + } + + function failure() { + if (surfaceTile.request.state === RequestState.CANCELLED) { + // Cancelled due to low priority - try again later. + surfaceTile.terrainData = undefined; + surfaceTile.terrainState = TerrainState.UNLOADED; + surfaceTile.request = undefined; + return; + } - // Transfer ownership of the vertex array to the tile itself. - tile.data.vertexArray = loaded.vertexArray; - loaded.vertexArray = undefined; + // Initially assume failure. handleError may retry, in which case the state will + // change to RECEIVING or UNLOADED. + surfaceTile.terrainState = TerrainState.FAILED; + surfaceTile.request = undefined; + + var message = 'Failed to obtain terrain tile X: ' + x + ' Y: ' + y + ' Level: ' + level + '.'; + terrainProvider._requestError = TileProviderError.handleError( + terrainProvider._requestError, + terrainProvider, + terrainProvider.errorEvent, + message, + x, y, level, + doRequest); + } - // No further loading or upsampling is necessary. - surfaceTile.pickTerrain = surfaceTile.loadedTerrain; - surfaceTile.loadedTerrain = undefined; + function doRequest() { + // Request the terrain from the terrain provider. + var request = new Request({ + throttle : true, + throttleByServer : true, + type : RequestType.TERRAIN, + priorityFunction : priorityFunction + }); + surfaceTile.request = request; + var requestPromise = terrainProvider.requestTileGeometry(x, y, level, request); + + // If the request method returns undefined (instead of a promise), the request + // has been deferred. + if (defined(requestPromise)) { + surfaceTile.terrainState = TerrainState.RECEIVING; + when(requestPromise, success, failure); + } else { + // Deferred - try again later. + surfaceTile.terrainState = TerrainState.UNLOADED; + surfaceTile.request = undefined; } } + + doRequest(); + } + + function transform(surfaceTile, frameState, terrainProvider, x, y, level) { + var tilingScheme = terrainProvider.tilingScheme; + + var terrainData = surfaceTile.terrainData; + var meshPromise = terrainData.createMesh(tilingScheme, x, y, level, frameState.terrainExaggeration); + + if (!defined(meshPromise)) { + // Postponed. + return; + } + + surfaceTile.terrainState = TerrainState.TRANSFORMING; + + when(meshPromise, function(mesh) { + surfaceTile.mesh = mesh; + surfaceTile.terrainState = TerrainState.TRANSFORMED; + }, function() { + surfaceTile.terrainState = TerrainState.FAILED; + }); + } + + function createResources(surfaceTile, context, terrainProvider, x, y, level) { + var mesh = surfaceTile.mesh; + + var typedArray = mesh.vertices; + var buffer = Buffer.createVertexBuffer({ + context : context, + typedArray : typedArray, + usage : BufferUsage.STATIC_DRAW + }); + var attributes = mesh.encoding.getAttributes(buffer); + + var indexBuffers = mesh.indices.indexBuffers || {}; + var indexBuffer = indexBuffers[context.id]; + if (!defined(indexBuffer) || indexBuffer.isDestroyed()) { + var indices = mesh.indices; + var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; + indexBuffer = Buffer.createIndexBuffer({ + context : context, + typedArray : indices, + usage : BufferUsage.STATIC_DRAW, + indexDatatype : indexDatatype + }); + indexBuffer.vertexArrayDestroyable = false; + indexBuffer.referenceCount = 1; + indexBuffers[context.id] = indexBuffer; + surfaceTile.mesh.indices.indexBuffers = indexBuffers; + } else { + ++indexBuffer.referenceCount; + } + + surfaceTile.vertexArray = new VertexArray({ + context : context, + attributes : attributes, + indexBuffer : indexBuffer + }); + + surfaceTile.terrainState = TerrainState.READY; } function getContextWaterMaskData(context) { diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 9a2d6cb1a12..b90d47aa929 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -524,9 +524,6 @@ define([ * @returns {Visibility} The visibility of the tile. */ GlobeSurfaceTileProvider.prototype.computeTileVisibility = function(tile, frameState, occluders) { - // TODO: this function is now called before the bounding volume and occludee point - // are initialized, so the visibility test is rubbish. 😳 - var distance = this.computeDistanceToTile(tile, frameState); tile._distance = distance; @@ -1298,8 +1295,8 @@ define([ --maxTextures; } - var rtc = surfaceTile.center; - var encoding = surfaceTile.pickTerrain.mesh.encoding; + var rtc = surfaceTile.mesh.center; + var encoding = surfaceTile.mesh.encoding; // Not used in 3D. var tileRectangle = tileRectangleScratch; @@ -1420,7 +1417,7 @@ define([ uniformMapProperties.lightingFadeDistance.y = tileProvider.lightingFadeInDistance; uniformMapProperties.zoomedOutOceanSpecularIntensity = tileProvider.zoomedOutOceanSpecularIntensity; - uniformMapProperties.center3D = surfaceTile.center; + uniformMapProperties.center3D = surfaceTile.mesh.center; Cartesian3.clone(rtc, uniformMapProperties.rtc); Cartesian4.clone(tileRectangle, uniformMapProperties.tileRectangle); diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 8bf5e88b065..b36ef75f244 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -88,7 +88,7 @@ define([ this.renderable = false; /** - * Gets or set a value indicating whether or not the tile was entire upsampled from its + * Gets or set a value indicating whether or not the tile was entirely upsampled from its * parent tile. If all four children of a parent tile were upsampled from the parent, * we will render the parent instead of the children even if the LOD indicates that * the children would be preferable. diff --git a/Source/Scene/ShadowMap.js b/Source/Scene/ShadowMap.js index 76c5798a292..ecf1296bf7b 100644 --- a/Source/Scene/ShadowMap.js +++ b/Source/Scene/ShadowMap.js @@ -1540,7 +1540,7 @@ define([ var hasTerrainNormal = false; if (isTerrain) { - hasTerrainNormal = command.owner.data.pickTerrain.mesh.encoding.hasVertexNormals; + hasTerrainNormal = command.owner.data.mesh.encoding.hasVertexNormals; } if (command.castShadows) { diff --git a/Source/Scene/TileTerrain.js b/Source/Scene/TileTerrain.js deleted file mode 100644 index 6517f75151d..00000000000 --- a/Source/Scene/TileTerrain.js +++ /dev/null @@ -1,319 +0,0 @@ -define([ - '../Core/BoundingSphere', - '../Core/Cartesian3', - '../Core/defined', - '../Core/DeveloperError', - '../Core/IndexDatatype', - '../Core/OrientedBoundingBox', - '../Core/Request', - '../Core/RequestState', - '../Core/RequestType', - '../Core/TileProviderError', - '../Renderer/Buffer', - '../Renderer/BufferUsage', - '../Renderer/VertexArray', - '../ThirdParty/when', - './TerrainState' - ], function( - BoundingSphere, - Cartesian3, - defined, - DeveloperError, - IndexDatatype, - OrientedBoundingBox, - Request, - RequestState, - RequestType, - TileProviderError, - Buffer, - BufferUsage, - VertexArray, - when, - TerrainState) { - 'use strict'; - - /** - * Manages details of the terrain load or upsample process. - * - * @alias TileTerrain - * @constructor - * @private - * - * @param {TerrainData} [upsampleDetails.data] The terrain data being upsampled. - * @param {Number} [upsampleDetails.x] The X coordinate of the tile being upsampled. - * @param {Number} [upsampleDetails.y] The Y coordinate of the tile being upsampled. - * @param {Number} [upsampleDetails.level] The level coordinate of the tile being upsampled. - */ - function TileTerrain(upsampleDetails) { - /** - * The current state of the terrain in the terrain processing pipeline. - * @type {TerrainState} - * @default {@link TerrainState.UNLOADED} - */ - this.state = TerrainState.UNLOADED; - this.data = undefined; - this.mesh = undefined; - this.vertexArray = undefined; - this.upsampleDetails = upsampleDetails; - this.request = undefined; - } - - TileTerrain.prototype.freeResources = function() { - this.state = TerrainState.UNLOADED; - this.data = undefined; - this.mesh = undefined; - - if (defined(this.vertexArray)) { - var indexBuffer = this.vertexArray.indexBuffer; - - this.vertexArray.destroy(); - this.vertexArray = undefined; - - if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { - --indexBuffer.referenceCount; - if (indexBuffer.referenceCount === 0) { - indexBuffer.destroy(); - } - } - } - }; - - TileTerrain.prototype.publishToTile = function(tile) { - var surfaceTile = tile.data; - - var mesh = this.mesh; - Cartesian3.clone(mesh.center, surfaceTile.center); - surfaceTile.minimumHeight = mesh.minimumHeight; - surfaceTile.maximumHeight = mesh.maximumHeight; - surfaceTile.boundingSphere3D = BoundingSphere.clone(mesh.boundingSphere3D, surfaceTile.boundingSphere3D); - surfaceTile.orientedBoundingBox = OrientedBoundingBox.clone(mesh.orientedBoundingBox, surfaceTile.orientedBoundingBox); - surfaceTile.tileBoundingRegion.minimumHeight = mesh.minimumHeight; - surfaceTile.tileBoundingRegion.maximumHeight = mesh.maximumHeight; - tile.data.occludeePointInScaledSpace = Cartesian3.clone(mesh.occludeePointInScaledSpace, surfaceTile.occludeePointInScaledSpace); - }; - - TileTerrain.prototype.processLoadStateMachine = function(tile, frameState, terrainProvider, x, y, level, priorityFunction) { - if (this.state === TerrainState.FAILED) { - upsample(this, tile, frameState, terrainProvider, x, y, level, priorityFunction) - } - - if (this.state === TerrainState.UNLOADED) { - requestTileGeometry(this, terrainProvider, x, y, level, priorityFunction); - } - - if (this.state === TerrainState.RECEIVED) { - transform(this, frameState, terrainProvider, x, y, level); - } - - if (this.state === TerrainState.TRANSFORMED) { - createResources(this, frameState.context, terrainProvider, x, y, level); - } - }; - - // var startTime; - // var stopTime; - - function upsample(tileTerrain, tile, frameState, terrainProvider, x, y, level, priorityFunction) { - var parent = tile.parent; - if (!parent) { - // Trying to upsample from a root tile. No can do. - return; - } - - var sourceData = parent.data.terrainData; - var sourceX = parent.x; - var sourceY = parent.y; - var sourceLevel = parent.level; - - if (sourceData === undefined || sourceData._mesh === undefined) { - // Parent is not available, so we can't upsample this tile yet. - return; - } - - // if (tile.level === 10 && tile.x === 349 && tile.y === 301 && startTime === undefined) { - // startTime = performance.now(); - // } - - tileTerrain.data = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level); - if (!defined(tileTerrain.data)) { - // The upsample request has been deferred - try again later. - return; - } - - // if (tile.level === 10 && tile.x === 349 && tile.y === 301 && stopTime === undefined) { - // stopTime = performance.now(); - // console.log('Waiting for a slot: ' + (stopTime - startTime)); - // } - - tileTerrain.state = TerrainState.RECEIVING; - - when(tileTerrain.data, function(terrainData) { - // if (tile.level === 10 && tile.x === 349 && tile.y === 301) { - // var stopTime = performance.now(); - // console.log('Full upsample2: ' + (stopTime - startTime)); - // } - tileTerrain.data = terrainData; - tileTerrain.state = TerrainState.RECEIVED; - }, function() { - tileTerrain.state = TerrainState.FAILED; - }); - } - - function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, priorityFunction) { - function success(terrainData) { - tileTerrain.data = terrainData; - tileTerrain.state = TerrainState.RECEIVED; - tileTerrain.request = undefined; - } - - function failure() { - if (tileTerrain.request.state === RequestState.CANCELLED) { - // Cancelled due to low priority - try again later. - tileTerrain.data = undefined; - tileTerrain.state = TerrainState.UNLOADED; - tileTerrain.request = undefined; - return; - } - - // Initially assume failure. handleError may retry, in which case the state will - // change to RECEIVING or UNLOADED. - tileTerrain.state = TerrainState.FAILED; - tileTerrain.request = undefined; - - var message = 'Failed to obtain terrain tile X: ' + x + ' Y: ' + y + ' Level: ' + level + '.'; - terrainProvider._requestError = TileProviderError.handleError( - terrainProvider._requestError, - terrainProvider, - terrainProvider.errorEvent, - message, - x, y, level, - doRequest); - } - - function doRequest() { - // Request the terrain from the terrain provider. - var request = new Request({ - throttle : true, - throttleByServer : true, - type : RequestType.TERRAIN, - priorityFunction : priorityFunction - }); - tileTerrain.request = request; - tileTerrain.data = terrainProvider.requestTileGeometry(x, y, level, request); - - // If the request method returns undefined (instead of a promise), the request - // has been deferred. - if (defined(tileTerrain.data)) { - tileTerrain.state = TerrainState.RECEIVING; - when(tileTerrain.data, success, failure); - } else { - // Deferred - try again later. - tileTerrain.state = TerrainState.UNLOADED; - tileTerrain.request = undefined; - } - } - - doRequest(); - } - - TileTerrain.prototype.processUpsampleStateMachine = function(frameState, terrainProvider, x, y, level) { - if (this.state === TerrainState.UNLOADED) { - var upsampleDetails = this.upsampleDetails; - - //>>includeStart('debug', pragmas.debug); - if (!defined(upsampleDetails)) { - throw new DeveloperError('TileTerrain cannot upsample unless provided upsampleDetails.'); - } - //>>includeEnd('debug'); - - var sourceData = upsampleDetails.data; - var sourceX = upsampleDetails.x; - var sourceY = upsampleDetails.y; - var sourceLevel = upsampleDetails.level; - - this.data = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level); - if (!defined(this.data)) { - // The upsample request has been deferred - try again later. - return; - } - - this.state = TerrainState.RECEIVING; - - var that = this; - when(this.data, function(terrainData) { - that.data = terrainData; - that.state = TerrainState.RECEIVED; - }, function() { - that.state = TerrainState.FAILED; - }); - } - - if (this.state === TerrainState.RECEIVED) { - transform(this, frameState, terrainProvider, x, y, level); - } - - if (this.state === TerrainState.TRANSFORMED) { - createResources(this, frameState.context, terrainProvider, x, y, level); - } - }; - - function transform(tileTerrain, frameState, terrainProvider, x, y, level) { - var tilingScheme = terrainProvider.tilingScheme; - - var terrainData = tileTerrain.data; - var meshPromise = terrainData.createMesh(tilingScheme, x, y, level, frameState.terrainExaggeration); - - if (!defined(meshPromise)) { - // Postponed. - return; - } - - tileTerrain.state = TerrainState.TRANSFORMING; - - when(meshPromise, function(mesh) { - tileTerrain.mesh = mesh; - tileTerrain.state = TerrainState.TRANSFORMED; - }, function() { - tileTerrain.state = TerrainState.FAILED; - }); - } - - function createResources(tileTerrain, context, terrainProvider, x, y, level) { - var typedArray = tileTerrain.mesh.vertices; - var buffer = Buffer.createVertexBuffer({ - context : context, - typedArray : typedArray, - usage : BufferUsage.STATIC_DRAW - }); - var attributes = tileTerrain.mesh.encoding.getAttributes(buffer); - - var indexBuffers = tileTerrain.mesh.indices.indexBuffers || {}; - var indexBuffer = indexBuffers[context.id]; - if (!defined(indexBuffer) || indexBuffer.isDestroyed()) { - var indices = tileTerrain.mesh.indices; - var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; - indexBuffer = Buffer.createIndexBuffer({ - context : context, - typedArray : indices, - usage : BufferUsage.STATIC_DRAW, - indexDatatype : indexDatatype - }); - indexBuffer.vertexArrayDestroyable = false; - indexBuffer.referenceCount = 1; - indexBuffers[context.id] = indexBuffer; - tileTerrain.mesh.indices.indexBuffers = indexBuffers; - } else { - ++indexBuffer.referenceCount; - } - - tileTerrain.vertexArray = new VertexArray({ - context : context, - attributes : attributes, - indexBuffer : indexBuffer - }); - - tileTerrain.state = TerrainState.READY; - } - - return TileTerrain; -}); From 4d5c53e3c7442d5cce7346790e27b74585e25804 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 20 Jun 2018 17:09:00 +1000 Subject: [PATCH 008/131] Remove more unused tile properties. --- Source/Scene/GlobeSurfaceTile.js | 3 --- Source/Scene/GlobeSurfaceTileProvider.js | 11 ++++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index bfbaae0db25..533b7801842 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -77,10 +77,7 @@ define([ this.waterMaskTranslationAndScale = new Cartesian4(0.0, 0.0, 1.0, 1.0); this.terrainData = undefined; - this.center = new Cartesian3(); this.vertexArray = undefined; - this.minimumHeight = 0.0; - this.maximumHeight = 0.0; this.boundingSphere3D = new BoundingSphere(); this.orientedBoundingBox = undefined; this.boundingVolumeSourceTile = undefined; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index b90d47aa929..0d16131350e 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -535,6 +535,7 @@ define([ } var surfaceTile = tile.data; + var tileBoundingRegion = surfaceTile.tileBoundingRegion; // We now need a bounding volume in order to determine visibility, but the tile // may not be loaded yet. @@ -548,7 +549,6 @@ define([ if (heightSource !== surfaceTile.boundingVolumeSourceTile) { // Heights are from a new source tile, so update the bounding volume. surfaceTile.boundingVolumeSourceTile = heightSource; - var tileBoundingRegion = surfaceTile.tileBoundingRegion; surfaceTile.orientedBoundingBox = OrientedBoundingBox.fromRectangle( tile.rectangle, tileBoundingRegion.minimumHeight, @@ -565,7 +565,7 @@ define([ if (frameState.mode !== SceneMode.SCENE3D) { boundingVolume = boundingSphereScratch; - BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, surfaceTile.minimumHeight, surfaceTile.maximumHeight, boundingVolume); + BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume); Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); if (frameState.mode === SceneMode.MORPHING) { @@ -1074,7 +1074,7 @@ define([ return frameState.fog.minimumBrightness; }, u_textureCoordinateSubset : function() { - return this.textureCoordinateSubset; + return this.properties.textureCoordinateSubset; }, // make a separate object so that changes to the properties are seen on @@ -1427,7 +1427,7 @@ define([ uniformMapProperties.southMercatorYAndOneOverHeight.y = oneOverMercatorHeight; if (subset !== undefined) { - Cartesian4.clone(subset, uniformMap.textureCoordinateSubset); + Cartesian4.clone(subset, uniformMapProperties.textureCoordinateSubset); } // For performance, use fog in the shader only when the tile is in fog. @@ -1553,7 +1553,8 @@ define([ var orientedBoundingBox = command.orientedBoundingBox; if (frameState.mode !== SceneMode.SCENE3D) { - BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, surfaceTile.minimumHeight, surfaceTile.maximumHeight, boundingVolume); + var tileBoundingRegion = surfaceTile.tileBoundingRegion; + BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume); Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); if (frameState.mode === SceneMode.MORPHING) { From c3f4931f0e6b725deb53df41d67c4f7fb7f74256 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 21 Jun 2018 14:05:57 +1000 Subject: [PATCH 009/131] More efficient way to finding closest renderable tile. --- Source/Scene/Globe.js | 18 ++++--- Source/Scene/GlobeSurfaceTile.js | 6 ++- Source/Scene/GlobeSurfaceTileProvider.js | 69 ++++++++++++------------ Source/Scene/QuadtreePrimitive.js | 62 +++++++++++---------- 4 files changed, 87 insertions(+), 68 deletions(-) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index b04cc7762ba..5b1ab35b8c4 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -438,23 +438,29 @@ define([ for (i = 0; i < length; ++i) { tile = tilesToRender[i]; - var tileData = tile.data; + var surfaceTile = tile.data; - if (!defined(tileData)) { + if (!defined(surfaceTile)) { continue; } - var boundingVolume = tileData.pickBoundingSphere; + var boundingVolume = surfaceTile.pickBoundingSphere; if (mode !== SceneMode.SCENE3D) { - BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, tileData.minimumHeight, tileData.maximumHeight, boundingVolume); + // TODO: ok to allocate / recreate the bounding sphere every time here? + boundingVolume = BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, surfaceTile.tileBoundingRegion.minimumHeight, surfaceTile.tileBoundingRegion.maximumHeight, boundingVolume); Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); + } else if (surfaceTile.renderableTile !== undefined) { + BoundingSphere.fromRectangle3D(tile.rectangle, tile.tilingScheme.ellipsoid, surfaceTile.tileBoundingRegion.maximumHeight, boundingVolume); + } else if (surfaceTile.mesh !== undefined) { + BoundingSphere.clone(surfaceTile.mesh.boundingSphere3D, boundingVolume); } else { - BoundingSphere.clone(tileData.boundingSphere3D, boundingVolume); + // So wait how did we render this thing then? It shouldn't be possible to get here. + continue; } var boundingSphereIntersection = IntersectionTests.raySphere(ray, boundingVolume, scratchSphereIntersectionResult); if (defined(boundingSphereIntersection)) { - sphereIntersections.push(tileData); + sphereIntersections.push(surfaceTile); } } diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 533b7801842..fccc32eaf6f 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -73,16 +73,17 @@ define([ this.imagery = []; this.waterMaskTexture = undefined; - this.waterMaskTranslationAndScale = new Cartesian4(0.0, 0.0, 1.0, 1.0); this.terrainData = undefined; this.vertexArray = undefined; - this.boundingSphere3D = new BoundingSphere(); this.orientedBoundingBox = undefined; this.boundingVolumeSourceTile = undefined; this._bvh = undefined; + this.renderableTile = undefined; + this.renderableTileSubset = new Cartesian4(); + /** * A bounding region used to estimate distance to the tile. The horizontal bounds are always tight-fitting, * but the `minimumHeight` and `maximumHeight` properties may be derived from the min/max of an ancestor tile @@ -97,6 +98,7 @@ define([ this.mesh = undefined; this.vertexArray = undefined; + // TODO: probably better to have a bounding sphere for 2D rather than one for picking. this.pickBoundingSphere = new BoundingSphere(); this.surfaceShader = undefined; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 0d16131350e..0e4c41e5c68 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -555,13 +555,12 @@ define([ tileBoundingRegion.maximumHeight, tile.tilingScheme.ellipsoid, surfaceTile.orientedBoundingBox); - surfaceTile.boundingSphere3D = undefined; // TODO: compute the bounding sphere for real, or get rid of it entirely. surfaceTile.occludeePointInScaledSpace = undefined; // TODO } } var cullingVolume = frameState.cullingVolume; - var boundingVolume = defaultValue(surfaceTile.orientedBoundingBox, surfaceTile.boundingSphere3D); + var boundingVolume = surfaceTile.orientedBoundingBox; if (frameState.mode !== SceneMode.SCENE3D) { boundingVolume = boundingSphereScratch; @@ -569,7 +568,8 @@ define([ Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); if (frameState.mode === SceneMode.MORPHING) { - boundingVolume = BoundingSphere.union(surfaceTile.boundingSphere3D, boundingVolume, boundingVolume); + // TODO: mesh not necessarily available yet. + boundingVolume = BoundingSphere.union(surfaceTile.mesh.boundingSphere3D, boundingVolume, boundingVolume); } } @@ -629,10 +629,11 @@ define([ * render commands to the commandList, or use any other method as appropriate. The tile is not * expected to be visible next frame as well, unless this method is called next frame, too. * - * @param {Object} tile The tile instance. + * @param {QuadtreeTile} tile The tile instance. * @param {FrameState} frameState The state information of the current rendering frame. + * @param {QuadtreeTile} [nearestRenderableTile] The nearest ancestor tile that is renderable. */ - GlobeSurfaceTileProvider.prototype.showTileThisFrame = function(tile, frameState) { + GlobeSurfaceTileProvider.prototype.showTileThisFrame = function(tile, frameState, nearestRenderableTile) { var readyTextureCount = 0; var tileImageryCollection = tile.data.imagery; for (var i = 0, len = tileImageryCollection.length; i < len; ++i) { @@ -650,6 +651,22 @@ define([ tileSet.push(tile); + var surfaceTile = tile.data; + if (nearestRenderableTile !== undefined && nearestRenderableTile !== tile) { + surfaceTile.renderableTile = nearestRenderableTile; + + var myRectangle = tile.rectangle; + var ancestorRectangle = nearestRenderableTile.rectangle; + var ancestorSubset = surfaceTile.renderableTileSubset; + + ancestorSubset.x = (myRectangle.west - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); + ancestorSubset.y = (myRectangle.south - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); + ancestorSubset.z = (myRectangle.east - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); + ancestorSubset.w = (myRectangle.north - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); + } else { + surfaceTile.renderableTile = undefined; + } + var debug = this._debug; ++debug.tilesRendered; debug.texturesRendered += readyTextureCount; @@ -1236,36 +1253,22 @@ define([ })(); var otherPassesInitialColor = new Cartesian4(0.0, 0.0, 0.0, 0.0); - var ancestorSubsetScratch = new Cartesian4(); function addDrawCommandsForTile(tileProvider, tile, frameState, subset) { - if (!tile.renderable) { - // We can't render this tile yet, so instead render a subset of our closest renderable ancestor. - var ancestor = tile.parent; - while (ancestor !== undefined && !ancestor.renderable) { - ancestor = ancestor.parent; - } - - if (ancestor === undefined) { - // Can't find any renderable ancestor. This shouldn't happen because we - // don't start tile selection at all until the root tiles are loaded. - return; - } - - var myRectangle = tile.rectangle; - var ancestorRectangle = ancestor.rectangle; - var ancestorSubset = ancestorSubsetScratch; - - ancestorSubset.x = (myRectangle.west - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); - ancestorSubset.y = (myRectangle.south - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); - ancestorSubset.z = (myRectangle.east - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); - ancestorSubset.w = (myRectangle.north - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); + var surfaceTile = tile.data; - addDrawCommandsForTile(tileProvider, ancestor, frameState, ancestorSubset); + if (surfaceTile.renderableTile !== undefined) { + // We can't render this tile yet, so instead render a subset of our closest renderable ancestor. + addDrawCommandsForTile(tileProvider, surfaceTile.renderableTile, frameState, surfaceTile.renderableTileSubset); return; } - var surfaceTile = tile.data; + //>>includeStart('debug', pragmas.debug); + if (!tile.renderable) { + throw new DeveloperError('A rendered tile is not renderable, this should not be possible.'); + } + //>>includeEnd('debug'); + var creditDisplay = frameState.creditDisplay; var terrainData = surfaceTile.terrainData; @@ -1405,8 +1408,8 @@ define([ // to have more than one selected tile, this would have to change. if (defined(surfaceTile.orientedBoundingBox)) { getDebugOrientedBoundingBox(surfaceTile.orientedBoundingBox, Color.RED).update(frameState); - } else if (defined(surfaceTile.boundingSphere3D)) { - getDebugBoundingSphere(surfaceTile.boundingSphere3D, Color.RED).update(frameState); + } else if (defined(surfaceTile.mesh) && defined(surfaceTile.mesh.boundingSphere3D)) { + getDebugBoundingSphere(surfaceTile.mesh.boundingSphere3D, Color.RED).update(frameState); } } @@ -1558,10 +1561,10 @@ define([ Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); if (frameState.mode === SceneMode.MORPHING) { - boundingVolume = BoundingSphere.union(surfaceTile.boundingSphere3D, boundingVolume, boundingVolume); + boundingVolume = BoundingSphere.union(surfaceTile.mesh.boundingSphere3D, boundingVolume, boundingVolume); } } else { - command.boundingVolume = BoundingSphere.clone(surfaceTile.boundingSphere3D, boundingVolume); + command.boundingVolume = BoundingSphere.clone(surfaceTile.mesh.boundingSphere3D, boundingVolume); command.orientedBoundingBox = OrientedBoundingBox.clone(surfaceTile.orientedBoundingBox, orientedBoundingBox); } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 42269f0cdf1..4504a52e05e 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -94,6 +94,7 @@ define([ var ellipsoid = tilingScheme.ellipsoid; this._tilesToRender = []; + this._nearestRenderableTiles = []; this._tileLoadQueueHigh = []; // high priority tiles are preventing refinement this._tileLoadQueueMedium = []; // medium priority tiles are being rendered this._tileLoadQueueLow = []; // low priority tiles were refined past or are non-visible parts of quads. @@ -449,6 +450,7 @@ define([ // Clear the render list. var tilesToRender = primitive._tilesToRender; tilesToRender.length = 0; + primitive._nearestRenderableTiles.length = 0; // We can't render anything before the level zero tiles exist. var tileProvider = primitive._tileProvider; @@ -522,7 +524,7 @@ define([ } ++debug.tilesWaitingForChildren; } else if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { - visitTile(primitive, frameState, tile); + visitTile(primitive, frameState, tile, tile); } else { if (tile.needsLoading) { primitive._tileLoadQueueLow.push(tile); @@ -532,7 +534,7 @@ define([ } } - function visitTile(primitive, frameState, tile) { + function visitTile(primitive, frameState, tile, nearestRenderableTile) { var debug = primitive._debug; ++debug.tilesVisited; @@ -544,13 +546,17 @@ define([ debug.maxDepth = tile.level; } + if (tile.renderable) { + nearestRenderableTile = tile; + } + if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) { // This tile meets SSE requirements, so render it. if (tile.needsLoading) { // Rendered tile meeting SSE loads with medium priority. primitive._tileLoadQueueMedium.push(tile); } - addTileToRenderList(primitive, tile); + addTileToRenderList(primitive, tile, nearestRenderableTile); return; } @@ -566,7 +572,7 @@ define([ if (tileProvider.canRefine(tile)) { if (allAreUpsampled) { // No point in rendering the children because they're all upsampled. Render this tile instead. - addTileToRenderList(primitive, tile); + addTileToRenderList(primitive, tile, nearestRenderableTile); // Load the children even though we're (currently) not going to render them. // A tile that is "upsampled only" right now might change its tune once it does more loading. @@ -580,7 +586,7 @@ define([ } else { // SSE is not good enough and children are loaded, so refine. // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. - visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState); + visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); // if (tile.needsLoading) { // // Tile is not rendered, so load it with low priority. @@ -591,7 +597,7 @@ define([ // We'd like to refine but can't because not all of our children are renderable. Load the refinement blockers with high priority and // render this tile in the meantime. queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild); - addTileToRenderList(primitive, tile); + addTileToRenderList(primitive, tile, nearestRenderableTile); if (tile.needsLoading) { // We will refine this tile when it's possible, so load this tile only with low priority. @@ -642,7 +648,7 @@ define([ } } - function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState) { + function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile) { var cameraPosition = frameState.camera.positionCartographic; var tileProvider = primitive._tileProvider; var occluders = primitive._occluders; @@ -650,35 +656,35 @@ define([ if (cameraPosition.longitude < southwest.rectangle.east) { if (cameraPosition.latitude < southwest.rectangle.north) { // Camera in southwest quadrant - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); } else { // Camera in northwest quadrant - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); } } else if (cameraPosition.latitude < southwest.rectangle.north) { // Camera southeast quadrant - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); } else { // Camera in northeast quadrant - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); } } - function visitIfVisible(primitive, tile, tileProvider, frameState, occluders) { + function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile) { if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { - visitTile(primitive, frameState, tile); + visitTile(primitive, frameState, tile, nearestRenderableTile); } else { ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); @@ -734,8 +740,9 @@ define([ return error; } - function addTileToRenderList(primitive, tile) { + function addTileToRenderList(primitive, tile, nearestRenderableTile) { primitive._tilesToRender.push(tile); + primitive._nearestRenderableTiles.push(nearestRenderableTile); ++primitive._debug.tilesRendered; } @@ -876,11 +883,12 @@ define([ function createRenderCommandsForSelectedTiles(primitive, frameState) { var tileProvider = primitive._tileProvider; var tilesToRender = primitive._tilesToRender; + var nearestRenderableTiles = primitive._nearestRenderableTiles; var tilesToUpdateHeights = primitive._tileToUpdateHeights; for (var i = 0, len = tilesToRender.length; i < len; ++i) { var tile = tilesToRender[i]; - tileProvider.showTileThisFrame(tile, frameState); + tileProvider.showTileThisFrame(tile, frameState, nearestRenderableTiles[i]); if (tile._frameRendered !== frameState.frameNumber - 1) { tilesToUpdateHeights.push(tile); From 37e352f2bbad72baec448ca03b5b9925030c7b98 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 22 Jun 2018 10:59:29 +1000 Subject: [PATCH 010/131] Fix problems with heightmap terrain / no availability data. --- Source/Core/HeightmapTerrainData.js | 10 +++- Source/Core/QuantizedMeshTerrainData.js | 6 +++ Source/Scene/GlobeSurfaceTile.js | 63 +++++++++++++++++++++--- Source/Scene/GlobeSurfaceTileProvider.js | 29 ++++++++--- Source/Scene/QuadtreePrimitive.js | 15 ++---- 5 files changed, 97 insertions(+), 26 deletions(-) diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index 905ece4de51..24f10f68196 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -162,8 +162,14 @@ define([ get : function() { return this._waterMask; } - } - }); + }, + + childTileMask : { + get : function() { + return this._childTileMask; + } + }, +}); var taskProcessor = new TaskProcessor('createVerticesFromHeightmap'); diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index c3aaae2caad..79ece87d5e6 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -225,6 +225,12 @@ define([ } }, + childTileMask : { + get : function() { + return this._childTileMask; + } + }, + /** * Gets the bounding-volume hierarchy (BVH) starting with this tile. * @memberof QuantizedMeshTerrainData.prototype diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index fccc32eaf6f..e8c8ec19aa4 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -4,6 +4,7 @@ define([ '../Core/Cartesian4', '../Core/defined', '../Core/defineProperties', + '../Core/DeveloperError', '../Core/IndexDatatype', '../Core/IntersectionTests', '../Core/PixelFormat', @@ -32,6 +33,7 @@ define([ Cartesian4, defined, defineProperties, + DeveloperError, IndexDatatype, IntersectionTests, PixelFormat, @@ -103,6 +105,8 @@ define([ this.surfaceShader = undefined; this.isClipped = true; + + this.childTileMask = undefined; } defineProperties(GlobeSurfaceTile.prototype, { @@ -390,17 +394,64 @@ define([ } }; + /** + * Determines if a given child tile is available. The given child tile coordinates are assumed + * to be one of the four children of this tile. If non-child tile coordinates are + * given, the availability of the southeast child tile is returned. This function determines + * the presence of child tiles from `terrainProvider.availability` or from `this.terrainData.childTileMask`. + * + * @param {TerrainProvider} terrainProvider The terrain provider. + * @param {QuatreeTile} tile The parent tile. + * @param {Number} childX The tile X coordinate of the child tile to check for availability. + * @param {Number} childY The tile Y coordinate of the child tile to check for availability. + * @returns {Boolean|undefined} True if the child tile is available; otherwise, false. If tile availability + * cannot be determined, this function returns undefined. + */ + GlobeSurfaceTile.prototype.isChildAvailable = function(terrainProvider, tile, childX, childY) { + //>>includeStart('debug', pragmas.debug); + if (!defined(terrainProvider)) { + throw new DeveloperError('terrainProvider is required.'); + } + if (!defined(tile)) { + throw new DeveloperError('tile is required.'); + } + if (!defined(childX)) { + throw new DeveloperError('childX is required.'); + } + if (!defined(childY)) { + throw new DeveloperError('childY is required.'); + } + //>>includeEnd('debug'); + + if (this.childTileMask === undefined) { + if (terrainProvider.availability !== undefined) { + this.childTileMask = terrainProvider.availability.computeChildMaskForTile(tile.level, tile.x, tile.y); + } else if (this.terrainData !== undefined && this.terrainData.childTileMask !== undefined) { + this.childTileMask = this.terrainData.childTileMask; + } else { + // No idea if children exist or not. + return undefined; + } + } + + var bitNumber = 2; // northwest child + if (childX !== tile.x * 2) { + ++bitNumber; // east child + } + if (childY !== tile.y * 2) { + bitNumber -= 2; // south child + } + + return (this.childTileMask & (1 << bitNumber)) !== 0; + }; + function prepareNewTile(tile, terrainProvider, imageryLayerCollection) { var surfaceTile = tile.data; - if (tile.parent && tile.parent.terrainData && !tile.parent.terrainData.isChildAvailable(tile.parent.x, tile.parent.y, tile.x, tile.y)) { + var parent = tile.parent; + if (parent !== undefined && !parent.data.isChildAvailable(parent.x, parent.y, tile.x, tile.y)) { // Start upsampling right away. surfaceTile.terrainState = TerrainState.FAILED; - } else if (terrainProvider.getTileDataAvailable) { - if (!terrainProvider.getTileDataAvailable(tile.x, tile.y, tile.level)) { - // Start upsampling right away. - surfaceTile.terrainState = TerrainState.FAILED; - } } // Map imagery tiles to this terrain tile diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 0e4c41e5c68..10b2c68affa 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -610,10 +610,12 @@ define([ * @returns {boolean} True if the tile can be refined, false if it cannot. */ GlobeSurfaceTileProvider.prototype.canRefine = function(tile) { - //return tile.southwestChild.renderable && tile.southeastChild.renderable && tile.northwestChild.renderable && tile.northeastChild.renderable; - // TODO: only allow refinement if we know if we know whether or not the children are available. - // TODO: do we need to do anything to limit the upsampled depth? - return true; + // Only allow refinement it we know whether or not the children of this tile exist. + // For a tileset with `availability`, we'll always be able to refine. + // We can ask for availability of _any_ child tile because we only need to confirm + // that we get a yes or no answer, it doesn't matter what the answer is. + var childAvailable = tile.data.isChildAvailable(this.terrainProvider, tile, 0, 0); + return childAvailable !== undefined; }; var modifiedModelViewScratch = new Matrix4(); @@ -740,11 +742,18 @@ define([ } var terrainData = surfaceTile.terrainData; + var mesh = surfaceTile.mesh; var tileBoundingRegion = surfaceTile.tileBoundingRegion; + if (mesh !== undefined && mesh.minimumHeight !== undefined && mesh.maximumHeight !== undefined) { + // We have tight-fitting min/max heights from the mesh. + tileBoundingRegion.minimumHeight = mesh.minimumHeight; + tileBoundingRegion.maximumHeight = mesh.maximumHeight; + return tile; + } + if (terrainData !== undefined && terrainData._minimumHeight !== undefined && terrainData._maximumHeight !== undefined) { - // We have tight-fitting min/max heights from the geometry. - // TODO: HeightmapTerrainData won't have min/max properties, but the TerrainMesh will. + // We have tight-fitting min/max heights from the terrain data. tileBoundingRegion.minimumHeight = terrainData._minimumHeight; tileBoundingRegion.maximumHeight = terrainData._maximumHeight; return tile; @@ -766,9 +775,15 @@ define([ while (ancestor !== undefined) { var ancestorSurfaceTile = ancestor.data; if (ancestorSurfaceTile !== undefined) { + var ancestorMesh = ancestorSurfaceTile.mesh; + if (ancestorMesh !== undefined && ancestorMesh.minimumHeight !== undefined && ancestorMesh.maximumHeight !== undefined) { + tileBoundingRegion.minimumHeight = ancestorMesh.minimumHeight; + tileBoundingRegion.maximumHeight = ancestorMesh.maximumHeight; + return ancestor; + } + var ancestorTerrainData = ancestorSurfaceTile.terrainData; if (ancestorTerrainData !== undefined && ancestorTerrainData._minimumHeight !== undefined && ancestorTerrainData._maximumHeight !== undefined) { - // TODO: HeightmapTerrainData won't have min/max properties, but the TerrainMesh will. tileBoundingRegion.minimumHeight = ancestorTerrainData._minimumHeight; tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight; return ancestor; diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 4504a52e05e..c7195b4cfff 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -588,10 +588,10 @@ define([ // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); - // if (tile.needsLoading) { - // // Tile is not rendered, so load it with low priority. - // primitive._tileLoadQueueLow.push(tile); - // } + if (tile.needsLoading) { + // Tile is not rendered, so load it with low priority. + primitive._tileLoadQueueLow.push(tile); + } } } else { // We'd like to refine but can't because not all of our children are renderable. Load the refinement blockers with high priority and @@ -688,13 +688,6 @@ define([ } else { ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); - - // We've decided this tile is not visible, but if it's not fully loaded yet, we've made - // this determination based on possibly-incorrect information. We need to load this - // culled tile with low priority just in case it turns out to be visible after all. - // if (tile.needsLoading) { - // primitive._tileLoadQueueLow.push(tile); - // } } } From 4ea1a5db86c597a4912b168809c7d9ab18091b8b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 24 Jun 2018 16:42:01 +1000 Subject: [PATCH 011/131] Fix flipped bounding region estimation. Also add tile state change reporting for debugging. --- Source/Scene/GlobeSurfaceTileProvider.js | 19 ++++++++++++--- Source/Scene/QuadtreePrimitive.js | 31 ++++++++++++++++++++++++ terrainnotes.md | 1 + 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 10b2c68affa..ad0fcc24240 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -524,6 +524,16 @@ define([ * @returns {Visibility} The visibility of the tile. */ GlobeSurfaceTileProvider.prototype.computeTileVisibility = function(tile, frameState, occluders) { + var result = this.computeTileVisibilityInternal(tile, frameState, occluders); + // if (result !== tile._lastVisibility) { + // console.log(`L${tile.level}X${tile.x}Y${tile.y}: state ${tile._lastState} -> ${tile.data.terrainState} visibility ${tile._lastVisibility} -> ${result}`); + // tile._lastVisibility = result; + // tile._lastState = tile.data.terrainState; + // } + return result; + }; + + GlobeSurfaceTileProvider.prototype.computeTileVisibilityInternal = function(tile, frameState, occluders) { var distance = this.computeDistanceToTile(tile, frameState); tile._distance = distance; @@ -614,6 +624,9 @@ define([ // For a tileset with `availability`, we'll always be able to refine. // We can ask for availability of _any_ child tile because we only need to confirm // that we get a yes or no answer, it doesn't matter what the answer is. + // if ((tile.level % 6) === 0 && tile.data.terrainData === undefined) { + // return false; + // } var childAvailable = tile.data.isChildAvailable(this.terrainProvider, tile, 0, 0); return childAvailable !== undefined; }; @@ -717,11 +730,11 @@ define([ var cameraHeight = frameState.camera.positionCartographic.height; if (Math.abs(cameraHeight - ancestorMin) < Math.abs(cameraHeight - ancestorMax)) { - tileBoundingRegion.minimumHeight = ancestorMin; - tileBoundingRegion.maximumHeight = ancestorMin; - } else { tileBoundingRegion.minimumHeight = ancestorMax; tileBoundingRegion.maximumHeight = ancestorMax; + } else { + tileBoundingRegion.minimumHeight = ancestorMin; + tileBoundingRegion.maximumHeight = ancestorMin; } } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index c7195b4cfff..e5c58e7a05b 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -534,6 +534,31 @@ define([ } } + function getState(action, heightSource, renderable) { + return `A:${action} HS:${heightSource !== undefined ? heightSource.level : 'undefined'} R:${renderable}`; + } + + var lastFrame; + + function reportTileAction(frameState, tile, action) { + var heightSource = tile.data.boundingVolumeSourceTile; + var renderable = tile.renderable; + if (tile._lastAction !== action || tile._lastHeightSource !== heightSource || tile._lastRenderable !== renderable) { + if (lastFrame !== frameState.frameNumber) { + console.log('****** FRAME ' + frameState.frameNumber); + lastFrame = frameState.frameNumber; + } + + var tileID = `L${tile.level}X${tile.x}Y${tile.y}`; + var emphasize = tile._lastAction !== undefined && tile._lastAction !== action ? '*' : ''; + console.log(`${emphasize}${tileID} NOW ${getState(action, heightSource, renderable)} WAS ${getState(tile._lastAction, tile._lastHeightSource, tile._lastRenderable)}`); + + tile._lastAction = action; + tile._lastHeightSource = heightSource; + tile._lastRenderable = renderable; + } + } + function visitTile(primitive, frameState, tile, nearestRenderableTile) { var debug = primitive._debug; @@ -551,6 +576,7 @@ define([ } if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) { + reportTileAction(frameState, tile, 'meets SSE'); // This tile meets SSE requirements, so render it. if (tile.needsLoading) { // Rendered tile meeting SSE loads with medium priority. @@ -571,6 +597,7 @@ define([ if (tileProvider.canRefine(tile)) { if (allAreUpsampled) { + reportTileAction(frameState, tile, 'all upsampled'); // No point in rendering the children because they're all upsampled. Render this tile instead. addTileToRenderList(primitive, tile, nearestRenderableTile); @@ -584,6 +611,8 @@ define([ primitive._tileLoadQueueMedium.push(tile); } } else { + reportTileAction(frameState, tile, 'refine'); + // SSE is not good enough and children are loaded, so refine. // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); @@ -594,6 +623,7 @@ define([ } } } else { + reportTileAction(frameState, tile, 'can\'t refine'); // We'd like to refine but can't because not all of our children are renderable. Load the refinement blockers with high priority and // render this tile in the meantime. queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild); @@ -686,6 +716,7 @@ define([ if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { visitTile(primitive, frameState, tile, nearestRenderableTile); } else { + reportTileAction(frameState, tile, 'culled'); ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); } diff --git a/terrainnotes.md b/terrainnotes.md index 2ce36ffb28c..cd2710cc12f 100644 --- a/terrainnotes.md +++ b/terrainnotes.md @@ -5,6 +5,7 @@ To refine past a tile, one of the following conditions must be met: * The distance to the tile is known accurately (e.g. we know the precise min/max height), or * The bounding volume for the tile is known to be X or _closer_, and X is sufficient to necessitate refinement. For example, if we have no min/max height information for this tile, but the parent tile has a min of A and a max of B, we know the child must fall within these bounds as well. Therefore, the child tile can be no farther away than max(distance to A, distance to B). +Idea: when a tile is determined to be _fully_ visible based on min/max heights from an ancestor, and it meets the SSE, we can be certain that either that tile or its children are relevant for rendering. But if its only partially visible, we might load it only to find out that it's culled. So, when we have BVH data available, and a tile is only partially visible based on estimated heights, we should load the nearest ancestor BVH node instead of loading the tile. GlobeSurfaceTile properties: From ee388ff9a13311cd8cbbba66070dadc097c02f2f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 25 Jun 2018 16:00:29 +1000 Subject: [PATCH 012/131] Load tiles near viewport center first. Somewhat hackily at the moment. --- Source/Scene/GlobeSurfaceTileProvider.js | 6 ++++++ Source/Scene/QuadtreePrimitive.js | 25 ++++++++++++++++++++++++ terrainnotes.md | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index ad0fcc24240..c8fc95677b6 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -504,6 +504,8 @@ define([ GlobeSurfaceTileProvider.prototype.loadTile = function(frameState, tile) { GlobeSurfaceTile.processStateMachine(tile, frameState, this._terrainProvider, this._imageryLayers, this._vertexArraysToDestroy); var tileLoadedEvent = this._tileLoadedEvent; + + // TODO: creating a new function for every loaded tile every frame?! tile._loadedCallbacks['tileLoadedEvent'] = function (tile) { tileLoadedEvent.raiseEvent(); return true; @@ -670,6 +672,10 @@ define([ if (nearestRenderableTile !== undefined && nearestRenderableTile !== tile) { surfaceTile.renderableTile = nearestRenderableTile; + // The renderable tile may have previously deferred to an ancestor. + // But we know it's renderable now, so mark it as such. + nearestRenderableTile.data.renderableTile = undefined; + var myRectangle = tile.rectangle; var ancestorRectangle = nearestRenderableTile.rectangle; var ancestorSubset = surfaceTile.renderableTileSubset; diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index e5c58e7a05b..92da2aac8e2 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -541,6 +541,7 @@ define([ var lastFrame; function reportTileAction(frameState, tile, action) { + return; var heightSource = tile.data.boundingVolumeSourceTile; var renderable = tile.renderable; if (tile._lastAction !== action || tile._lastHeightSource !== heightSource || tile._lastRenderable !== renderable) { @@ -791,7 +792,31 @@ define([ processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow); } + function sortByCenterness(a, b) { + return a._centerness - b._centerness; + } + + var tileDirectionScratch = new Cartesian3(); + function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue) { + var cameraPosition = frameState.camera.positionWC; + var cameraDirection = frameState.camera.directionWC; + for (var i = 0, len = loadQueue.length; i < len && getTimestamp() < endTime; ++i) { + var tile = loadQueue[i]; + var surfaceTile = tile.data; + if (surfaceTile === undefined) { + continue; + } + var obb = surfaceTile.orientedBoundingBox; + if (obb === undefined) { + continue; + } + var tileDirection = Cartesian3.normalize(Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch), tileDirectionScratch); + tile._centerness = (1.0 - Cartesian3.dot(tileDirection, cameraDirection)) * tile._distance; + } + + loadQueue.sort(sortByCenterness); + for (var i = 0, len = loadQueue.length; i < len && getTimestamp() < endTime; ++i) { var tile = loadQueue[i]; primitive._tileReplacementQueue.markTileRendered(tile); diff --git a/terrainnotes.md b/terrainnotes.md index cd2710cc12f..2c07717e9f8 100644 --- a/terrainnotes.md +++ b/terrainnotes.md @@ -5,7 +5,7 @@ To refine past a tile, one of the following conditions must be met: * The distance to the tile is known accurately (e.g. we know the precise min/max height), or * The bounding volume for the tile is known to be X or _closer_, and X is sufficient to necessitate refinement. For example, if we have no min/max height information for this tile, but the parent tile has a min of A and a max of B, we know the child must fall within these bounds as well. Therefore, the child tile can be no farther away than max(distance to A, distance to B). -Idea: when a tile is determined to be _fully_ visible based on min/max heights from an ancestor, and it meets the SSE, we can be certain that either that tile or its children are relevant for rendering. But if its only partially visible, we might load it only to find out that it's culled. So, when we have BVH data available, and a tile is only partially visible based on estimated heights, we should load the nearest ancestor BVH node instead of loading the tile. +Idea: when a tile is determined to be _fully_ visible based on min/max heights from an ancestor, and it meets the SSE, we can be certain that either that tile or its children are relevant for rendering. But if it's only partially visible, we might load it only to find out that it's culled. So, when we have BVH data available, and a tile is only partially visible based on estimated heights, we should load the nearest ancestor BVH node instead of loading the tile. GlobeSurfaceTile properties: From 827c8bc729ec81bed49bd594b367608cf2a93c62 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 26 Jun 2018 14:36:17 +1000 Subject: [PATCH 013/131] Improve tile load prioritization. --- Source/Core/RequestScheduler.js | 10 +-- Source/Scene/GlobeSurfaceTile.js | 25 ++----- Source/Scene/GlobeSurfaceTileProvider.js | 25 +++++++ Source/Scene/Imagery.js | 4 +- Source/Scene/ImageryLayer.js | 8 +-- Source/Scene/QuadtreePrimitive.js | 91 ++++++++++-------------- Source/Scene/QuadtreeTile.js | 6 +- Source/Scene/TileBoundingRegion.js | 18 +++-- Source/Scene/TileImagery.js | 4 +- 9 files changed, 98 insertions(+), 93 deletions(-) diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index 6fd455b5800..a60a5696efe 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -342,6 +342,11 @@ define([ request.serverKey = RequestScheduler.getServerKey(request.url); } + if (request.throttleByServer && !serverHasOpenSlots(request.serverKey)) { + // Server is saturated. Try again later. + return undefined; + } + if (!RequestScheduler.throttleRequests || !request.throttle) { return startRequest(request); } @@ -351,11 +356,6 @@ define([ return undefined; } - if (request.throttleByServer && !serverHasOpenSlots(request.serverKey)) { - // Server is saturated. Try again later. - return undefined; - } - // Insert into the priority heap and see if a request was bumped off. If this request is the lowest // priority it will be returned. updatePriority(request); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index e8c8ec19aa4..b0f38bafcdb 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -293,12 +293,6 @@ define([ }); } - function createPriorityFunction(surfaceTile, frameState) { - return function() { - return surfaceTile.tileBoundingRegion.distanceToCamera(frameState); - }; - } - GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { var surfaceTile = tile.data; if (!defined(surfaceTile)) { @@ -308,11 +302,6 @@ define([ surfaceTile.tileBoundingRegion = createTileBoundingRegion(tile); } - if (!defined(tile._priorityFunction)) { - // The priority function is used to prioritize requests among all requested tiles - tile._priorityFunction = createPriorityFunction(surfaceTile, frameState); - } - if (tile.state === QuadtreeTileLoadState.START) { prepareNewTile(tile, terrainProvider, imageryLayerCollection); tile.state = QuadtreeTileLoadState.LOADING; @@ -389,7 +378,6 @@ define([ tile._loadedCallbacks = newCallbacks; tile.state = QuadtreeTileLoadState.DONE; - tile._priorityFunction = undefined; } } }; @@ -481,11 +469,11 @@ define([ } if (surfaceTile.terrainState === TerrainState.FAILED) { - upsample(surfaceTile, tile, frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); + upsample(surfaceTile, tile, frameState, terrainProvider, tile.x, tile.y, tile.level); } if (surfaceTile.terrainState === TerrainState.UNLOADED) { - requestTileGeometry(surfaceTile, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); + requestTileGeometry(surfaceTile, terrainProvider, tile.x, tile.y, tile.level); } if (surfaceTile.terrainState === TerrainState.RECEIVED) { @@ -497,7 +485,7 @@ define([ } } - function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level, priorityFunction) { + function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level) { var parent = tile.parent; if (!parent) { // Trying to upsample from a root tile. No can do. @@ -530,7 +518,7 @@ define([ }); } - function requestTileGeometry(surfaceTile, terrainProvider, x, y, level, priorityFunction) { + function requestTileGeometry(surfaceTile, terrainProvider, x, y, level) { function success(terrainData) { surfaceTile.terrainData = terrainData; surfaceTile.terrainState = TerrainState.RECEIVED; @@ -564,10 +552,9 @@ define([ function doRequest() { // Request the terrain from the terrain provider. var request = new Request({ - throttle : true, + throttle : false, throttleByServer : true, - type : RequestType.TERRAIN, - priorityFunction : priorityFunction + type : RequestType.TERRAIN }); surfaceTile.request = request; var requestPromise = terrainProvider.requestTileGeometry(x, y, level, request); diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index c8fc95677b6..c3472727b3e 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -633,6 +633,31 @@ define([ return childAvailable !== undefined; }; + var tileDirectionScratch = new Cartesian3(); + + /** + * Determines the priority for loading this tile. Lower priority values load sooner. + * @param {QuatreeTile} tile The tile. + * @param {FrameState} frameState The frame state. + * @returns {Number} The load priority value. + */ + GlobeSurfaceTileProvider.prototype.computeTileLoadPriority = function(tile, frameState) { + var surfaceTile = tile.data; + if (surfaceTile === undefined) { + return 0.0; + } + + var obb = surfaceTile.orientedBoundingBox; + if (obb === undefined) { + return 0.0; + } + + var cameraPosition = frameState.camera.positionWC; + var cameraDirection = frameState.camera.directionWC; + var tileDirection = Cartesian3.normalize(Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch), tileDirectionScratch); + return (1.0 - Cartesian3.dot(tileDirection, cameraDirection)) * tile._distance; + }; + var modifiedModelViewScratch = new Matrix4(); var modifiedModelViewProjectionScratch = new Matrix4(); var tileRectangleScratch = new Cartesian4(); diff --git a/Source/Scene/Imagery.js b/Source/Scene/Imagery.js index e486a2a804e..40520117dfe 100644 --- a/Source/Scene/Imagery.js +++ b/Source/Scene/Imagery.js @@ -84,10 +84,10 @@ define([ return this.referenceCount; }; - Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, priorityFunction) { + Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection) { if (this.state === ImageryState.UNLOADED) { this.state = ImageryState.TRANSITIONING; - this.imageryLayer._requestImagery(this, priorityFunction); + this.imageryLayer._requestImagery(this); } if (this.state === ImageryState.RECEIVED) { diff --git a/Source/Scene/ImageryLayer.js b/Source/Scene/ImageryLayer.js index 5764b35f46f..e55e7284f4a 100644 --- a/Source/Scene/ImageryLayer.js +++ b/Source/Scene/ImageryLayer.js @@ -712,9 +712,8 @@ define([ * @private * * @param {Imagery} imagery The imagery to request. - * @param {Function} [priorityFunction] The priority function used for sorting the imagery request. */ - ImageryLayer.prototype._requestImagery = function(imagery, priorityFunction) { + ImageryLayer.prototype._requestImagery = function(imagery) { var imageryProvider = this._imageryProvider; var that = this; @@ -757,10 +756,9 @@ define([ function doRequest() { var request = new Request({ - throttle : true, + throttle : false, throttleByServer : true, - type : RequestType.IMAGERY, - priorityFunction : priorityFunction + type : RequestType.IMAGERY }); imagery.request = request; imagery.state = ImageryState.TRANSITIONING; diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 92da2aac8e2..c1ef5e81d30 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -519,21 +519,28 @@ define([ tile = levelZeroTiles[i]; primitive._tileReplacementQueue.markTileRendered(tile); if (!tile.renderable) { - if (tile.needsLoading) { - primitive._tileLoadQueueHigh.push(tile); - } + queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); ++debug.tilesWaitingForChildren; } else if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { visitTile(primitive, frameState, tile, tile); } else { - if (tile.needsLoading) { - primitive._tileLoadQueueLow.push(tile); - } + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); ++debug.tilesCulled; } } } + function queueTileLoad(primitive, queue, tile, frameState) { + if (!tile.needsLoading) { + return; + } + + if (primitive.computeTileLoadPriority !== undefined) { + tile._loadPriority = primitive.computeTileLoadPriority(tile, frameState); + } + queue.push(tile); + } + function getState(action, heightSource, renderable) { return `A:${action} HS:${heightSource !== undefined ? heightSource.level : 'undefined'} R:${renderable}`; } @@ -541,7 +548,7 @@ define([ var lastFrame; function reportTileAction(frameState, tile, action) { - return; + //return; var heightSource = tile.data.boundingVolumeSourceTile; var renderable = tile.renderable; if (tile._lastAction !== action || tile._lastHeightSource !== heightSource || tile._lastRenderable !== renderable) { @@ -578,11 +585,8 @@ define([ if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) { reportTileAction(frameState, tile, 'meets SSE'); - // This tile meets SSE requirements, so render it. - if (tile.needsLoading) { - // Rendered tile meeting SSE loads with medium priority. - primitive._tileLoadQueueMedium.push(tile); - } + // This tile meets SSE requirements, so render it and load it with medium priority. + queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); addTileToRenderList(primitive, tile, nearestRenderableTile); return; } @@ -591,12 +595,13 @@ define([ var southeastChild = tile.southeastChild; var northwestChild = tile.northwestChild; var northeastChild = tile.northeastChild; - var allAreUpsampled = southwestChild.upsampledFromParent && southeastChild.upsampledFromParent && - northwestChild.upsampledFromParent && northeastChild.upsampledFromParent; var tileProvider = primitive.tileProvider; if (tileProvider.canRefine(tile)) { + var allAreUpsampled = southwestChild.upsampledFromParent && southeastChild.upsampledFromParent && + northwestChild.upsampledFromParent && northeastChild.upsampledFromParent; + if (allAreUpsampled) { reportTileAction(frameState, tile, 'all upsampled'); // No point in rendering the children because they're all upsampled. Render this tile instead. @@ -607,10 +612,8 @@ define([ // A tile that is upsampled now and forever should also be done loading, so no harm done. queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild); - if (tile.needsLoading) { - // Rendered tile that's not waiting on children loads with medium priority. - primitive._tileLoadQueueMedium.push(tile); - } + // Rendered tile that's not waiting on children loads with medium priority. + queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); } else { reportTileAction(frameState, tile, 'refine'); @@ -618,22 +621,15 @@ define([ // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); - if (tile.needsLoading) { - // Tile is not rendered, so load it with low priority. - primitive._tileLoadQueueLow.push(tile); - } + // Tile is not rendered, so load it with low priority. + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } } else { reportTileAction(frameState, tile, 'can\'t refine'); - // We'd like to refine but can't because not all of our children are renderable. Load the refinement blockers with high priority and - // render this tile in the meantime. - queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild); - addTileToRenderList(primitive, tile, nearestRenderableTile); - if (tile.needsLoading) { - // We will refine this tile when it's possible, so load this tile only with low priority. - primitive._tileLoadQueueLow.push(tile); - } + // We'd like to refine but can't because this tile isn't loaded yet. + addTileToRenderList(primitive, tile, nearestRenderableTile); + queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); } } @@ -787,40 +783,25 @@ define([ var endTime = getTimestamp() + primitive._loadQueueTimeSlice; var tileProvider = primitive._tileProvider; - processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueHigh); - processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueMedium); - processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow); + var didSomething = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueHigh, false); + didSomething = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueMedium, didSomething); + processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow, didSomething); } - function sortByCenterness(a, b) { - return a._centerness - b._centerness; + function sortByLoadPriority(a, b) { + return a._loadPriority - b._loadPriority; } - var tileDirectionScratch = new Cartesian3(); - - function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue) { - var cameraPosition = frameState.camera.positionWC; - var cameraDirection = frameState.camera.directionWC; - for (var i = 0, len = loadQueue.length; i < len && getTimestamp() < endTime; ++i) { - var tile = loadQueue[i]; - var surfaceTile = tile.data; - if (surfaceTile === undefined) { - continue; - } - var obb = surfaceTile.orientedBoundingBox; - if (obb === undefined) { - continue; - } - var tileDirection = Cartesian3.normalize(Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch), tileDirectionScratch); - tile._centerness = (1.0 - Cartesian3.dot(tileDirection, cameraDirection)) * tile._distance; + function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue, didSomething) { + if (tileProvider.computeTileLoadPriority !== undefined) { + loadQueue.sort(sortByLoadPriority); } - loadQueue.sort(sortByCenterness); - - for (var i = 0, len = loadQueue.length; i < len && getTimestamp() < endTime; ++i) { + for (var i = 0, len = loadQueue.length; i < len && (getTimestamp() < endTime || !didSomething); ++i) { var tile = loadQueue[i]; primitive._tileReplacementQueue.markTileRendered(tile); tileProvider.loadTile(frameState, tile); + didSomething = true; } } diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index b36ef75f244..7e83f682b16 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -66,7 +66,7 @@ define([ // distance - for example, by using the natural ordering of a quadtree. // QuadtreePrimitive gets/sets this private property. this._distance = 0.0; - this._priorityFunction = undefined; + this._loadPriority = 0.0; this._customData = []; this._frameUpdated = undefined; @@ -106,6 +106,10 @@ define([ this.data = undefined; } + function getPriority() { + return this._priority; + } + /** * Creates a rectangular set of tiles for level of detail zero, the coarsest, least detailed level. * diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js index aa7a070c119..8abb7bd9442 100644 --- a/Source/Scene/TileBoundingRegion.js +++ b/Source/Scene/TileBoundingRegion.js @@ -304,16 +304,26 @@ define([ } var cameraHeight; + var minimumHeight; + var maximumHeight; if (frameState.mode === SceneMode.SCENE3D) { cameraHeight = cameraCartographicPosition.height; + minimumHeight = this.minimumHeight; + maximumHeight = this.maximumHeight; } else { cameraHeight = cameraCartesianPosition.x; + minimumHeight = 0.0; + maximumHeight = 0.0; } - var maximumHeight = frameState.mode === SceneMode.SCENE3D ? this.maximumHeight : 0.0; - var distanceFromTop = cameraHeight - maximumHeight; - if (distanceFromTop > 0.0) { - result += distanceFromTop * distanceFromTop; + var distanceAboveTop = cameraHeight - maximumHeight; + if (distanceAboveTop > 0.0) { + result += distanceAboveTop * distanceAboveTop; + } else { + var distanceBelowBottom = minimumHeight - cameraHeight; + if (distanceBelowBottom > 0.0) { + result += distanceBelowBottom * distanceBelowBottom; + } } return Math.sqrt(result); diff --git a/Source/Scene/TileImagery.js b/Source/Scene/TileImagery.js index 592ca81550f..2e0d4a2a216 100644 --- a/Source/Scene/TileImagery.js +++ b/Source/Scene/TileImagery.js @@ -49,7 +49,7 @@ define([ var loadingImagery = this.loadingImagery; var imageryLayer = loadingImagery.imageryLayer; - loadingImagery.processStateMachine(frameState, !this.useWebMercatorT, tile._priorityFunction); + loadingImagery.processStateMachine(frameState, !this.useWebMercatorT); if (loadingImagery.state === ImageryState.READY) { if (defined(this.readyImagery)) { @@ -91,7 +91,7 @@ define([ // Push the ancestor's load process along a bit. This is necessary because some ancestor imagery // tiles may not be attached directly to a terrain tile. Such tiles will never load if // we don't do it here. - closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT, tile._priorityFunction); + closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT); return false; // not done loading } // This imagery tile is failed or invalid, and we have the "best available" substitute. From 7e93c95b1d091d1e18588dc32a10e8db0e6ffa7d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 26 Jun 2018 16:07:45 +1000 Subject: [PATCH 014/131] Tiny optimization. --- Source/Scene/QuadtreePrimitive.js | 2 +- Source/Scene/TileBoundingRegion.js | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index c1ef5e81d30..eccc67a0295 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -548,7 +548,7 @@ define([ var lastFrame; function reportTileAction(frameState, tile, action) { - //return; + return; var heightSource = tile.data.boundingVolumeSourceTile; var renderable = tile.renderable; if (tile._lastAction !== action || tile._lastHeightSource !== heightSource || tile._lastRenderable !== renderable) { diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js index 8abb7bd9442..215efd64294 100644 --- a/Source/Scene/TileBoundingRegion.js +++ b/Source/Scene/TileBoundingRegion.js @@ -316,14 +316,12 @@ define([ maximumHeight = 0.0; } - var distanceAboveTop = cameraHeight - maximumHeight; - if (distanceAboveTop > 0.0) { + if (cameraHeight > maximumHeight) { + var distanceAboveTop = cameraHeight - maximumHeight; result += distanceAboveTop * distanceAboveTop; - } else { + } else if (cameraHeight < minimumHeight) { var distanceBelowBottom = minimumHeight - cameraHeight; - if (distanceBelowBottom > 0.0) { - result += distanceBelowBottom * distanceBelowBottom; - } + result += distanceBelowBottom * distanceBelowBottom; } return Math.sqrt(result); From a4de6d473eb19ac02dee47efc1174d470aa43066 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 26 Jun 2018 17:25:46 +1000 Subject: [PATCH 015/131] Don't render a million subsets of the same tile for now reason. --- Source/Core/Request.js | 2 +- Source/Scene/QuadtreePrimitive.js | 59 +++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/Source/Core/Request.js b/Source/Core/Request.js index ba8d57f5066..1cc44f22c01 100644 --- a/Source/Core/Request.js +++ b/Source/Core/Request.js @@ -30,7 +30,7 @@ define([ options = defaultValue(options, defaultValue.EMPTY_OBJECT); var throttleByServer = defaultValue(options.throttleByServer, false); - var throttle = throttleByServer || defaultValue(options.throttle, false); + var throttle = defaultValue(options.throttle, false); /** * The URL to request. diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index eccc67a0295..2347c681835 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -588,7 +588,7 @@ define([ // This tile meets SSE requirements, so render it and load it with medium priority. queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); addTileToRenderList(primitive, tile, nearestRenderableTile); - return; + return tile.renderable; } var southwestChild = tile.southwestChild; @@ -614,15 +614,29 @@ define([ // Rendered tile that's not waiting on children loads with medium priority. queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + + return tile.renderable; } else { reportTileAction(frameState, tile, 'refine'); + var firstRenderedDescendantIndex = primitive._tilesToRender.length; + // SSE is not good enough and children are loaded, so refine. // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. - visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); + var anyAreRenderable = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); + + if (!anyAreRenderable && firstRenderedDescendantIndex !== primitive._tilesToRender.length) { + // None of our descendants are renderable, so they'll all end up rendering an ancestor. + // That's a big waste of time, so kick them all out of the render list and render this tile instead. + primitive._tilesToRender.length = firstRenderedDescendantIndex; + primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; + addTileToRenderList(primitive, tile, nearestRenderableTile); + } // Tile is not rendered, so load it with low priority. queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + + return anyAreRenderable || tile.renderable; } } else { reportTileAction(frameState, tile, 'can\'t refine'); @@ -630,6 +644,8 @@ define([ // We'd like to refine but can't because this tile isn't loaded yet. addTileToRenderList(primitive, tile, nearestRenderableTile); queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); + + return tile.renderable; } } @@ -680,42 +696,47 @@ define([ var tileProvider = primitive._tileProvider; var occluders = primitive._occluders; + var anyAreRenderable = false; + if (cameraPosition.longitude < southwest.rectangle.east) { if (cameraPosition.latitude < southwest.rectangle.north) { // Camera in southwest quadrant - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; } else { // Camera in northwest quadrant - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; } } else if (cameraPosition.latitude < southwest.rectangle.north) { // Camera southeast quadrant - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; } else { // Camera in northeast quadrant - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; } + + return anyAreRenderable; } function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile) { if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { - visitTile(primitive, frameState, tile, nearestRenderableTile); + return visitTile(primitive, frameState, tile, nearestRenderableTile); } else { reportTileAction(frameState, tile, 'culled'); ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); + return false; } } From 72e548e190720314f1bbd4ccbe328986ceee996b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 28 Jun 2018 22:42:00 +1000 Subject: [PATCH 016/131] Optimize out heaps of unnecessary loading. --- Source/Core/CesiumTerrainProvider.js | 31 ++++++++ Source/Core/QuantizedMeshTerrainData.js | 56 +++++++++++++++ Source/Scene/GlobeSurfaceTile.js | 70 ++++++++++++------ Source/Scene/GlobeSurfaceTileProvider.js | 90 +++++++++++++----------- Source/Scene/QuadtreePrimitive.js | 21 +++--- 5 files changed, 197 insertions(+), 71 deletions(-) diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index e4965f5f438..aaba4bc315b 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -57,6 +57,7 @@ define([ this.hasVertexNormals = layer.hasVertexNormals; this.hasWaterMask = layer.hasWaterMask; this.hasBvh = layer.hasBvh; + this.bvhLevels = layer.bvhLevels; this.littleEndianExtensionSize = layer.littleEndianExtensionSize; } @@ -291,6 +292,7 @@ define([ hasVertexNormals: hasVertexNormals, hasWaterMask: hasWaterMask, hasBvh: hasBvh, + bvhLevels: data.bvhlevels, littleEndianExtensionSize: littleEndianExtensionSize })); @@ -596,6 +598,35 @@ define([ }); } + CesiumTerrainProvider.prototype.getNearestBvhLevel = function(x, y, level) { + var layers = this._layers; + var layerToUse; + var layerCount = layers.length; + + if (layerCount === 1) { // Optimized path for single layers + layerToUse = layers[0]; + } else { + for (var i = 0; i < layerCount; ++i) { + var layer = layers[i]; + if (!defined(layer.availability) || layer.availability.isTileAvailable(level, x, y)) { + layerToUse = layer; + break; + } + } + } + + if (!defined(layerToUse)) { + return when.reject(new RuntimeError('Terrain tile doesn\'t exist')); + } + + if (!layerToUse.hasBvh) { + return -1; + } + + var bvhLevels = layerToUse.bvhLevels; + return ((level / bvhLevels) | 0) * bvhLevels; + }; + /** * Requests the geometry for a given tile. This function should not be called before * {@link CesiumTerrainProvider#ready} returns true. The result must include terrain data and diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index 79ece87d5e6..6643ae2b24f 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -521,6 +521,62 @@ define([ }; var maxShort = 32767; + + // QuantizedMeshTerrainData.prototype.getFastMinMaxHeightsOfSubset = function(thisRectangle, descendantRectangle) { + // var minU = maxShort * (descendantRectangle.west - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); + // var maxU = maxShort * (descendantRectangle.east - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); + // var minV = maxShort * (descendantRectangle.south - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); + // var maxV = maxShort * (descendantRectangle.north - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); + + // var indices = this._indices; + // var uBuffer = this._uValues; + // var vBuffer = this._vValues; + // var heightBuffer = this._heightValues; + + // var minHeight = maxShort; + // var maxHeight = 0; + + // //CesiumMath.lerp(terrainData._minimumHeight, terrainData._maximumHeight, quantizedHeight / maxShort) + + // for (var i = 0, len = indices.length; i < len; i += 3) { + // var i0 = indices[i]; + // var i1 = indices[i + 1]; + // var i2 = indices[i + 2]; + + // var u0 = uBuffer[i0]; + // var u1 = uBuffer[i1]; + // var u2 = uBuffer[i2]; + + // var v0 = vBuffer[i0]; + // var v1 = vBuffer[i1]; + // var v2 = vBuffer[i2]; + + // var relevant = false; + // if (u0 >= minU && u0 <= maxU && v0 >= minV && v0 <= maxV || + // u1 >= minU && u1 <= maxU && v1 >= minV && v1 <= maxV || + // u2 >= minU && u2 <= maxU && v2 >= minV && v2 <= maxV) { + // // At least one vertex is inside the descendant bounds + // relevant = true; + // } else if (u0 < minU && u1 < minU && u2 < minU || + // u0 > maxU && u1 > maxU && u2 > maxU || + // v0 < minV && v1 < minV && v2 < minV || + // v0 > maxV && v1 > maxV && v2 < maxV) { + // // Triangle is entirely on a single side of a descendant border. + // relevant = false; + // } else if () + + // if (relevant) { + // minHeight = Math.min(minHeight, heightBuffer[i0]); + // minHeight = Math.min(minHeight, heightBuffer[i1]); + // minHeight = Math.min(minHeight, heightBuffer[i2]); + + // maxHeight = Math.max(maxHeight, heightBuffer[i0]); + // maxHeight = Math.max(maxHeight, heightBuffer[i1]); + // maxHeight = Math.max(maxHeight, heightBuffer[i2]); + // } + // } + // }; + var barycentricCoordinateScratch = new Cartesian3(); /** diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index b0f38bafcdb..c67a0d54977 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -277,29 +277,26 @@ define([ } }; - function createTileBoundingRegion(tile) { - var minimumHeight; - var maximumHeight; - if (defined(tile.parent) && defined(tile.parent.data)) { - minimumHeight = tile.parent.data.minimumHeight; - maximumHeight = tile.parent.data.maximumHeight; - } - return new TileBoundingRegion({ - computeBoundingVolumes : false, - rectangle : tile.rectangle, - ellipsoid : tile.tilingScheme.ellipsoid, - minimumHeight : minimumHeight, - maximumHeight : maximumHeight - }); - } - - GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { + // var renderedJson = [{"level":18,"x":87944,"y":76105},{"level":18,"x":87945,"y":76105},{"level":19,"x":175890,"y":152209},{"level":19,"x":175891,"y":152209},{"level":19,"x":175892,"y":152210},{"level":19,"x":175892,"y":152208},{"level":18,"x":87947,"y":76104},{"level":18,"x":87948,"y":76103},{"level":18,"x":87949,"y":76102},{"level":17,"x":43975,"y":38051},{"level":17,"x":43975,"y":38050},{"level":16,"x":21991,"y":19026},{"level":16,"x":21991,"y":19027},{"level":16,"x":21989,"y":19025},{"level":17,"x":43977,"y":38049},{"level":16,"x":21989,"y":19024},{"level":16,"x":21990,"y":19025},{"level":16,"x":21991,"y":19025},{"level":16,"x":21990,"y":19024},{"level":16,"x":21991,"y":19024},{"level":16,"x":21991,"y":19028},{"level":16,"x":21991,"y":19029},{"level":16,"x":21991,"y":19030},{"level":16,"x":21992,"y":19026},{"level":16,"x":21992,"y":19027},{"level":16,"x":21993,"y":19026},{"level":16,"x":21993,"y":19027},{"level":15,"x":10997,"y":9513},{"level":15,"x":10996,"y":9512},{"level":15,"x":10997,"y":9512},{"level":16,"x":21992,"y":19028},{"level":16,"x":21992,"y":19029},{"level":16,"x":21993,"y":19028},{"level":16,"x":21993,"y":19029},{"level":15,"x":10996,"y":9515},{"level":15,"x":10997,"y":9514},{"level":15,"x":10997,"y":9515},{"level":15,"x":10998,"y":9513},{"level":15,"x":10999,"y":9513},{"level":15,"x":10998,"y":9512},{"level":15,"x":10999,"y":9512},{"level":15,"x":10998,"y":9514},{"level":15,"x":10998,"y":9515},{"level":15,"x":10999,"y":9514},{"level":15,"x":10999,"y":9515},{"level":15,"x":10998,"y":9516},{"level":15,"x":10999,"y":9516},{"level":14,"x":5500,"y":4756},{"level":14,"x":5500,"y":4757},{"level":14,"x":5501,"y":4756},{"level":14,"x":5501,"y":4757},{"level":14,"x":5500,"y":4758},{"level":14,"x":5501,"y":4758},{"level":14,"x":5501,"y":4759},{"level":14,"x":5502,"y":4756},{"level":14,"x":5502,"y":4757},{"level":14,"x":5503,"y":4756},{"level":14,"x":5503,"y":4757},{"level":14,"x":5502,"y":4758},{"level":14,"x":5502,"y":4759},{"level":14,"x":5503,"y":4758},{"level":14,"x":5503,"y":4759},{"level":16,"x":21991,"y":19023},{"level":16,"x":21993,"y":19023},{"level":16,"x":21993,"y":19022},{"level":15,"x":10997,"y":9511},{"level":15,"x":10998,"y":9511},{"level":15,"x":10999,"y":9511},{"level":15,"x":10998,"y":9510},{"level":15,"x":10999,"y":9510},{"level":14,"x":5500,"y":4755},{"level":14,"x":5501,"y":4755},{"level":14,"x":5500,"y":4754},{"level":14,"x":5501,"y":4754},{"level":14,"x":5502,"y":4755},{"level":14,"x":5503,"y":4755},{"level":14,"x":5502,"y":4754},{"level":14,"x":5503,"y":4754},{"level":14,"x":5503,"y":4753},{"level":13,"x":2751,"y":2380},{"level":13,"x":2752,"y":2378},{"level":13,"x":2752,"y":2379},{"level":13,"x":2753,"y":2378},{"level":13,"x":2753,"y":2379},{"level":13,"x":2754,"y":2378},{"level":13,"x":2754,"y":2379},{"level":13,"x":2755,"y":2378},{"level":13,"x":2755,"y":2379},{"level":13,"x":2752,"y":2377},{"level":13,"x":2753,"y":2377},{"level":13,"x":2752,"y":2376},{"level":13,"x":2753,"y":2376},{"level":13,"x":2754,"y":2377},{"level":13,"x":2755,"y":2377},{"level":13,"x":2754,"y":2376},{"level":13,"x":2752,"y":2380},{"level":13,"x":2753,"y":2380},{"level":13,"x":2753,"y":2381},{"level":12,"x":1377,"y":1190},{"level":12,"x":1377,"y":1191},{"level":12,"x":1378,"y":1189},{"level":12,"x":1379,"y":1189},{"level":12,"x":1378,"y":1188},{"level":12,"x":1379,"y":1188},{"level":12,"x":1378,"y":1190},{"level":12,"x":1378,"y":1191},{"level":12,"x":1379,"y":1190},{"level":11,"x":690,"y":594},{"level":11,"x":690,"y":595},{"level":11,"x":691,"y":594},{"level":11,"x":691,"y":595},{"level":12,"x":1377,"y":1187},{"level":12,"x":1379,"y":1187},{"level":12,"x":1379,"y":1186},{"level":11,"x":690,"y":593},{"level":11,"x":691,"y":593},{"level":11,"x":691,"y":592},{"level":11,"x":689,"y":596},{"level":11,"x":690,"y":596},{"level":11,"x":691,"y":596},{"level":11,"x":691,"y":597},{"level":10,"x":346,"y":297},{"level":10,"x":347,"y":297},{"level":10,"x":346,"y":296},{"level":10,"x":347,"y":296},{"level":10,"x":346,"y":298},{"level":10,"x":346,"y":299},{"level":10,"x":347,"y":298},{"level":10,"x":347,"y":299},{"level":9,"x":174,"y":148},{"level":9,"x":174,"y":149},{"level":9,"x":175,"y":148},{"level":9,"x":175,"y":149},{"level":9,"x":174,"y":150},{"level":9,"x":175,"y":150},{"level":10,"x":346,"y":295},{"level":10,"x":347,"y":295},{"level":9,"x":174,"y":147},{"level":9,"x":175,"y":147}]; + // var expectedTiles = {}; + // var unexpectedTiles = {}; + + // function tileID(level, x, y) { + // return `L${level}X${x}Y${y}`; + // } + // renderedJson.forEach(tile => { + // while (tile.level >= 0) { + // expectedTiles[tileID(tile.level, tile.x, tile.y)] = true; + // --tile.level; + // tile.x >>= 1; + // tile.y >>= 1; + // } + // }); + + GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, terrainOnly) { var surfaceTile = tile.data; if (!defined(surfaceTile)) { surfaceTile = tile.data = new GlobeSurfaceTile(); - // Create the TileBoundingRegion now in order to estimate the distance, which is used to prioritize the request. - // Since the terrain isn't loaded yet, estimate the heights using its parent's values. - surfaceTile.tileBoundingRegion = createTileBoundingRegion(tile); } if (tile.state === QuadtreeTileLoadState.START) { @@ -307,10 +304,35 @@ define([ tile.state = QuadtreeTileLoadState.LOADING; } + var bvhLevel = terrainProvider.getNearestBvhLevel(tile.x, tile.y, tile.level); + if (bvhLevel !== -1 && bvhLevel !== tile.level) { + var ancestor = tile.parent; + while (ancestor.level !== bvhLevel) { + ancestor = ancestor.parent; + } + + if (ancestor.data === undefined || ancestor.data.terrainData === undefined) { + GlobeSurfaceTile.processStateMachine(ancestor, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, true); + return; + } + } + if (tile.state === QuadtreeTileLoadState.LOADING) { processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); } + if (terrainOnly || (tile.level !== 0 && tile.data.boundingVolumeSourceTile !== tile)) { + return; + } + + // var id = tileID(tile.level, tile.x, tile.y); + // if (!expectedTiles[id]) { + // if (!unexpectedTiles[id]) { + // unexpectedTiles[id] = true; + // console.log('Unexpected: ' + id); + // } + // } + // The terrain is renderable as soon as we have a valid vertex array. var isRenderable = defined(surfaceTile.vertexArray); @@ -464,7 +486,7 @@ define([ var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined && parent.data.terrainData._mesh !== undefined; if (!parentReady) { //console.log('Waiting on L' + parent.level + 'X' + parent.x + 'Y' + parent.y); - GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); + GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, true); } } @@ -485,6 +507,9 @@ define([ } } + var upsamplesStarted = 0; + var upsamplesCompleted = 0; + function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level) { var parent = tile.parent; if (!parent) { @@ -508,9 +533,12 @@ define([ return; } + ++upsamplesStarted; + surfaceTile.terrainState = TerrainState.RECEIVING; when(terrainDataPromise, function(terrainData) { + ++upsamplesCompleted; surfaceTile.terrainData = terrainData; surfaceTile.terrainState = TerrainState.RECEIVED; }, function() { diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index c3472727b3e..b438da7ee61 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -549,26 +549,9 @@ define([ var surfaceTile = tile.data; var tileBoundingRegion = surfaceTile.tileBoundingRegion; - // We now need a bounding volume in order to determine visibility, but the tile - // may not be loaded yet. - if (surfaceTile.boundingVolumeSourceTile !== tile) { - var heightSource = updateTileBoundingRegion(tile, this.terrainProvider); - if (heightSource === undefined) { - // We have no idea where this tile is, so let's just call it visible. - return Visibility.PARTIAL; - } - - if (heightSource !== surfaceTile.boundingVolumeSourceTile) { - // Heights are from a new source tile, so update the bounding volume. - surfaceTile.boundingVolumeSourceTile = heightSource; - surfaceTile.orientedBoundingBox = OrientedBoundingBox.fromRectangle( - tile.rectangle, - tileBoundingRegion.minimumHeight, - tileBoundingRegion.maximumHeight, - tile.tilingScheme.ellipsoid, - surfaceTile.orientedBoundingBox); - surfaceTile.occludeePointInScaledSpace = undefined; // TODO - } + if (surfaceTile.boundingVolumeSourceTile === undefined) { + // We have no idea where this tile is, so let's just call it partially visible. + return Visibility.PARTIAL; } var cullingVolume = frameState.cullingVolume; @@ -642,6 +625,8 @@ define([ * @returns {Number} The load priority value. */ GlobeSurfaceTileProvider.prototype.computeTileLoadPriority = function(tile, frameState) { + return tile._distance; + var surfaceTile = tile.data; if (surfaceTile === undefined) { return 0.0; @@ -718,6 +703,8 @@ define([ debug.texturesRendered += readyTextureCount; }; + var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()]; + /** * Gets the distance from the camera to the closest point on the tile. This is used for level-of-detail selection. * @@ -742,40 +729,61 @@ define([ // better (faster) than loading tiles that are too detailed. var heightSource = updateTileBoundingRegion(tile, this.terrainProvider); - var surfaceTile = tile.data; var tileBoundingRegion = surfaceTile.tileBoundingRegion; - if (heightSource !== tile) { - if (heightSource === undefined) { - // Can't find any min/max heights anywhere? Ok, let's just say the - // tile is really far away so we'll load and render it rather than - // refining. - return 9999999999.0; - } + if (heightSource === undefined) { + // Can't find any min/max heights anywhere? Ok, let's just say the + // tile is really far away so we'll load and render it rather than + // refining. + return 9999999999.0; + } else if (surfaceTile.boundingVolumeSourceTile !== heightSource) { + // Heights are from a new source tile, so update the bounding volume. + surfaceTile.boundingVolumeSourceTile = heightSource; + surfaceTile.orientedBoundingBox = OrientedBoundingBox.fromRectangle( + tile.rectangle, + tileBoundingRegion.minimumHeight, + tileBoundingRegion.maximumHeight, + tile.tilingScheme.ellipsoid, + surfaceTile.orientedBoundingBox); + + var rectangle = tile.rectangle; + var height = tileBoundingRegion.minimumHeight; + + var ellipsoidalOccluder = this.quadtree._occluders.ellipsoid; + var ellipsoid = ellipsoidalOccluder.ellipsoid; + + var cornerPositions = cornerPositionsScratch; + Cartesian3.fromRadians(rectangle.west, rectangle.south, height, ellipsoid, cornerPositions[0]); + Cartesian3.fromRadians(rectangle.east, rectangle.south, height, ellipsoid, cornerPositions[1]); + Cartesian3.fromRadians(rectangle.west, rectangle.north, height, ellipsoid, cornerPositions[2]); + Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]); + + surfaceTile.occludeePointInScaledSpace = ellipsoidalOccluder.computeHorizonCullingPoint(surfaceTile.orientedBoundingBox.center, cornerPositions, surfaceTile.occludeePointInScaledSpace); + } - // Make the bounding region a pancake located at the farthest-away - // ancestor height surface. - var ancestorMin = tileBoundingRegion.minimumHeight; - var ancestorMax = tileBoundingRegion.maximumHeight; + // Compute the distance to the OBB + var distanceToObb = Math.sqrt(surfaceTile.orientedBoundingBox.distanceSquaredTo(frameState.camera.positionWC)); - var cameraHeight = frameState.camera.positionCartographic.height; - if (Math.abs(cameraHeight - ancestorMin) < Math.abs(cameraHeight - ancestorMax)) { - tileBoundingRegion.minimumHeight = ancestorMax; - tileBoundingRegion.maximumHeight = ancestorMax; - } else { - tileBoundingRegion.minimumHeight = ancestorMin; - tileBoundingRegion.maximumHeight = ancestorMin; - } + if (surfaceTile.boundingVolumeSourceTile !== tile) { + // Bounding volume is approximate, so, as described above, we want the distance to the + // farther-away height surface, not the closer one. The following is a super quick-and-dirty + // way to compute a distance that is that _or farther_. + // TODO: could do a better job approximating this. Maybe like the dot product of + // the "height" part of the OBB with the camera look direction or something like that? + distanceToObb += (tileBoundingRegion.maximumHeight - tileBoundingRegion.minimumHeight); } - return tileBoundingRegion.distanceToCamera(frameState); + return distanceToObb; }; function updateTileBoundingRegion(tile, terrainProvider) { var surfaceTile = tile.data; if (surfaceTile === undefined) { surfaceTile = tile.data = new GlobeSurfaceTile(); + } + + if (surfaceTile.tileBoundingRegion === undefined) { surfaceTile.tileBoundingRegion = new TileBoundingRegion({ computeBoundingVolumes : false, rectangle : tile.rectangle, diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 2347c681835..d768c1b0d3c 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -585,6 +585,7 @@ define([ if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) { reportTileAction(frameState, tile, 'meets SSE'); + // This tile meets SSE requirements, so render it and load it with medium priority. queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); addTileToRenderList(primitive, tile, nearestRenderableTile); @@ -625,16 +626,18 @@ define([ // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. var anyAreRenderable = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); - if (!anyAreRenderable && firstRenderedDescendantIndex !== primitive._tilesToRender.length) { - // None of our descendants are renderable, so they'll all end up rendering an ancestor. - // That's a big waste of time, so kick them all out of the render list and render this tile instead. - primitive._tilesToRender.length = firstRenderedDescendantIndex; - primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; - addTileToRenderList(primitive, tile, nearestRenderableTile); - } + if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) { + if (!anyAreRenderable) { + // None of our descendants are renderable, so they'll all end up rendering an ancestor. + // That's a big waste of time, so kick them all out of the render list and render this tile instead. + primitive._tilesToRender.length = firstRenderedDescendantIndex; + primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; + addTileToRenderList(primitive, tile, nearestRenderableTile); + } - // Tile is not rendered, so load it with low priority. - queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + // Tile is not rendered but some descendants are, so load it with low priority. + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + } return anyAreRenderable || tile.renderable; } From c5aa64d3a0cdf03a275079a1484efd3d222e61c9 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 29 Jun 2018 16:19:47 +1000 Subject: [PATCH 017/131] Fix distance sorting, cleanup and commenting. --- Source/Scene/GlobeSurfaceTileProvider.js | 5 -- Source/Scene/QuadtreePrimitive.js | 100 ++++++++++------------- 2 files changed, 45 insertions(+), 60 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index b438da7ee61..3917c462b5e 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -609,9 +609,6 @@ define([ // For a tileset with `availability`, we'll always be able to refine. // We can ask for availability of _any_ child tile because we only need to confirm // that we get a yes or no answer, it doesn't matter what the answer is. - // if ((tile.level % 6) === 0 && tile.data.terrainData === undefined) { - // return false; - // } var childAvailable = tile.data.isChildAvailable(this.terrainProvider, tile, 0, 0); return childAvailable !== undefined; }; @@ -625,8 +622,6 @@ define([ * @returns {Number} The load priority value. */ GlobeSurfaceTileProvider.prototype.computeTileLoadPriority = function(tile, frameState) { - return tile._distance; - var surfaceTile = tile.data; if (surfaceTile === undefined) { return 0.0; diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index d768c1b0d3c..a2593160cc4 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -535,8 +535,8 @@ define([ return; } - if (primitive.computeTileLoadPriority !== undefined) { - tile._loadPriority = primitive.computeTileLoadPriority(tile, frameState); + if (primitive.tileProvider.computeTileLoadPriority !== undefined) { + tile._loadPriority = primitive.tileProvider.computeTileLoadPriority(tile, frameState); } queue.push(tile); } @@ -567,6 +567,19 @@ define([ } } + /** + * Visits a tile for possible rendering. When we call this function with a tile: + * + * * the tile has been determined to be visible (possibly based on a bounding volume that is not very tight-fitting) + * * its parent tile does _not_ meet the SSE. + * * may or may not be renderable + * + * @private + * @param {Primitive} primitive The QuadtreePrimitive. + * @param {FrameState} frameState The frame state. + * @param {QuadtreeTile} tile The tile to visit + * @param {QuadtreeTile} nearestRenderableTile The nearest ancestor tile for which the `renderable` property is true. + */ function visitTile(primitive, frameState, tile, nearestRenderableTile) { var debug = primitive._debug; @@ -584,9 +597,8 @@ define([ } if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) { - reportTileAction(frameState, tile, 'meets SSE'); - // This tile meets SSE requirements, so render it and load it with medium priority. + reportTileAction(frameState, tile, 'meets SSE'); queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); addTileToRenderList(primitive, tile, nearestRenderableTile); return tile.renderable; @@ -608,34 +620,51 @@ define([ // No point in rendering the children because they're all upsampled. Render this tile instead. addTileToRenderList(primitive, tile, nearestRenderableTile); - // Load the children even though we're (currently) not going to render them. - // A tile that is "upsampled only" right now might change its tune once it does more loading. - // A tile that is upsampled now and forever should also be done loading, so no harm done. - queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild); - // Rendered tile that's not waiting on children loads with medium priority. queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + // Make sure we don't unload the children and forget they're upsampled. + primitive._tileReplacementQueue.markTileRendered(southwestChild); + primitive._tileReplacementQueue.markTileRendered(southeastChild); + primitive._tileReplacementQueue.markTileRendered(northwestChild); + primitive._tileReplacementQueue.markTileRendered(northeastChild); + return tile.renderable; } else { + // SSE is not good enough, so refine. reportTileAction(frameState, tile, 'refine'); var firstRenderedDescendantIndex = primitive._tilesToRender.length; - // SSE is not good enough and children are loaded, so refine. // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. var anyAreRenderable = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) { + // If no descendant tiles were added to the render list, it means they were all + // culled even though this tile was deemed visible. That's pretty common. + + // Since we're in this `if` block though, at least one descendant tile _was_ added to the render list! + if (!anyAreRenderable) { - // None of our descendants are renderable, so they'll all end up rendering an ancestor. + // But none of our descendants are renderable, so they'll all end up rendering an ancestor. // That's a big waste of time, so kick them all out of the render list and render this tile instead. primitive._tilesToRender.length = firstRenderedDescendantIndex; primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; addTileToRenderList(primitive, tile, nearestRenderableTile); } - // Tile is not rendered but some descendants are, so load it with low priority. + // We'd like to be rendering one or more descendents, and we're either not rendering this + // tile at all, or we're only rendering it temporarily because _none_ of our children + // are renderable. In either case, load this tile with low priority so that zooming + // out or panning quickly doesn't leave us with huge holes in the terrain surface. + + // It'd be nice if we didn't need to do this; we could avoid loading heaps of tiles. + // But first we'd need a way to fill those holes. If we're showing a bunch of level 15 + // tiles and the user zooms out so we want to be showing level 14 but none of those + // level 14 tiles are loaded because we removed this line below, it's not really acceptable + // to a) draw nothing, or b) draw level 0 tiles, because either strategy will likely + // leave us with massive gaps visible in the globe. + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } @@ -644,7 +673,10 @@ define([ } else { reportTileAction(frameState, tile, 'can\'t refine'); - // We'd like to refine but can't because this tile isn't loaded yet. + // We'd like to refine but can't because we have no availability data for this tile's children, + // so we have no idea if refinining would involve a load or an upsample. We'll have to finish + // loading this tile first in order to find that out, so load this refinement blocker with + // high priority. addTileToRenderList(primitive, tile, nearestRenderableTile); queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); @@ -652,48 +684,6 @@ define([ } } - function queueChildLoadNearToFar(primitive, cameraPosition, southwest, southeast, northwest, northeast) { - if (cameraPosition.longitude < southwest.east) { - if (cameraPosition.latitude < southwest.north) { - // Camera in southwest quadrant - queueChildTileLoad(primitive, southwest); - queueChildTileLoad(primitive, southeast); - queueChildTileLoad(primitive, northwest); - queueChildTileLoad(primitive, northeast); - } else { - // Camera in northwest quadrant - queueChildTileLoad(primitive, northwest); - queueChildTileLoad(primitive, southwest); - queueChildTileLoad(primitive, northeast); - queueChildTileLoad(primitive, southeast); - } - } else if (cameraPosition.latitude < southwest.north) { - // Camera southeast quadrant - queueChildTileLoad(primitive, southeast); - queueChildTileLoad(primitive, southwest); - queueChildTileLoad(primitive, northeast); - queueChildTileLoad(primitive, northwest); - } else { - // Camera in northeast quadrant - queueChildTileLoad(primitive, northeast); - queueChildTileLoad(primitive, northwest); - queueChildTileLoad(primitive, southeast); - queueChildTileLoad(primitive, southwest); - } - } - - function queueChildTileLoad(primitive, childTile) { - primitive._tileReplacementQueue.markTileRendered(childTile); - if (childTile.needsLoading) { - // if (childTile.renderable) { - // primitive._tileLoadQueueLow.push(childTile); - // } else { - // // A tile blocking refine loads with high priority - // primitive._tileLoadQueueHigh.push(childTile); - // } - } - } - function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile) { var cameraPosition = frameState.camera.positionCartographic; var tileProvider = primitive._tileProvider; From a11e43ff01463f5cb090d273e8e10c161c77bd6b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 29 Jun 2018 16:47:36 +1000 Subject: [PATCH 018/131] Fix some test failures, add some comments. --- Source/Scene/GlobeSurfaceTile.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index c67a0d54977..dcf99ad72c4 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -304,16 +304,24 @@ define([ tile.state = QuadtreeTileLoadState.LOADING; } - var bvhLevel = terrainProvider.getNearestBvhLevel(tile.x, tile.y, tile.level); - if (bvhLevel !== -1 && bvhLevel !== tile.level) { - var ancestor = tile.parent; - while (ancestor.level !== bvhLevel) { - ancestor = ancestor.parent; - } + if (surfaceTile.boundingVolumeSourceTile !== tile && terrainProvider.getNearestBvhLevel !== undefined) { + // So here we are loading this tile, but we know our bounding volume isn't very good, and so our + // judgement that it's visible is kind of suspect. If this terrain source has bounding volume data + // outside of individual tiles, let's get our hands on that before we waste time downloading + // potentially not-actually-visible tiles like this one. + var bvhLevel = terrainProvider.getNearestBvhLevel(tile.x, tile.y, tile.level); + if (bvhLevel !== -1 && bvhLevel !== tile.level) { + var ancestor = tile.parent; + while (ancestor.level !== bvhLevel) { + ancestor = ancestor.parent; + } - if (ancestor.data === undefined || ancestor.data.terrainData === undefined) { - GlobeSurfaceTile.processStateMachine(ancestor, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, true); - return; + if (ancestor.data === undefined || ancestor.data.terrainData === undefined) { + // The ancestor that holds the BVH data isn't loaded yet; load it (terrain only!) instead of this tile. + GlobeSurfaceTile.processStateMachine(ancestor, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, true); + return; + + } } } @@ -321,6 +329,12 @@ define([ processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); } + // From here down we're loading imagery, not terrain. We don't want to load imagery until + // we're certain that the terrain tiles are actually visible, though. So if our bounding + // volume isn't accurate, stop here. Also stop here if we're explicitly loading terrain + // only, which happens in these two scenarios: + // * we want ancestor BVH data from this tile but don't plan to render it (see code above). + // * we want to upsample from this tile but don't plan to render it (see processTerrainStateMachine). if (terrainOnly || (tile.level !== 0 && tile.data.boundingVolumeSourceTile !== tile)) { return; } From 93105ce38b9d982e3b794ad612728f8d5031f007 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 29 Jun 2018 17:01:25 +1000 Subject: [PATCH 019/131] Tweak root tile selection. --- Source/Scene/QuadtreePrimitive.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index a2593160cc4..67d24c05a1e 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -521,11 +521,8 @@ define([ if (!tile.renderable) { queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); ++debug.tilesWaitingForChildren; - } else if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { - visitTile(primitive, frameState, tile, tile); } else { - queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); - ++debug.tilesCulled; + visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile); } } } From 78bc48062bdb7cf03302c5f61afdc68ce14b1a65 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 29 Jun 2018 17:52:23 +1000 Subject: [PATCH 020/131] Fix test failures in QuadtreePrimitiveSpec. --- Specs/Scene/QuadtreePrimitiveSpec.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index e9d23d794bb..b609f1bb792 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -47,7 +47,7 @@ defineSuite([ var result = jasmine.createSpyObj('tileProvider', [ 'getQuadtree', 'setQuadtree', 'getReady', 'getTilingScheme', 'getErrorEvent', 'initialize', 'updateImagery', 'beginUpdate', 'endUpdate', 'getLevelMaximumGeometricError', 'loadTile', - 'computeTileVisibility', 'showTileThisFrame', 'computeDistanceToTile', 'isDestroyed', 'destroy']); + 'computeTileVisibility', 'showTileThisFrame', 'computeDistanceToTile', 'canRefine', 'isDestroyed', 'destroy']); defineProperties(result, { quadtree : { @@ -68,6 +68,10 @@ defineSuite([ var tilingScheme = new GeographicTilingScheme(); result.getTilingScheme.and.returnValue(tilingScheme); + result.canRefine.and.callFake(function(tile) { + return tile.renderable; + }); + return result; } @@ -298,6 +302,8 @@ defineSuite([ quadtree.render(scene.frameState); quadtree.endFrame(scene.frameState); + ++scene.frameState.frameNumber; + // load tiles quadtree.update(scene.frameState); quadtree.beginFrame(scene.frameState); @@ -313,6 +319,8 @@ defineSuite([ removeFunc(); + ++scene.frameState.frameNumber; + quadtree.update(scene.frameState); quadtree.beginFrame(scene.frameState); quadtree.render(scene.frameState); @@ -462,16 +470,11 @@ defineSuite([ quadtree.endFrame(scene.frameState); // levelZeroTiles[0] should move to medium priority. - // Its descendents should continue loading, so they have a chance to decide they're not upsampled later. expect(quadtree._tileLoadQueueHigh.length).toBe(1); expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); expect(quadtree._tileLoadQueueMedium.length).toBe(1); expect(quadtree._tileLoadQueueMedium).toContain(quadtree._levelZeroTiles[0]); - expect(quadtree._tileLoadQueueLow.length).toBe(4); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]); + expect(quadtree._tileLoadQueueLow.length).toBe(0); }); it('renders tiles in approximate near-to-far order', function() { From 8fc7a0d8a88bd52195cc99dcf7b38b0135414353 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 2 Jul 2018 21:56:19 +1000 Subject: [PATCH 021/131] Don't render children until all visible ones are renderable. Unless they were rendered last frame. --- Source/Scene/QuadtreePrimitive.js | 110 +++++++++++------- .../Workers/upsampleQuantizedTerrainMesh.js | 33 +++--- terrainnotes.md | 15 +++ 3 files changed, 103 insertions(+), 55 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 67d24c05a1e..6260298805e 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -135,6 +135,8 @@ define([ this._tileLoadProgressEvent = new Event(); this._lastTileLoadQueueLength = 0; + + this._lastSelectionFrameNumber = undefined; } defineProperties(QuadtreePrimitive.prototype, { @@ -525,6 +527,8 @@ define([ visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile); } } + + primitive._lastSelectionFrameNumber = frameNumber; } function queueTileLoad(primitive, queue, tile, frameState) { @@ -546,22 +550,38 @@ define([ function reportTileAction(frameState, tile, action) { return; - var heightSource = tile.data.boundingVolumeSourceTile; - var renderable = tile.renderable; - if (tile._lastAction !== action || tile._lastHeightSource !== heightSource || tile._lastRenderable !== renderable) { - if (lastFrame !== frameState.frameNumber) { - console.log('****** FRAME ' + frameState.frameNumber); - lastFrame = frameState.frameNumber; - } + // var heightSource = tile.data.boundingVolumeSourceTile; + // var renderable = tile.renderable; + // if (tile._lastAction !== action || tile._lastHeightSource !== heightSource || tile._lastRenderable !== renderable) { + // if (lastFrame !== frameState.frameNumber) { + // console.log('****** FRAME ' + frameState.frameNumber); + // lastFrame = frameState.frameNumber; + // } + + // var tileID = `L${tile.level}X${tile.x}Y${tile.y}`; + // var emphasize = tile._lastAction !== undefined && tile._lastAction !== action ? '*' : ''; + // console.log(`${emphasize}${tileID} NOW ${getState(action, heightSource, renderable)} WAS ${getState(tile._lastAction, tile._lastHeightSource, tile._lastRenderable)}`); + + // tile._lastAction = action; + // tile._lastHeightSource = heightSource; + // tile._lastRenderable = renderable; + // } + } + + function isAnyAreRenderable(bitMask) { + return (bitMask & 1) === 1; + } - var tileID = `L${tile.level}X${tile.x}Y${tile.y}`; - var emphasize = tile._lastAction !== undefined && tile._lastAction !== action ? '*' : ''; - console.log(`${emphasize}${tileID} NOW ${getState(action, heightSource, renderable)} WAS ${getState(tile._lastAction, tile._lastHeightSource, tile._lastRenderable)}`); + function isAllAreRenderable(bitMask) { + return (bitMask & 2) === 2; + } - tile._lastAction = action; - tile._lastHeightSource = heightSource; - tile._lastRenderable = renderable; - } + function isAnyWereRenderedLastFrame(bitMask) { + return (bitMask & 4) === 4; + } + + function createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame) { + return (anyAreRenderable ? 1 : 0) | (allAreRenderable ? 2 : 0) | (anyWereRenderedLastFrame ? 4 : 0); } /** @@ -576,6 +596,8 @@ define([ * @param {FrameState} frameState The frame state. * @param {QuadtreeTile} tile The tile to visit * @param {QuadtreeTile} nearestRenderableTile The nearest ancestor tile for which the `renderable` property is true. + * @returns A bit mask where bit 1 is set if this tile or _any_ of its descendants are renderable, and bit 2 is + * set if _all_ selected tiles starting with this tile are renderable. */ function visitTile(primitive, frameState, tile, nearestRenderableTile) { var debug = primitive._debug; @@ -598,7 +620,7 @@ define([ reportTileAction(frameState, tile, 'meets SSE'); queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); addTileToRenderList(primitive, tile, nearestRenderableTile); - return tile.renderable; + return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); } var southwestChild = tile.southwestChild; @@ -626,7 +648,7 @@ define([ primitive._tileReplacementQueue.markTileRendered(northwestChild); primitive._tileReplacementQueue.markTileRendered(northeastChild); - return tile.renderable; + return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); } else { // SSE is not good enough, so refine. reportTileAction(frameState, tile, 'refine'); @@ -634,7 +656,10 @@ define([ var firstRenderedDescendantIndex = primitive._tilesToRender.length; // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. - var anyAreRenderable = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); + var bitMask = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); + var anyAreRenderable = isAnyAreRenderable(bitMask); + var allAreRenderable = isAllAreRenderable(bitMask); + var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(bitMask); if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) { // If no descendant tiles were added to the render list, it means they were all @@ -642,12 +667,15 @@ define([ // Since we're in this `if` block though, at least one descendant tile _was_ added to the render list! - if (!anyAreRenderable) { + if (!allAreRenderable && !anyWereRenderedLastFrame) { // But none of our descendants are renderable, so they'll all end up rendering an ancestor. // That's a big waste of time, so kick them all out of the render list and render this tile instead. primitive._tilesToRender.length = firstRenderedDescendantIndex; primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; addTileToRenderList(primitive, tile, nearestRenderableTile); + anyAreRenderable = tile.renderable; + allAreRenderable = tile.renderable; + anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; } // We'd like to be rendering one or more descendents, and we're either not rendering this @@ -665,7 +693,7 @@ define([ queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } - return anyAreRenderable || tile.renderable; + return createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); } } else { reportTileAction(frameState, tile, 'can\'t refine'); @@ -677,7 +705,7 @@ define([ addTileToRenderList(primitive, tile, nearestRenderableTile); queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); - return tile.renderable; + return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); } } @@ -686,37 +714,41 @@ define([ var tileProvider = primitive._tileProvider; var occluders = primitive._occluders; - var anyAreRenderable = false; + var swMask, seMask, nwMask, neMask; if (cameraPosition.longitude < southwest.rectangle.east) { if (cameraPosition.latitude < southwest.rectangle.north) { // Camera in southwest quadrant - anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); } else { // Camera in northwest quadrant - anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); } } else if (cameraPosition.latitude < southwest.rectangle.north) { // Camera southeast quadrant - anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); } else { // Camera in northeast quadrant - anyAreRenderable = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; - anyAreRenderable = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile) || anyAreRenderable; + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); } - return anyAreRenderable; + var anyAreRenderable = isAnyAreRenderable(swMask) || isAnyAreRenderable(seMask) || isAnyAreRenderable(nwMask) || isAnyAreRenderable(neMask); + var allAreRenderable = isAllAreRenderable(swMask) && isAllAreRenderable(seMask) && isAllAreRenderable(nwMask) && isAllAreRenderable(neMask); + var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(swMask) || isAnyWereRenderedLastFrame(seMask) || isAnyWereRenderedLastFrame(nwMask) || isAnyWereRenderedLastFrame(neMask); + var mask = createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); + return mask } function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile) { @@ -726,7 +758,7 @@ define([ reportTileAction(frameState, tile, 'culled'); ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); - return false; + return createBitMask(false, true, false); } } diff --git a/Source/Workers/upsampleQuantizedTerrainMesh.js b/Source/Workers/upsampleQuantizedTerrainMesh.js index 9921c0bbaaa..3879e98ff95 100644 --- a/Source/Workers/upsampleQuantizedTerrainMesh.js +++ b/Source/Workers/upsampleQuantizedTerrainMesh.js @@ -107,41 +107,40 @@ define([ var height; var i, n; + var u, v; for (i = 0, n = 0; i < quantizedVertexCount; ++i, n += 2) { var texCoords = encoding.decodeTextureCoordinates(parentVertices, i, decodeTexCoordsScratch); height = encoding.decodeHeight(parentVertices, i) / exaggeration; - parentUBuffer[i] = CesiumMath.clamp((texCoords.x * maxShort) | 0, 0, maxShort); - parentVBuffer[i] = CesiumMath.clamp((texCoords.y * maxShort) | 0, 0, maxShort); + u = CesiumMath.clamp((texCoords.x * maxShort) | 0, 0, maxShort); + v = CesiumMath.clamp((texCoords.y * maxShort) | 0, 0, maxShort); parentHeightBuffer[i] = CesiumMath.clamp((((height - parentMinimumHeight) / (parentMaximumHeight - parentMinimumHeight)) * maxShort) | 0, 0, maxShort); - if (parentUBuffer[i] < threshold) { - parentUBuffer[i] = 0; + if (u < threshold) { + u = 0; } - if (parentVBuffer[i] < threshold) { - parentVBuffer[i] = 0; + if (v < threshold) { + v = 0; } - if (maxShort - parentUBuffer[i] < threshold) { - parentUBuffer[i] = maxShort; + if (maxShort - u < threshold) { + u = maxShort; } - if (maxShort - parentVBuffer[i] < threshold) { - parentVBuffer[i] = maxShort; + if (maxShort - v < threshold) { + v = maxShort; } + parentUBuffer[i] = u; + parentVBuffer[i] = v; + if (hasVertexNormals) { var encodedNormal = encoding.getOctEncodedNormal(parentVertices, i, octEncodedNormalScratch); parentNormalBuffer[n] = encodedNormal.x; parentNormalBuffer[n + 1] = encodedNormal.y; } - } - var u, v; - for (i = 0, n = 0; i < quantizedVertexCount; ++i, n += 2) { - u = parentUBuffer[i]; - v = parentVBuffer[i]; if ((isEastChild && u >= halfMaxShort || !isEastChild && u <= halfMaxShort) && (isNorthChild && v >= halfMaxShort || !isNorthChild && v <= halfMaxShort)) { @@ -548,5 +547,7 @@ define([ } } - return createTaskProcessorWorker(upsampleQuantizedTerrainMesh); + var worker = createTaskProcessorWorker(upsampleQuantizedTerrainMesh); + worker._workerFunction = upsampleQuantizedTerrainMesh; + return worker; }); diff --git a/terrainnotes.md b/terrainnotes.md index 2c07717e9f8..15f07ad51e1 100644 --- a/terrainnotes.md +++ b/terrainnotes.md @@ -80,3 +80,18 @@ A tile conceptually has three sets of min/max heights: TileBoundingRegion has a both an oriented bounding box and a bounding sphere, but we don't use either of them for terrain. Instead, tiles store separate copies of these things (orientingBoundingBox and boundingSphere3D). That's probably good because we update the min/max height in publishToTile but we don't update either of the other two. +## Rendering without data + +When the camera moves, new tiles become visible. If those tiles aren't loaded yet, what do we do? Options are: + + 1. Render nothing. There will be holes in the Earth's surface. + 2. Upsample synchronously. Each upsample takes 2-3ms on a fast computer so we can't do much of this without the frame rate taking a dive. + 3. Render an ancestor tile and discard fragments outside the bounds of the tile. This is nearly free on the CPU, but expensive on the GPU because we end up rendering large tiles and discarding most of the fragments. + 4. Create a tile on-the-fly to fill the space, e.g. that aligns with the edge vertices of adjacent tiles. + 5. Don't refine until all descendants are renderable (but do load the descendants we want to render, of course!). + 6. Show some kind of blurry blobby placeholder for the not-yet-available tile. + +Number 5 is a tempting strategy, with one big problem: it can cause us to lose detail from the scene with small camera movements. For example, if we're looking at a detailed scene that shows 3 out of 4 children of a level 14 tile, and then move the camera so that the 4th is visible, suddenly that level 14 tile isn't refinable anymore. If we start rendering that level 14 tiles intead of its children, we'll lose detail from the scene for a second until that 4th tile loads. This looks really bad. + +So, our rule is: if we rendered a tile last frame, and it's still visible and the correct SSE this frame, we must render it this frame. In the scenario above, we must use one of our other strategies to fill the space of that 4th child of the level 14 tiles. + From 1ce367d92398e3fd3ff299427019bc0d2a681279 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 5 Jul 2018 10:55:47 +1000 Subject: [PATCH 022/131] Add some tweakable options to QuadtreePrimitive and GlobeSurfaceTileProvider. --- Source/Scene/GlobeSurfaceTileProvider.js | 15 ++++++++++++++- Source/Scene/QuadtreePrimitive.js | 14 +++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 3917c462b5e..0d6f0904990 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -94,6 +94,11 @@ define([ ShadowMode) { 'use strict'; + var MissingTileStrategy = { + RENDER_NOTHING: 0, + RENDER_ANCESTOR_SUBSET: 1 + }; + /** * Provides quadtree tiles representing the surface of the globe. This type is intended to be used * with {@link QuadtreePrimitive}. @@ -129,6 +134,11 @@ define([ this.enableLighting = false; this.shadows = ShadowMode.RECEIVE_ONLY; + /** + * The strategy to use to fill holes in the globe when terrain tiles are not yet loaded. + */ + this.missingTileStrategy = MissingTileStrategy.RENDER_ANCESTOR_SUBSET; + this._quadtree = undefined; this._terrainProvider = options.terrainProvider; this._imageryLayers = options.imageryLayers; @@ -1321,7 +1331,10 @@ define([ if (surfaceTile.renderableTile !== undefined) { // We can't render this tile yet, so instead render a subset of our closest renderable ancestor. - addDrawCommandsForTile(tileProvider, surfaceTile.renderableTile, frameState, surfaceTile.renderableTileSubset); + var missingTileStrategy = tileProvider.missingTileStrategy; + if (missingTileStrategy === MissingTileStrategy.RENDER_ANCESTOR_SUBSET) { + addDrawCommandsForTile(tileProvider, surfaceTile.renderableTile, frameState, surfaceTile.renderableTileSubset); + } return; } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 6260298805e..266ba602cc5 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -129,6 +129,16 @@ define([ */ this.tileCacheSize = defaultValue(options.tileCacheSize, 100); + /** + * Gets or sets whether to reveal tiles in chunks while loading. If true, tiles + * will be shown as they become renderable. If false, tiles will only be shown + * when all visible siblings in the same quad are renderable, OR if they were + * shown in the last frame. Detail will not flicker out with either settings. + * @type {Boolean} + * @default true + */ + this.revealInChunks = false; + this._occluders = new QuadtreeOccluders({ ellipsoid : ellipsoid }); @@ -667,7 +677,9 @@ define([ // Since we're in this `if` block though, at least one descendant tile _was_ added to the render list! - if (!allAreRenderable && !anyWereRenderedLastFrame) { + var revealInChunks = primitive.revealInChunks; + if (revealInChunks && !allAreRenderable && !anyWereRenderedLastFrame || + !revealInChunks && !anyAreRenderable) { // But none of our descendants are renderable, so they'll all end up rendering an ancestor. // That's a big waste of time, so kick them all out of the render list and render this tile instead. primitive._tilesToRender.length = firstRenderedDescendantIndex; From e614d6e46f3a598053819952cf1aaeaa3c641f33 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 5 Jul 2018 13:50:11 +1000 Subject: [PATCH 023/131] Fix test failure, improve camera terrain adjustment. --- Source/Scene/Globe.js | 11 +++++++++-- Specs/Core/RequestSchedulerSpec.js | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 5b1ab35b8c4..bd949f47b90 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -531,7 +531,7 @@ define([ tile = tile.parent; } - if (!defined(tile)) { + if (!defined(tile) || !defined(tile.data) || !defined(tile.data.tileBoundingRegion)) { return undefined; } @@ -563,7 +563,14 @@ define([ return undefined; } - return ellipsoid.cartesianToCartographic(intersection, scratchGetHeightCartographic).height; + var height = ellipsoid.cartesianToCartographic(intersection, scratchGetHeightCartographic).height; + + // For low-detail tiles, large triangles often cut the the globe and appear to be at a much + // lower height than actually makes any sense. So clamp the height to the actual height range + // of the tile. + height = Math.max(height, tile.data.tileBoundingRegion.minimumHeight); + height = Math.min(height, tile.data.tileBoundingRegion.maximumHeight); + return height; }; /** diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index 55f49fc1492..d01723d7e39 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -610,6 +610,7 @@ defineSuite([ return new Request({ url : 'http://test.invalid/1', requestFunction : requestFunction, + throttle : throttleByServer, throttleByServer : throttleByServer }); } From a462ddc26cf0b7aa42c1071b2120bef472ed053e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Jul 2018 13:55:39 +1000 Subject: [PATCH 024/131] Fix exaggeration, other cleanup. --- Source/Scene/GlobeSurfaceTileProvider.js | 55 +++++++++++------------- Source/Scene/QuadtreePrimitive.js | 3 +- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 0d6f0904990..27555a8b58d 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -536,16 +536,6 @@ define([ * @returns {Visibility} The visibility of the tile. */ GlobeSurfaceTileProvider.prototype.computeTileVisibility = function(tile, frameState, occluders) { - var result = this.computeTileVisibilityInternal(tile, frameState, occluders); - // if (result !== tile._lastVisibility) { - // console.log(`L${tile.level}X${tile.x}Y${tile.y}: state ${tile._lastState} -> ${tile.data.terrainState} visibility ${tile._lastVisibility} -> ${result}`); - // tile._lastVisibility = result; - // tile._lastState = tile.data.terrainState; - // } - return result; - }; - - GlobeSurfaceTileProvider.prototype.computeTileVisibilityInternal = function(tile, frameState, occluders) { var distance = this.computeDistanceToTile(tile, frameState); tile._distance = distance; @@ -733,7 +723,7 @@ define([ // based that conservative metric, we may end up loading tiles that are not detailed enough, but that's much // better (faster) than loading tiles that are too detailed. - var heightSource = updateTileBoundingRegion(tile, this.terrainProvider); + var heightSource = updateTileBoundingRegion(tile, this.terrainProvider, frameState); var surfaceTile = tile.data; var tileBoundingRegion = surfaceTile.tileBoundingRegion; @@ -767,22 +757,29 @@ define([ surfaceTile.occludeePointInScaledSpace = ellipsoidalOccluder.computeHorizonCullingPoint(surfaceTile.orientedBoundingBox.center, cornerPositions, surfaceTile.occludeePointInScaledSpace); } - // Compute the distance to the OBB - var distanceToObb = Math.sqrt(surfaceTile.orientedBoundingBox.distanceSquaredTo(frameState.camera.positionWC)); + var min = tileBoundingRegion.minimumHeight; + var max = tileBoundingRegion.maximumHeight; if (surfaceTile.boundingVolumeSourceTile !== tile) { - // Bounding volume is approximate, so, as described above, we want the distance to the - // farther-away height surface, not the closer one. The following is a super quick-and-dirty - // way to compute a distance that is that _or farther_. - // TODO: could do a better job approximating this. Maybe like the dot product of - // the "height" part of the OBB with the camera look direction or something like that? - distanceToObb += (tileBoundingRegion.maximumHeight - tileBoundingRegion.minimumHeight); + var cameraHeight = frameState.camera.positionCartographic.height; + var distanceToMin = Math.abs(cameraHeight - min); + var distanceToMax = Math.abs(cameraHeight - max); + if (distanceToMin > distanceToMax) { + tileBoundingRegion.minimumHeight = min; + tileBoundingRegion.maximumHeight = min; + } else { + tileBoundingRegion.minimumHeight = max; + tileBoundingRegion.maximumHeight = max; + } } - return distanceToObb; + tileBoundingRegion.minimumHeight = min; + tileBoundingRegion.maximumHeight = max; + + return tileBoundingRegion.distanceToCamera(frameState); }; - function updateTileBoundingRegion(tile, terrainProvider) { + function updateTileBoundingRegion(tile, terrainProvider, frameState) { var surfaceTile = tile.data; if (surfaceTile === undefined) { surfaceTile = tile.data = new GlobeSurfaceTile(); @@ -811,16 +808,16 @@ define([ if (terrainData !== undefined && terrainData._minimumHeight !== undefined && terrainData._maximumHeight !== undefined) { // We have tight-fitting min/max heights from the terrain data. - tileBoundingRegion.minimumHeight = terrainData._minimumHeight; - tileBoundingRegion.maximumHeight = terrainData._maximumHeight; + tileBoundingRegion.minimumHeight = terrainData._minimumHeight * frameState.terrainExaggeration; + tileBoundingRegion.maximumHeight = terrainData._maximumHeight * frameState.terrainExaggeration; return tile; } var bvh = surfaceTile.getBvh(tile, terrainProvider.terrainProvider); if (bvh !== undefined && bvh[0] === bvh[0] && bvh[1] === bvh[1]) { // Have a BVH that covers this tile and the heights are not NaN. - tileBoundingRegion.minimumHeight = bvh[0]; - tileBoundingRegion.maximumHeight = bvh[1]; + tileBoundingRegion.minimumHeight = bvh[0] * frameState.terrainExaggeration; + tileBoundingRegion.maximumHeight = bvh[1] * frameState.terrainExaggeration; return tile; } @@ -841,15 +838,15 @@ define([ var ancestorTerrainData = ancestorSurfaceTile.terrainData; if (ancestorTerrainData !== undefined && ancestorTerrainData._minimumHeight !== undefined && ancestorTerrainData._maximumHeight !== undefined) { - tileBoundingRegion.minimumHeight = ancestorTerrainData._minimumHeight; - tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight; + tileBoundingRegion.minimumHeight = ancestorTerrainData._minimumHeight * frameState.terrainExaggeration; + tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight * frameState.terrainExaggeration; return ancestor; } var ancestorBvh = ancestorSurfaceTile._bvh; if (ancestorBvh !== undefined && ancestorBvh[0] === ancestorBvh[0] && ancestorBvh[1] === ancestorBvh[1]) { - tileBoundingRegion.minimumHeight = ancestorBvh[0]; - tileBoundingRegion.maximumHeight = ancestorBvh[1]; + tileBoundingRegion.minimumHeight = ancestorBvh[0] * frameState.terrainExaggeration; + tileBoundingRegion.maximumHeight = ancestorBvh[1] * frameState.terrainExaggeration; return ancestor; } } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 266ba602cc5..adcbe7adf2c 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -599,9 +599,10 @@ define([ * * * the tile has been determined to be visible (possibly based on a bounding volume that is not very tight-fitting) * * its parent tile does _not_ meet the SSE. - * * may or may not be renderable + * * the tile may or may not be renderable * * @private + * * @param {Primitive} primitive The QuadtreePrimitive. * @param {FrameState} frameState The frame state. * @param {QuadtreeTile} tile The tile to visit From 2858d6b4c8ac3e85e2bb9e78860acc49a22fd869 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Jul 2018 14:17:06 +1000 Subject: [PATCH 025/131] Fix eslint warnings. --- Source/Core/HeightmapTerrainData.js | 2 +- Source/Scene/GlobeSurfaceTile.js | 6 -- Source/Scene/QuadtreePrimitive.js | 126 ++++++++++++++-------------- Source/Scene/QuadtreeTile.js | 4 - 4 files changed, 64 insertions(+), 74 deletions(-) diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index 24f10f68196..5c92fe6bca5 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -168,7 +168,7 @@ define([ get : function() { return this._childTileMask; } - }, + } }); var taskProcessor = new TaskProcessor('createVerticesFromHeightmap'); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index dcf99ad72c4..bc3eb1817ec 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -521,9 +521,6 @@ define([ } } - var upsamplesStarted = 0; - var upsamplesCompleted = 0; - function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level) { var parent = tile.parent; if (!parent) { @@ -547,12 +544,9 @@ define([ return; } - ++upsamplesStarted; - surfaceTile.terrainState = TerrainState.RECEIVING; when(terrainDataPromise, function(terrainData) { - ++upsamplesCompleted; surfaceTile.terrainData = terrainData; surfaceTile.terrainState = TerrainState.RECEIVED; }, function() { diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index adcbe7adf2c..194a143d8e6 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -552,11 +552,11 @@ define([ queue.push(tile); } - function getState(action, heightSource, renderable) { - return `A:${action} HS:${heightSource !== undefined ? heightSource.level : 'undefined'} R:${renderable}`; - } + // function getState(action, heightSource, renderable) { + // return `A:${action} HS:${heightSource !== undefined ? heightSource.level : 'undefined'} R:${renderable}`; + // } - var lastFrame; + // var lastFrame; function reportTileAction(frameState, tile, action) { return; @@ -660,66 +660,66 @@ define([ primitive._tileReplacementQueue.markTileRendered(northeastChild); return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); - } else { - // SSE is not good enough, so refine. - reportTileAction(frameState, tile, 'refine'); - - var firstRenderedDescendantIndex = primitive._tilesToRender.length; - - // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. - var bitMask = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); - var anyAreRenderable = isAnyAreRenderable(bitMask); - var allAreRenderable = isAllAreRenderable(bitMask); - var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(bitMask); - - if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) { - // If no descendant tiles were added to the render list, it means they were all - // culled even though this tile was deemed visible. That's pretty common. - - // Since we're in this `if` block though, at least one descendant tile _was_ added to the render list! - - var revealInChunks = primitive.revealInChunks; - if (revealInChunks && !allAreRenderable && !anyWereRenderedLastFrame || - !revealInChunks && !anyAreRenderable) { - // But none of our descendants are renderable, so they'll all end up rendering an ancestor. - // That's a big waste of time, so kick them all out of the render list and render this tile instead. - primitive._tilesToRender.length = firstRenderedDescendantIndex; - primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; - addTileToRenderList(primitive, tile, nearestRenderableTile); - anyAreRenderable = tile.renderable; - allAreRenderable = tile.renderable; - anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; - } + } - // We'd like to be rendering one or more descendents, and we're either not rendering this - // tile at all, or we're only rendering it temporarily because _none_ of our children - // are renderable. In either case, load this tile with low priority so that zooming - // out or panning quickly doesn't leave us with huge holes in the terrain surface. + // SSE is not good enough, so refine. + reportTileAction(frameState, tile, 'refine'); + + var firstRenderedDescendantIndex = primitive._tilesToRender.length; + + // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. + var bitMask = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); + var anyAreRenderable = isAnyAreRenderable(bitMask); + var allAreRenderable = isAllAreRenderable(bitMask); + var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(bitMask); + + if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) { + // If no descendant tiles were added to the render list, it means they were all + // culled even though this tile was deemed visible. That's pretty common. + + // Since we're in this `if` block though, at least one descendant tile _was_ added to the render list! + + var revealInChunks = primitive.revealInChunks; + if (revealInChunks && !allAreRenderable && !anyWereRenderedLastFrame || + !revealInChunks && !anyAreRenderable) { + // But none of our descendants are renderable, so they'll all end up rendering an ancestor. + // That's a big waste of time, so kick them all out of the render list and render this tile instead. + primitive._tilesToRender.length = firstRenderedDescendantIndex; + primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; + addTileToRenderList(primitive, tile, nearestRenderableTile); + anyAreRenderable = tile.renderable; + allAreRenderable = tile.renderable; + anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + } - // It'd be nice if we didn't need to do this; we could avoid loading heaps of tiles. - // But first we'd need a way to fill those holes. If we're showing a bunch of level 15 - // tiles and the user zooms out so we want to be showing level 14 but none of those - // level 14 tiles are loaded because we removed this line below, it's not really acceptable - // to a) draw nothing, or b) draw level 0 tiles, because either strategy will likely - // leave us with massive gaps visible in the globe. + // We'd like to be rendering one or more descendents, and we're either not rendering this + // tile at all, or we're only rendering it temporarily because _none_ of our children + // are renderable. In either case, load this tile with low priority so that zooming + // out or panning quickly doesn't leave us with huge holes in the terrain surface. - queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); - } + // It'd be nice if we didn't need to do this; we could avoid loading heaps of tiles. + // But first we'd need a way to fill those holes. If we're showing a bunch of level 15 + // tiles and the user zooms out so we want to be showing level 14 but none of those + // level 14 tiles are loaded because we removed this line below, it's not really acceptable + // to a) draw nothing, or b) draw level 0 tiles, because either strategy will likely + // leave us with massive gaps visible in the globe. - return createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } - } else { - reportTileAction(frameState, tile, 'can\'t refine'); - - // We'd like to refine but can't because we have no availability data for this tile's children, - // so we have no idea if refinining would involve a load or an upsample. We'll have to finish - // loading this tile first in order to find that out, so load this refinement blocker with - // high priority. - addTileToRenderList(primitive, tile, nearestRenderableTile); - queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); - return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); + return createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); } + + reportTileAction(frameState, tile, 'can\'t refine'); + + // We'd like to refine but can't because we have no availability data for this tile's children, + // so we have no idea if refinining would involve a load or an upsample. We'll have to finish + // loading this tile first in order to find that out, so load this refinement blocker with + // high priority. + addTileToRenderList(primitive, tile, nearestRenderableTile); + queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); + + return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); } function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile) { @@ -761,18 +761,18 @@ define([ var allAreRenderable = isAllAreRenderable(swMask) && isAllAreRenderable(seMask) && isAllAreRenderable(nwMask) && isAllAreRenderable(neMask); var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(swMask) || isAnyWereRenderedLastFrame(seMask) || isAnyWereRenderedLastFrame(nwMask) || isAnyWereRenderedLastFrame(neMask); var mask = createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); - return mask + return mask; } function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile) { if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { return visitTile(primitive, frameState, tile, nearestRenderableTile); - } else { - reportTileAction(frameState, tile, 'culled'); - ++primitive._debug.tilesCulled; - primitive._tileReplacementQueue.markTileRendered(tile); - return createBitMask(false, true, false); } + + reportTileAction(frameState, tile, 'culled'); + ++primitive._debug.tilesCulled; + primitive._tileReplacementQueue.markTileRendered(tile); + return createBitMask(false, true, false); } function screenSpaceError(primitive, frameState, tile) { diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 7e83f682b16..58f0e5d06a0 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -106,10 +106,6 @@ define([ this.data = undefined; } - function getPriority() { - return this._priority; - } - /** * Creates a rectangular set of tiles for level of detail zero, the coarsest, least detailed level. * From 5d79e11e5ff758894bc5e76ec4cb403378915702 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Jul 2018 14:48:39 +1000 Subject: [PATCH 026/131] Fix broken water mask. --- Source/Scene/GlobeSurfaceTile.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index bc3eb1817ec..825df49c112 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -519,6 +519,15 @@ define([ if (surfaceTile.terrainState === TerrainState.TRANSFORMED) { createResources(surfaceTile, frameState.context, terrainProvider, tile.x, tile.y, tile.level); } + + if (surfaceTile.terrainState >= TerrainState.RECEIVED && surfaceTile.waterMaskTexture === undefined && terrainProvider.hasWaterMask) { + var terrainData = surfaceTile.terrainData; + if (terrainData.waterMask !== undefined) { + createWaterMaskTextureIfNeeded(frameState.context, surfaceTile); + } else { + upsampleWaterMask(tile); + } + } } function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level) { From 61432c1fb54773751a95f64b18cf5f0d2673043b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Jul 2018 15:34:47 +1000 Subject: [PATCH 027/131] Fix 2D/CV. --- Source/Scene/GlobeSurfaceTileProvider.js | 3 +-- Source/Scene/QuadtreePrimitive.js | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 27555a8b58d..47850fd9c11 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -562,8 +562,7 @@ define([ BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume); Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); - if (frameState.mode === SceneMode.MORPHING) { - // TODO: mesh not necessarily available yet. + if (frameState.mode === SceneMode.MORPHING && surfaceTile.mesh !== undefined) { boundingVolume = BoundingSphere.union(surfaceTile.mesh.boundingSphere3D, boundingVolume, boundingVolume); } } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 194a143d8e6..fe0061c5746 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -9,6 +9,7 @@ define([ '../Core/getTimestamp', '../Core/Math', '../Core/OrthographicFrustum', + '../Core/OrthographicOffCenterFrustum', '../Core/Ray', '../Core/Rectangle', '../Core/Visibility', @@ -28,6 +29,7 @@ define([ getTimestamp, CesiumMath, OrthographicFrustum, + OrthographicOffCenterFrustum, Ray, Rectangle, Visibility, @@ -776,7 +778,7 @@ define([ } function screenSpaceError(primitive, frameState, tile) { - if (frameState.mode === SceneMode.SCENE2D || frameState.camera.frustum instanceof OrthographicFrustum) { + if (frameState.mode === SceneMode.SCENE2D || frameState.camera.frustum instanceof OrthographicFrustum || frameState.camera.frustum instanceof OrthographicOffCenterFrustum) { return screenSpaceError2D(primitive, frameState, tile); } From 3b77787a59013e1b784a6664e39a32975f62098a Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 26 Jul 2018 21:43:53 +1000 Subject: [PATCH 028/131] Super hacky fill tile generation. --- Apps/Sandcastle/gallery/Terrain.html | 18 + .../gallery/TerrainPerformance.html | 194 ++++++ Source/Core/CesiumTerrainProvider.js | 2 +- Source/Core/QuantizedMeshTerrainData.js | 70 +-- Source/Core/TerrainEncoding.js | 12 +- Source/Core/TerrainMesh.js | 273 +++++++- Source/Core/TerrainTileEdgeDetails.js | 17 + Source/Core/TileEdge.js | 13 + Source/Scene/GlobeSurfaceShaderSet.js | 8 +- Source/Scene/GlobeSurfaceTileProvider.js | 588 +++++++++++++++++- Source/Scene/QuadtreePrimitive.js | 22 +- Source/Scene/QuadtreeTile.js | 102 +++ Source/Scene/TileImagery.js | 10 +- .../createVerticesFromQuantizedTerrainMesh.js | 24 +- .../Workers/upsampleQuantizedTerrainMesh.js | 3 + Specs/Scene/GlobeSurfaceTileSpec.js | 6 - Specs/Scene/QuadtreeTileSpec.js | 192 ++++++ .../upsampleQuantizedTerrainMeshSpec.js | 65 ++ terrainnotes.md | 20 +- 19 files changed, 1535 insertions(+), 104 deletions(-) create mode 100644 Apps/Sandcastle/gallery/TerrainPerformance.html create mode 100644 Source/Core/TerrainTileEdgeDetails.js create mode 100644 Source/Core/TileEdge.js create mode 100644 Specs/Workers/upsampleQuantizedTerrainMeshSpec.js diff --git a/Apps/Sandcastle/gallery/Terrain.html b/Apps/Sandcastle/gallery/Terrain.html index abbbeab0ef0..2c8421dd5e9 100644 --- a/Apps/Sandcastle/gallery/Terrain.html +++ b/Apps/Sandcastle/gallery/Terrain.html @@ -40,6 +40,8 @@ terrainProvider: worldTerrain }); +Cesium.viewerCesiumInspectorMixin(viewer); + // set lighting to true viewer.scene.globe.enableLighting = true; @@ -57,6 +59,22 @@ viewer.terrainProvider = worldTerrain; viewer.scene.globe.enableLighting = true; } +}, { + text : 'STK World Terrain', + onselect : function() { + viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ + url: 'http://assets.agi.com/stk-terrain/world' + }); + viewer.scene.globe.enableLighting = false; + } +}, { + text : 'Local with Half Dome', + onselect : function() { + viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ + url: 'http://localhost:8081/2018-07-10' + }); + viewer.scene.globe.enableLighting = false; + } }, { text : 'CesiumTerrainProvider - Cesium World Terrain - no effects', onselect : function() { diff --git a/Apps/Sandcastle/gallery/TerrainPerformance.html b/Apps/Sandcastle/gallery/TerrainPerformance.html new file mode 100644 index 00000000000..64be27d730d --- /dev/null +++ b/Apps/Sandcastle/gallery/TerrainPerformance.html @@ -0,0 +1,194 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index aaba4bc315b..5924ce4d661 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -623,7 +623,7 @@ define([ return -1; } - var bvhLevels = layerToUse.bvhLevels; + var bvhLevels = layerToUse.bvhLevels - 1; return ((level / bvhLevels) | 0) * bvhLevels; }; diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index 6643ae2b24f..f73b83baadf 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -13,7 +13,8 @@ define([ './OrientedBoundingBox', './TaskProcessor', './TerrainEncoding', - './TerrainMesh' + './TerrainMesh', + './TileEdge' ], function( when, BoundingSphere, @@ -29,7 +30,8 @@ define([ OrientedBoundingBox, TaskProcessor, TerrainEncoding, - TerrainMesh) { + TerrainMesh, + TileEdge) { 'use strict'; /** @@ -353,7 +355,11 @@ define([ stride, obb, terrainEncoding, - exaggeration); + exaggeration, + result.westIndicesSouthToNorth, + result.southIndicesEastToWest, + result.eastIndicesNorthToSouth, + result.northIndicesWestToEast); // Free memory received from server after mesh is created. that._quantizedVertices = undefined; @@ -374,6 +380,7 @@ define([ }; var upsampleTaskProcessor = new TaskProcessor('upsampleQuantizedTerrainMesh'); + upsampleTaskProcessor.scheduleTask({}); // var startTime; // var notDeferredTime; @@ -442,7 +449,7 @@ define([ var childRectangle = tilingScheme.tileXYToRectangle(descendantX, descendantY, descendantLevel); var upsamplePromise = upsampleTaskProcessor.scheduleTask({ - // startTime : Date.now(), + startTime : Date.now(), // level : descendantLevel, // x : descendantX, // y : descendantY, @@ -522,61 +529,6 @@ define([ var maxShort = 32767; - // QuantizedMeshTerrainData.prototype.getFastMinMaxHeightsOfSubset = function(thisRectangle, descendantRectangle) { - // var minU = maxShort * (descendantRectangle.west - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); - // var maxU = maxShort * (descendantRectangle.east - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); - // var minV = maxShort * (descendantRectangle.south - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); - // var maxV = maxShort * (descendantRectangle.north - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); - - // var indices = this._indices; - // var uBuffer = this._uValues; - // var vBuffer = this._vValues; - // var heightBuffer = this._heightValues; - - // var minHeight = maxShort; - // var maxHeight = 0; - - // //CesiumMath.lerp(terrainData._minimumHeight, terrainData._maximumHeight, quantizedHeight / maxShort) - - // for (var i = 0, len = indices.length; i < len; i += 3) { - // var i0 = indices[i]; - // var i1 = indices[i + 1]; - // var i2 = indices[i + 2]; - - // var u0 = uBuffer[i0]; - // var u1 = uBuffer[i1]; - // var u2 = uBuffer[i2]; - - // var v0 = vBuffer[i0]; - // var v1 = vBuffer[i1]; - // var v2 = vBuffer[i2]; - - // var relevant = false; - // if (u0 >= minU && u0 <= maxU && v0 >= minV && v0 <= maxV || - // u1 >= minU && u1 <= maxU && v1 >= minV && v1 <= maxV || - // u2 >= minU && u2 <= maxU && v2 >= minV && v2 <= maxV) { - // // At least one vertex is inside the descendant bounds - // relevant = true; - // } else if (u0 < minU && u1 < minU && u2 < minU || - // u0 > maxU && u1 > maxU && u2 > maxU || - // v0 < minV && v1 < minV && v2 < minV || - // v0 > maxV && v1 > maxV && v2 < maxV) { - // // Triangle is entirely on a single side of a descendant border. - // relevant = false; - // } else if () - - // if (relevant) { - // minHeight = Math.min(minHeight, heightBuffer[i0]); - // minHeight = Math.min(minHeight, heightBuffer[i1]); - // minHeight = Math.min(minHeight, heightBuffer[i2]); - - // maxHeight = Math.max(maxHeight, heightBuffer[i0]); - // maxHeight = Math.max(maxHeight, heightBuffer[i1]); - // maxHeight = Math.max(maxHeight, heightBuffer[i2]); - // } - // } - // }; - var barycentricCoordinateScratch = new Cartesian3(); /** diff --git a/Source/Core/TerrainEncoding.js b/Source/Core/TerrainEncoding.js index 332fdf0ebd8..21bd2273dfb 100644 --- a/Source/Core/TerrainEncoding.js +++ b/Source/Core/TerrainEncoding.js @@ -45,7 +45,7 @@ define([ * @private */ function TerrainEncoding(axisAlignedBoundingBox, minimumHeight, maximumHeight, fromENU, hasVertexNormals, hasWebMercatorT) { - var quantization; + var quantization = TerrainQuantization.NONE; var center; var toENU; var matrix; @@ -248,6 +248,16 @@ define([ return buffer[index + 3]; }; + TerrainEncoding.prototype.decodeWebMercatorT = function(buffer, index) { + index *= this.getStride(); + + if (this.quantization === TerrainQuantization.BITS12) { + return AttributeCompression.decompressTextureCoordinates(buffer[index + 3], cartesian2Scratch).x; + } + + return buffer[index + 6]; + }; + TerrainEncoding.prototype.getOctEncodedNormal = function(buffer, index, result) { var stride = this.getStride(); index = (index + 1) * stride - 1; diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js index cffa35b8c7c..95cf942f793 100644 --- a/Source/Core/TerrainMesh.js +++ b/Source/Core/TerrainMesh.js @@ -1,7 +1,21 @@ define([ - './defaultValue' + './AttributeCompression', + './Cartesian2', + './Cartesian3', + './defaultValue', + './Math', + './TerrainTileEdgeDetails', + './TileEdge', + './WebMercatorProjection' ], function( - defaultValue) { + AttributeCompression, + Cartesian2, + Cartesian3, + defaultValue, + CesiumMath, + TerrainTileEdgeDetails, + TileEdge, + WebMercatorProjection) { 'use strict'; /** @@ -27,10 +41,19 @@ define([ * @param {OrientedBoundingBox} [orientedBoundingBox] A bounding box that completely contains the tile. * @param {TerrainEncoding} encoding Information used to decode the mesh. * @param {Number} exaggeration The amount that this mesh was exaggerated. + * @param {Number[]} westIndicesSouthToNorth The indices of the vertices on the Western edge of the tile, ordered from South to North (clockwise). + * @param {Number[]} southIndicesEastToWest The indices of the vertices on the Southern edge of the tile, ordered from East to West (clockwise). + * @param {Number[]} eastIndicesNorthToSouth The indices of the vertices on the Eastern edge of the tile, ordered from North to South (clockwise). + * @param {Number[]} northIndicesWestToEast The indices of the vertices on the Northern edge of the tile, ordered tom West to East (clockwise). * * @private */ - function TerrainMesh(center, vertices, indices, minimumHeight, maximumHeight, boundingSphere3D, occludeePointInScaledSpace, vertexStride, orientedBoundingBox, encoding, exaggeration) { + function TerrainMesh( + center, vertices, indices, minimumHeight, maximumHeight, + boundingSphere3D, occludeePointInScaledSpace, + vertexStride, orientedBoundingBox, encoding, exaggeration, + westIndicesSouthToNorth, southIndicesEastToWest, eastIndicesNorthToSouth, northIndicesWestToEast) { + /** * The center of the tile. Vertex positions are specified relative to this center. * @type {Cartesian3} @@ -104,7 +127,251 @@ define([ * @type {Number} */ this.exaggeration = exaggeration; + + /** + * The indices of the vertices on the Western edge of the tile, ordered from South to North (clockwise). + * @type {Number[]} + */ + this.westIndicesSouthToNorth = westIndicesSouthToNorth; + + /** + * The indices of the vertices on the Southern edge of the tile, ordered from East to West (clockwise). + * @type {Number[]} + */ + this.southIndicesEastToWest = southIndicesEastToWest; + + /** + * The indices of the vertices on the Eastern edge of the tile, ordered from North to South (clockwise). + * @type {Number[]} + */ + this.eastIndicesNorthToSouth = eastIndicesNorthToSouth; + + /** + * The indices of the vertices on the Northern edge of the tile, ordered from West to East (clockwise). + * @type {Number[]} + */ + this.northIndicesWestToEast = northIndicesWestToEast; + } + + var positionScratch = new Cartesian3(); + var encodedNormalScratch = new Cartesian2(); + var uvScratch = new Cartesian2(); + + function getVertex(encoding, vertices, index, result, resultIndex) { + resultIndex = defaultValue(resultIndex, result.length); + + encoding.decodePosition(vertices, index, positionScratch); + result[resultIndex++] = positionScratch.x; + result[resultIndex++] = positionScratch.y; + result[resultIndex++] = positionScratch.z; + + result[resultIndex++] = encoding.decodeHeight(vertices, index); + + encoding.decodeTextureCoordinates(vertices, index, uvScratch); + result[resultIndex++] = uvScratch.x; + result[resultIndex++] = uvScratch.y; + + if (encoding.hasWebMercatorT) { + result[resultIndex++] = encoding.decodeWebMercatorT(vertices, index); + } + + if (encoding.hasVertexNormals) { + encoding.getOctEncodedNormal(vertices, index, encodedNormalScratch); + result[resultIndex++] = encodedNormalScratch.x; + result[resultIndex++] = encodedNormalScratch.y; + } + + return resultIndex; + } + + function addVertex(ellipsoid, rectangle, encoding, u, v, height, octEncodedNormal, southMercatorY, oneOverMercatorHeight, result, resultIndex) { + var longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); + var latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); + Cartesian3.fromRadians(longitude, latitude, height, ellipsoid, positionScratch); + + result[resultIndex++] = positionScratch.x; + result[resultIndex++] = positionScratch.y; + result[resultIndex++] = positionScratch.z; + result[resultIndex++] = height; + result[resultIndex++] = u; + result[resultIndex++] = v; + + if (encoding.hasWebMercatorT) { + result[resultIndex++] = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - southMercatorY) * oneOverMercatorHeight; + } + + if (encoding.hasVertexNormals) { + result[resultIndex++] = octEncodedNormal.x; + result[resultIndex++] = octEncodedNormal.y; + } } + function transformTextureCoordinate(toMin, toMax, fromValue) { + return (fromValue - toMin) / (toMax - toMin); + } + + var previousVertexScratch = new Array(9); + var currentVertexScratch = new Array(9); + var normalScratch1 = new Cartesian3(); + var normalScratch2 = new Cartesian3(); + var normalScratch3 = new Cartesian3(); + + TerrainMesh.prototype.getEdgeVertices = function(tileEdge, thisRectangle, clipRectangle, ellipsoid, result) { + if (result === undefined) { + result = new TerrainTileEdgeDetails(); + } + var outputVertices = result.vertices; + var minimumHeight = result.minimumHeight; + var maximumHeight = result.maximumHeight; + + var clipRectangleUMin = (clipRectangle.west - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); + var clipRectangleUMax = (clipRectangle.east - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); + var clipRectangleVMin = (clipRectangle.south - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); + var clipRectangleVMax = (clipRectangle.north - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); + + var indices; + var first; + var second; + var edgeCoordinate; + var compareU; + + switch (tileEdge) { + case TileEdge.WEST: + indices = this.westIndicesSouthToNorth; + first = 0.0; + second = 1.0; + edgeCoordinate = transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, 0.0); + compareU = false; + break; + case TileEdge.NORTH: + indices = this.northIndicesWestToEast; + first = 0.0; + second = 1.0; + edgeCoordinate = transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, 1.0); + compareU = true; + break; + case TileEdge.EAST: + indices = this.eastIndicesNorthToSouth; + first = 1.0; + second = 0.0; + edgeCoordinate = transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, 1.0); + compareU = false; + break; + case TileEdge.SOUTH: + indices = this.southIndicesEastToWest; + first = 1.0; + second = 0.0; + edgeCoordinate = transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, 0.0); + compareU = true; + break; + } + + var encoding = this.encoding; + var vertices = this.vertices; + var southMercatorY; + var oneOverMercatorHeight; + var lastUOrV; + + var destinationStride = 6; + if (encoding.hasVertexNormals) { + destinationStride += 2; + } + if (encoding.hasWebMercatorT) { + ++destinationStride; + } + + if (encoding.hasWebMercatorT) { + southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(clipRectangle.south); + oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(clipRectangle.north) - southMercatorY); + } + + for (var i = 0; i < indices.length; ++i) { + var index = indices[i]; + + var uv = encoding.decodeTextureCoordinates(vertices, index, uvScratch); + var u = transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, uv.x); + var v = transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, uv.y); + var uOrV = compareU ? u : v; + + var inside = uOrV >= 0.0 && uOrV <= 1.0; + var crossedFirst = uOrV < first && lastUOrV > first || uOrV > first && lastUOrV < first; + var crossedSecond = uOrV < second && lastUOrV > second || uOrV > second && lastUOrV < second; + + var previousVertex = previousVertexScratch; + var currentVertex = currentVertexScratch; + + if (crossedFirst || crossedSecond) { + getVertex(encoding, vertices, index - 1, previousVertex, 0); + getVertex(encoding, vertices, index, currentVertex, 0); + } + + var ratio; + var interpolatedHeight; + var interpolatedU; + var interpolatedV; + var interpolatedOctEncodedNormal; + var previousNormal; + var currentNormal; + + if (crossedFirst) { + ratio = (first - uOrV) / (lastUOrV - uOrV); + interpolatedHeight = CesiumMath.lerp(currentVertex[3], previousVertex[3], ratio); + interpolatedU = compareU ? first : edgeCoordinate; + interpolatedV = compareU ? edgeCoordinate : first; + + if (encoding.hasVertexNormals) { + previousNormal = AttributeCompression.octDecode(previousVertex[destinationStride - 2], previousVertex[destinationStride - 1], normalScratch1); + currentNormal = AttributeCompression.octDecode(currentVertex[destinationStride - 2], currentVertex[destinationStride - 1], normalScratch2); + Cartesian3.lerp(currentNormal, previousNormal, ratio, normalScratch3); + Cartesian3.normalize(normalScratch3, normalScratch3); + interpolatedOctEncodedNormal = AttributeCompression.octEncode(normalScratch3, encodedNormalScratch); + } + + addVertex(ellipsoid, clipRectangle, encoding, interpolatedU, interpolatedV, interpolatedHeight, interpolatedOctEncodedNormal, southMercatorY, oneOverMercatorHeight, outputVertices, outputVertices.length); + minimumHeight = Math.min(minimumHeight, interpolatedHeight); + maximumHeight = Math.max(maximumHeight, interpolatedHeight); + } + + if (inside) { + getVertex(encoding, vertices, index, outputVertices, outputVertices.length); + var vertexStart = outputVertices.length - destinationStride; + outputVertices[vertexStart + 4] = compareU ? transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, outputVertices[vertexStart + 4]) : edgeCoordinate; + outputVertices[vertexStart + 5] = compareU ? edgeCoordinate : transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, outputVertices[vertexStart + 5]); + if (encoding.hasWebMercatorT) { + var latitude = CesiumMath.lerp(clipRectangle.south, clipRectangle.north, v); + outputVertices[vertexStart + 6] = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - southMercatorY) * oneOverMercatorHeight; + } + minimumHeight = Math.min(minimumHeight, outputVertices[vertexStart + 3]); + maximumHeight = Math.max(maximumHeight, outputVertices[vertexStart + 3]); + } + + if (crossedSecond) { + ratio = (second - uOrV) / (lastUOrV - uOrV); + interpolatedHeight = CesiumMath.lerp(currentVertex[3], previousVertex[3], ratio); + interpolatedU = compareU ? second : edgeCoordinate; + interpolatedV = compareU ? edgeCoordinate : second; + + if (encoding.hasVertexNormals) { + previousNormal = AttributeCompression.octDecode(previousVertex[destinationStride - 2], previousVertex[destinationStride - 1], normalScratch1); + currentNormal = AttributeCompression.octDecode(currentVertex[destinationStride - 2], currentVertex[destinationStride - 1], normalScratch2); + Cartesian3.lerp(currentNormal, previousNormal, ratio, normalScratch3); + Cartesian3.normalize(normalScratch3, normalScratch3); + interpolatedOctEncodedNormal = AttributeCompression.octEncode(normalScratch3, encodedNormalScratch); + } + + addVertex(ellipsoid, clipRectangle, encoding, interpolatedU, interpolatedV, interpolatedHeight, interpolatedOctEncodedNormal, southMercatorY, oneOverMercatorHeight, outputVertices, outputVertices.length); + minimumHeight = Math.min(minimumHeight, interpolatedHeight); + maximumHeight = Math.max(maximumHeight, interpolatedHeight); + } + + lastUOrV = uOrV; + } + + result.minimumHeight = minimumHeight; + result.maximumHeight = maximumHeight; + + return result; + }; + return TerrainMesh; }); diff --git a/Source/Core/TerrainTileEdgeDetails.js b/Source/Core/TerrainTileEdgeDetails.js new file mode 100644 index 00000000000..0b3db614834 --- /dev/null +++ b/Source/Core/TerrainTileEdgeDetails.js @@ -0,0 +1,17 @@ +define([], function() { + 'use strict'; + + function TerrainTileEdgeDetails() { + this.vertices = []; + this.minimumHeight = Number.MAX_VALUE; + this.maximumHeight = -Number.MAX_VALUE; + } + + TerrainTileEdgeDetails.prototype.clear = function() { + this.vertices.length = 0; + this.minimumHeight = Number.MAX_VALUE; + this.maximumHeight = -Number.MAX_VALUE; + }; + + return TerrainTileEdgeDetails; +}); diff --git a/Source/Core/TileEdge.js b/Source/Core/TileEdge.js new file mode 100644 index 00000000000..9fd47013365 --- /dev/null +++ b/Source/Core/TileEdge.js @@ -0,0 +1,13 @@ +define([ + ], function() { + 'use strict'; + + var TileEdge = { + WEST: 0, + NORTH: 1, + EAST: 2, + SOUTH: 3 + }; + + return TileEdge; +}); diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 674b8ca9a82..b0ebe8fe39e 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -79,10 +79,10 @@ define([ var vertexLogDepth = 0; var vertexLogDepthDefine = ''; - if (surfaceTile.terrainData._createdByUpsampling) { - vertexLogDepth = 1; - vertexLogDepthDefine = 'DISABLE_GL_POSITION_LOG_DEPTH'; - } + // if (surfaceTile.terrainData._createdByUpsampling) { + // vertexLogDepth = 1; + // vertexLogDepthDefine = 'DISABLE_GL_POSITION_LOG_DEPTH'; + // } var sceneMode = frameState.mode; var flags = sceneMode | diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 47850fd9c11..b6b7646d70c 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1,9 +1,11 @@ define([ + '../Core/AttributeCompression', '../Core/BoundingSphere', '../Core/BoxOutlineGeometry', '../Core/Cartesian2', '../Core/Cartesian3', '../Core/Cartesian4', + '../Core/Cartographic', '../Core/Color', '../Core/ColorGeometryInstanceAttribute', '../Core/combine', @@ -18,13 +20,18 @@ define([ '../Core/IndexDatatype', '../Core/Intersect', '../Core/Math', + '../Core/Matrix3', '../Core/Matrix4', '../Core/OrientedBoundingBox', '../Core/OrthographicFrustum', '../Core/PrimitiveType', '../Core/Rectangle', '../Core/SphereOutlineGeometry', + '../Core/TerrainEncoding', + '../Core/TerrainMesh', '../Core/TerrainQuantization', + '../Core/TerrainTileEdgeDetails', + '../Core/TileEdge', '../Core/Visibility', '../Core/WebMercatorProjection', '../Renderer/Buffer', @@ -36,6 +43,7 @@ define([ '../Renderer/VertexArray', '../Scene/BlendingState', '../Scene/DepthFunction', + '../Scene/ImageryState', '../Scene/PerInstanceColorAppearance', '../Scene/Primitive', '../Scene/TileBoundingRegion', @@ -46,11 +54,13 @@ define([ './SceneMode', './ShadowMode' ], function( + AttributeCompression, BoundingSphere, BoxOutlineGeometry, Cartesian2, Cartesian3, Cartesian4, + Cartographic, Color, ColorGeometryInstanceAttribute, combine, @@ -65,13 +75,18 @@ define([ IndexDatatype, Intersect, CesiumMath, + Matrix3, Matrix4, OrientedBoundingBox, OrthographicFrustum, PrimitiveType, Rectangle, SphereOutlineGeometry, + TerrainEncoding, + TerrainMesh, TerrainQuantization, + TerrainTileEdgeDetails, + TileEdge, Visibility, WebMercatorProjection, Buffer, @@ -83,6 +98,7 @@ define([ VertexArray, BlendingState, DepthFunction, + ImageryState, PerInstanceColorAppearance, Primitive, TileBoundingRegion, @@ -94,9 +110,35 @@ define([ ShadowMode) { 'use strict'; + /** + * The strategy to use to fill the space of tiles that are selected for rendering but that + * are not yet loaded / renderable. + * @private + */ var MissingTileStrategy = { + /** + * Render nothing, the globe will have holes during load. + */ RENDER_NOTHING: 0, - RENDER_ANCESTOR_SUBSET: 1 + + /** + * Render a subset of the closest renderable ancestor. This is cheap on the CPU, but can be very expensive in terms + * of GPU fill rate. It also leads to cracking due to missing skirts. + */ + RENDER_ANCESTOR_SUBSET: 1, + + /** + * Create a very simple tile to fill the space by matching the heights of adjacent tiles on the edges. This is + * cheaper on the CPU than a full upsample and avoids cracks. But it's less representative of the real + * terrain surface than an upsample from a nearby ancestor. + */ + CREATE_FILL_TILE: 2 + + /** + * Synchronously upsample the tile. This is expensive on the CPU and, when skipping several levels, it sometimes + * results in big cracks anyway. (currently not implemented) + */ + // SYNCHRONOUS_UPSAMPLE: 3 }; /** @@ -137,7 +179,7 @@ define([ /** * The strategy to use to fill holes in the globe when terrain tiles are not yet loaded. */ - this.missingTileStrategy = MissingTileStrategy.RENDER_ANCESTOR_SUBSET; + this.missingTileStrategy = MissingTileStrategy.CREATE_FILL_TILE; this._quadtree = undefined; this._terrainProvider = options.terrainProvider; @@ -501,6 +543,8 @@ define([ return this._terrainProvider.getLevelMaximumGeometricError(level); }; + var stopLoad = false; + /** * Loads, or continues loading, a given tile. This function will continue to be called * until {@link QuadtreeTile#state} is no longer {@link QuadtreeTileLoadState#LOADING}. This function should @@ -512,6 +556,9 @@ define([ * @exception {DeveloperError} loadTile must not be called before the tile provider is ready. */ GlobeSurfaceTileProvider.prototype.loadTile = function(frameState, tile) { + if (stopLoad) { + return; + } GlobeSurfaceTile.processStateMachine(tile, frameState, this._terrainProvider, this._imageryLayers, this._vertexArraysToDestroy); var tileLoadedEvent = this._tileLoadedEvent; @@ -699,6 +746,19 @@ define([ var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()]; + function computeOccludeePoint(tileProvider, center, rectangle, height, result) { + var ellipsoidalOccluder = tileProvider.quadtree._occluders.ellipsoid; + var ellipsoid = ellipsoidalOccluder.ellipsoid; + + var cornerPositions = cornerPositionsScratch; + Cartesian3.fromRadians(rectangle.west, rectangle.south, height, ellipsoid, cornerPositions[0]); + Cartesian3.fromRadians(rectangle.east, rectangle.south, height, ellipsoid, cornerPositions[1]); + Cartesian3.fromRadians(rectangle.west, rectangle.north, height, ellipsoid, cornerPositions[2]); + Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]); + + return ellipsoidalOccluder.computeHorizonCullingPoint(center, cornerPositions, result); +} + /** * Gets the distance from the camera to the closest point on the tile. This is used for level-of-detail selection. * @@ -741,19 +801,7 @@ define([ tile.tilingScheme.ellipsoid, surfaceTile.orientedBoundingBox); - var rectangle = tile.rectangle; - var height = tileBoundingRegion.minimumHeight; - - var ellipsoidalOccluder = this.quadtree._occluders.ellipsoid; - var ellipsoid = ellipsoidalOccluder.ellipsoid; - - var cornerPositions = cornerPositionsScratch; - Cartesian3.fromRadians(rectangle.west, rectangle.south, height, ellipsoid, cornerPositions[0]); - Cartesian3.fromRadians(rectangle.east, rectangle.south, height, ellipsoid, cornerPositions[1]); - Cartesian3.fromRadians(rectangle.west, rectangle.north, height, ellipsoid, cornerPositions[2]); - Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]); - - surfaceTile.occludeePointInScaledSpace = ellipsoidalOccluder.computeHorizonCullingPoint(surfaceTile.orientedBoundingBox.center, cornerPositions, surfaceTile.occludeePointInScaledSpace); + surfaceTile.occludeePointInScaledSpace = computeOccludeePoint(this, surfaceTile.orientedBoundingBox.center, tile.rectangle, tileBoundingRegion.maximumHeight, surfaceTile.occludeePointInScaledSpace); } var min = tileBoundingRegion.minimumHeight; @@ -772,10 +820,12 @@ define([ } } + var result = tileBoundingRegion.distanceToCamera(frameState); + tileBoundingRegion.minimumHeight = min; tileBoundingRegion.maximumHeight = max; - return tileBoundingRegion.distanceToCamera(frameState); + return result; }; function updateTileBoundingRegion(tile, terrainProvider, frameState) { @@ -1320,6 +1370,490 @@ define([ }; })(); + function findRenderedTiles(startTile, currentFrameNumber, edge) { + if (startTile._frameRendered === currentFrameNumber) { + return [startTile]; + } + + if (startTile._frameVisited !== currentFrameNumber) { + // This tile wasn't even visited, so find the closest ancestor that was. + var tile = startTile.parent; + while (tile && tile._frameVisited !== currentFrameNumber) { + tile = tile.parent; + } + + if (!tile || tile._frameRendered !== currentFrameNumber) { + // No ancestor was visited, or the closest visited ancestor was culled. + return []; + } + + return [tile]; + } + + // This tile was visited but not rendered, so find rendered children, if any. + // Return the tiles in clockwise order. + switch (edge) { + case TileEdge.WEST: + return findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge)); + case TileEdge.EAST: + return findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge)); + case TileEdge.SOUTH: + return findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge)); + case TileEdge.NORTH: + return findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge)); + default: + throw new DeveloperError('Invalid edge'); + } + } + + function getEdgeVertices(tile, startingTile, currentFrameNumber, tileEdge, result) { + var ellipsoid = tile.tilingScheme.ellipsoid; + var edgeTiles = findRenderedTiles(startingTile, currentFrameNumber, tileEdge); + + result.clear(); + + for (var i = 0; i < edgeTiles.length; ++i) { + var edgeTile = edgeTiles[i]; + var surfaceTile = edgeTile.data; + if (surfaceTile && surfaceTile.mesh) { + surfaceTile.mesh.getEdgeVertices(tileEdge, edgeTile.rectangle, tile.rectangle, ellipsoid, result); + } + } + + return result; + } + + function getAverageHeight(vertices, stride) { + var sum = 0; + for (var i = 3; i < vertices.length; i += stride) { + sum += vertices[i]; + } + return sum / (vertices.length / stride); + } + + function hasVertexAtStart(edge, edgeDetails, stride) { + var vertices = edgeDetails.vertices; + + if (vertices.length === 0) { + return false; + } + + var firstU = vertices[4]; + var firstV = vertices[5]; + + switch (edge) { + case TileEdge.WEST: + return firstV === 1.0; + case TileEdge.SOUTH: + return firstU === 0.0; + case TileEdge.EAST: + return firstV === 0.0; + case TileEdge.NORTH: + return firstU === 1.0; + default: + throw new DeveloperError('Unsupported case'); + } + } + + function hasVertexAtEnd(edge, edgeDetails, stride) { + var vertices = edgeDetails.vertices; + + if (vertices.length === 0) { + return false; + } + + var lastVertexStart = (vertices.length - 1) * stride; + var lastU = vertices[lastVertexStart + 4]; + var lastV = vertices[lastVertexStart + 5]; + + switch (edge) { + case TileEdge.WEST: + return lastV === 0.0; + case TileEdge.SOUTH: + return lastU === 1.0; + case TileEdge.EAST: + return lastV === 1.0; + case TileEdge.NORTH: + return lastU === 0.0; + default: + throw new DeveloperError('Unsupported case'); + } + } + + function countOutputVertices(edge, edgeDetails, stride) { + // Don't count the last vertex, because that is shared with the next edge + var vertices = edgeDetails.vertices; + var vertexCount = vertices.length - 1; + + if (!hasVertexAtStart(edge, edgeDetails, stride)) { + // First vertex is not at the start of the edge, so we'll have to insert an extra vertex there. + ++vertexCount; + } + + if (!hasVertexAtEnd(edge, edgeDetails, stride)) { + // Normally the vertex at the end of the edge is included in the _next_ edge. But in this case, + // the last vertex falls short of the end of the edge, so include it. + ++vertexCount; + } + + return vertexCount; + } + + var cartographicScratch = new Cartographic(); + var cartesianScratch = new Cartesian3(); + var normalScratch = new Cartesian3(); + var octEncodedNormalScratch = new Cartesian2(); + + function addCornerVertexIfNecessary(ellipsoid, u, v, longitude, latitude, height, edgeDetails, previousEdgeDetails, hasVertexNormals, hasWebMercatorT, tileVertices) { + var vertices = edgeDetails.vertices; + + if (u === vertices[4] && v === vertices[5]) { + // First vertex is a corner vertex, as expected. + return; + } + + // Can we use the last vertex of the previous edge as the corner vertex? + var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); + var previousVertices = previousEdgeDetails.vertices; + var lastVertexStart = previousVertices.length - stride; + var lastU = previousVertices[lastVertexStart + 4]; + var lastV = previousVertices[lastVertexStart + 5]; + + if (lastU === u && lastV === v) { + for (var i = 0; i < stride; ++i) { + tileVertices.push(previousVertices[lastVertexStart + i]); + } + return; + } + + // Previous edge doesn't contain a suitable vertex either, so fabricate one. + cartographicScratch.longitude = longitude; + cartographicScratch.latitude = latitude; + cartographicScratch.height = height; + ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + tileVertices.push(cartesianScratch.x, cartesianScratch.y, cartesianScratch.z, height, u, v); + + if (hasWebMercatorT) { + // Identical to v at 0.0 and 1.0. + tileVertices.push(v); + } + + if (hasVertexNormals) { + ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); + AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); + tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); + //tileVertices.push(AttributeCompression.octPackFloat(octEncodedNormalScratch)); + } + } + + function addVerticesToFillTile(edgeDetails, stride, tileVertices) { + var vertices = edgeDetails.vertices; + + // Copy all but the last vertex. + var i; + for (i = 0; i < vertices.length - stride; ++i) { + tileVertices.push(vertices[i]); + } + + // Copy the last vertex too if it's _not_ a corner vertex. + var lastVertexStart = i; + var u = vertices[lastVertexStart + 4]; + var v = vertices[lastVertexStart + 5]; + if (lastVertexStart < vertices.length && ((u !== 0.0 && u !== 1.0) || (v !== 0.0 && v !== 1.0))) { + for (; i < vertices.length; ++i) { + tileVertices.push(vertices[i]); + } + } + } + + var westScratch = new TerrainTileEdgeDetails(); + var southScratch = new TerrainTileEdgeDetails(); + var eastScratch = new TerrainTileEdgeDetails(); + var northScratch = new TerrainTileEdgeDetails(); + var tileVerticesScratch = []; + + function createFillTile(tileProvider, tile, frameState) { + console.log('L' + tile.level + 'X' + tile.x + 'Y' + tile.y); + var start = performance.now(); + + var mesh; + var typedArray; + var indices; + var surfaceTile = tile.data; + // if (surfaceTile.mesh === undefined) { + var levelZeroTiles = tileProvider._quadtree._levelZeroTiles; + + var west = getEdgeVertices(tile, tile.findTileToWest(levelZeroTiles), frameState.frameNumber, TileEdge.EAST, westScratch); + var south = getEdgeVertices(tile, tile.findTileToSouth(levelZeroTiles), frameState.frameNumber, TileEdge.NORTH, southScratch); + var east = getEdgeVertices(tile, tile.findTileToEast(levelZeroTiles), frameState.frameNumber, TileEdge.WEST, eastScratch); + var north = getEdgeVertices(tile, tile.findTileToNorth(levelZeroTiles), frameState.frameNumber, TileEdge.SOUTH, northScratch); + + var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; + var hasWebMercatorT = true; // TODO + var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); + + var minimumHeight = Number.MAX_VALUE; + var maximumHeight = -Number.MAX_VALUE; + var hasAnyVertices = false; + + if (west.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, west.minimumHeight); + maximumHeight = Math.max(maximumHeight, west.maximumHeight); + hasAnyVertices = true; + } + + if (south.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, south.minimumHeight); + maximumHeight = Math.max(maximumHeight, south.maximumHeight); + hasAnyVertices = true; + } + + if (east.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, east.minimumHeight); + maximumHeight = Math.max(maximumHeight, east.maximumHeight); + hasAnyVertices = true; + } + + if (north.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, north.minimumHeight); + maximumHeight = Math.max(maximumHeight, north.maximumHeight); + hasAnyVertices = true; + } + + if (!hasAnyVertices) { + var tileBoundingRegion = surfaceTile.tileBoundingRegion; + minimumHeight = tileBoundingRegion.minimumHeight; + maximumHeight = tileBoundingRegion.maximumHeight; + } + + var middleHeight = (minimumHeight + maximumHeight) * 0.5; + + var tileVertices = tileVerticesScratch; + tileVertices.length = 0; + + var ellipsoid = tile.tilingScheme.ellipsoid; + var rectangle = tile.rectangle; + + var northwestIndex = 0; + addCornerVertexIfNecessary(ellipsoid, 0.0, 1.0, rectangle.west, rectangle.north, middleHeight, west, north, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(west, stride, tileVertices); + var southwestIndex = tileVertices.length / stride; + addCornerVertexIfNecessary(ellipsoid, 0.0, 0.0, rectangle.west, rectangle.south, middleHeight, south, west, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(south, stride, tileVertices); + var southeastIndex = tileVertices.length / stride; + addCornerVertexIfNecessary(ellipsoid, 1.0, 0.0, rectangle.east, rectangle.south, middleHeight, east, south, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(east, stride, tileVertices); + var northeastIndex = tileVertices.length / stride; + addCornerVertexIfNecessary(ellipsoid, 1.0, 1.0, rectangle.east, rectangle.north, middleHeight, north, east, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(north, stride, tileVertices); + + // Add a single vertex at the center of the tile. + var obb = OrientedBoundingBox.fromRectangle(tile.rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); + var center = obb.center; + + ellipsoid.cartesianToCartographic(center, cartographicScratch); + cartographicScratch.height = middleHeight; + var centerVertexPosition = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + + tileVertices.push(centerVertexPosition.x, centerVertexPosition.y, centerVertexPosition.z, middleHeight); + tileVertices.push((cartographicScratch.longitude - rectangle.west) / (rectangle.east - rectangle.west)); + tileVertices.push((cartographicScratch.latitude - rectangle.south) / (rectangle.north - rectangle.south)); + + if (hasWebMercatorT) { + var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); + var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); + tileVertices.push((WebMercatorProjection.geodeticLatitudeToMercatorAngle(cartographicScratch.latitude) - southMercatorY) * oneOverMercatorHeight); + } + + if (hasVertexNormals) { + ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); + AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); + tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); + } + + var vertexCount = tileVertices.length / stride; + indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex + var centerIndex = vertexCount - 1; + + var indexOut = 0; + var i; + for (i = 0; i < vertexCount - 2; ++i) { + indices[indexOut++] = centerIndex; + indices[indexOut++] = i; + indices[indexOut++] = i + 1; + } + + indices[indexOut++] = centerIndex; + indices[indexOut++] = i; + indices[indexOut++] = 0; + + var westIndicesSouthToNorth = []; + for (i = southwestIndex; i >= northwestIndex; --i) { + westIndicesSouthToNorth.push(i); + } + + var southIndicesEastToWest = []; + for (i = southeastIndex; i >= southwestIndex; --i) { + southIndicesEastToWest.push(i); + } + + var eastIndicesNorthToSouth = []; + for (i = northeastIndex; i >= southeastIndex; --i) { + eastIndicesNorthToSouth.push(i); + } + + var northIndicesWestToEast = []; + northIndicesWestToEast.push(0); + for (i = centerIndex - 1; i >= northeastIndex; --i) { + northIndicesWestToEast.push(i); + } + + // var westVertexCount = countOutputVertices(west, stride); + // var southVertexCount = countOutputVertices(west, stride); + // var eastVertexCount = countOutputVertices(east, stride); + // var northVertexCount = countOutputVertices(north, stride); + // var vertexCount = westVertexCount + southVertexCount + eastVertexCount + northVertexCount; + + var packedStride = hasVertexNormals ? stride - 1 : stride; // normal is packed into 1 float + typedArray = new Float32Array(vertexCount * packedStride); + + for (i = 0; i < vertexCount; ++i) { + var read = i * stride; + var write = i * packedStride; + typedArray[write++] = tileVertices[read++] - center.x; + typedArray[write++] = tileVertices[read++] - center.y; + typedArray[write++] = tileVertices[read++] - center.z; + typedArray[write++] = tileVertices[read++]; + typedArray[write++] = tileVertices[read++]; + typedArray[write++] = tileVertices[read++]; + + if (hasWebMercatorT) { + typedArray[write++] = tileVertices[read++]; + } + + if (hasVertexNormals) { + typedArray[write++] = AttributeCompression.octPackFloat(Cartesian2.fromElements(tileVertices[read++], tileVertices[read++], octEncodedNormalScratch)); + } + } + + var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, hasVertexNormals, hasWebMercatorT); + encoding.center = center; + + mesh = new TerrainMesh( + obb.center, + typedArray, + indices, + minimumHeight, + maximumHeight, + BoundingSphere.fromOrientedBoundingBox(obb), + computeOccludeePoint(tileProvider, center, rectangle, maximumHeight), + encoding.getStride(), + obb, + encoding, + frameState.terrainExaggeration, + westIndicesSouthToNorth, + southIndicesEastToWest, + eastIndicesNorthToSouth, + northIndicesWestToEast + ); + + var oldMesh = surfaceTile.mesh; + surfaceTile.mesh = mesh; + // } else { + // mesh = surfaceTile.mesh; + // typedArray = mesh.vertices; + // indices = mesh.indices; + // } + + var context = frameState.context; + + var buffer = Buffer.createVertexBuffer({ + context : context, + typedArray : typedArray, + usage : BufferUsage.STATIC_DRAW + }); + var attributes = mesh.encoding.getAttributes(buffer); + + var indexBuffers = mesh.indices.indexBuffers || {}; + var indexBuffer = indexBuffers[context.id]; + if (!defined(indexBuffer) || indexBuffer.isDestroyed()) { + var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; + indexBuffer = Buffer.createIndexBuffer({ + context : context, + typedArray : surfaceTile.mesh.indices, + usage : BufferUsage.STATIC_DRAW, + indexDatatype : indexDatatype + }); + indexBuffer.vertexArrayDestroyable = false; + indexBuffer.referenceCount = 1; + indexBuffers[context.id] = indexBuffer; + surfaceTile.mesh.indices.indexBuffers = indexBuffers; + } else { + ++indexBuffer.referenceCount; + } + + var oldVertexArray = surfaceTile.vertexArray; + surfaceTile.vertexArray = new VertexArray({ + context : context, + attributes : attributes, + indexBuffer : indexBuffer + }); + + var tileImageryCollection = surfaceTile.imagery; + + var len; + if (tileImageryCollection.length === 0) { + var imageryLayerCollection = tileProvider._imageryLayers; + var terrainProvider = tileProvider.terrainProvider; + for (i = 0, len = imageryLayerCollection.length; i < len; ++i) { + var layer = imageryLayerCollection.get(i); + if (layer.show) { + layer._createTileImagerySkeletons(tile, terrainProvider); + } + } + } + + for (i = 0, len = tileImageryCollection.length; i < len; ++i) { + var tileImagery = tileImageryCollection[i]; + if (!defined(tileImagery.loadingImagery)) { + continue; + } + + if (tileImagery.loadingImagery.state === ImageryState.PLACEHOLDER) { + var imageryLayer = tileImagery.loadingImagery.imageryLayer; + if (imageryLayer.imageryProvider.ready) { + // Remove the placeholder and add the actual skeletons (if any) + // at the same position. Then continue the loop at the same index. + tileImagery.freeResources(); + tileImageryCollection.splice(i, 1); + imageryLayer._createTileImagerySkeletons(tile, tileProvider.terrainProvider, i); + --i; + len = tileImageryCollection.length; + continue; + } + } + + tileImagery.processStateMachine(tile, frameState, true); + } + + var oldRenderable = tile.renderable; + tile.renderable = true; + + var stop = performance.now(); + + console.log('fill: ' + (stop - start)); + + var oldRenderableTile = surfaceTile.renderableTile; + surfaceTile.renderableTile = undefined; + + addDrawCommandsForTile(tileProvider, tile, frameState, undefined); + + surfaceTile.renderableTile = oldRenderableTile; + tile.renderable = oldRenderable; + surfaceTile.vertexArray = oldVertexArray; // TODO: free it + surfaceTile.mesh = oldMesh; + } + var otherPassesInitialColor = new Cartesian4(0.0, 0.0, 0.0, 0.0); function addDrawCommandsForTile(tileProvider, tile, frameState, subset) { @@ -1330,14 +1864,20 @@ define([ var missingTileStrategy = tileProvider.missingTileStrategy; if (missingTileStrategy === MissingTileStrategy.RENDER_ANCESTOR_SUBSET) { addDrawCommandsForTile(tileProvider, surfaceTile.renderableTile, frameState, surfaceTile.renderableTileSubset); + return; + } else if (missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE) { + //if (tile.level===11&&tile.x===3037&&tile.y===705) { + createFillTile(tileProvider, tile, frameState); + //} + return; + // TODO: get ancestor imagery if necessary } - return; } //>>includeStart('debug', pragmas.debug); - if (!tile.renderable) { - throw new DeveloperError('A rendered tile is not renderable, this should not be possible.'); - } + // if (!tile.renderable) { + // throw new DeveloperError('A rendered tile is not renderable, this should not be possible.'); + // } //>>includeEnd('debug'); var creditDisplay = frameState.creditDisplay; @@ -1474,11 +2014,15 @@ define([ ++tileProvider._usedDrawCommands; if (tile === tileProvider._debug.boundingSphereTile) { + //var obb = surfaceTile.orientedBoundingBox; + var obb = new OrientedBoundingBox( + new Cartesian3(296241.1872327779, 5633328.627100673, 2981274.607864871), + Matrix3.unpack([-2161.393502911665, 113.66171224228782, 0, -60.148176278433304, -1143.778981114305, 2152.7355430938214, 82.359194412753, 1566.144167609453, 834.4157931580422], 0)); // If a debug primitive already exists for this tile, it will not be // re-created, to avoid allocation every frame. If it were possible // to have more than one selected tile, this would have to change. - if (defined(surfaceTile.orientedBoundingBox)) { - getDebugOrientedBoundingBox(surfaceTile.orientedBoundingBox, Color.RED).update(frameState); + if (defined(obb)) { + getDebugOrientedBoundingBox(obb, Color.RED).update(frameState); } else if (defined(surfaceTile.mesh) && defined(surfaceTile.mesh.boundingSphere3D)) { getDebugBoundingSphere(surfaceTile.mesh.boundingSphere3D, Color.RED).update(frameState); } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index fe0061c5746..ead4cf3efdd 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -137,7 +137,7 @@ define([ * when all visible siblings in the same quad are renderable, OR if they were * shown in the last frame. Detail will not flicker out with either settings. * @type {Boolean} - * @default true + * @default false */ this.revealInChunks = false; @@ -629,6 +629,24 @@ define([ } if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) { + // TODO: This is the tile we want to render this frame, but if it's not renderable yet + // we'll do something different depending on what we did _last_ frame. + // 1. If any descendant tiles were rendered last frame: + // A. Continue rendering previously-rendered descendants, and + // B. Create fill tiles for previously-culled descendants. + // 2. If this tile's subtree was culled last frame: + // A. Generate a fill for this tile. + // 3. If an ancestor tile was rendered last frame: + // A. Continue rendering the ancestor until all its visible descendants are renderable. + // + // 3A means that we will sometimes wait a long time with no visible progress and then + // a bunch of detail will appear all at once. To mitigate this, count the number of + // visible-but-not-loaded descendants of a given tile. If it's higher than + // some threshold, load this tile's visible children instead of any of its + // deeper descendants. Then, for traversal back up, we say that the tile is + // waiting on (up to) 4 descendants instead of the total number its really + // waiting on. + // This tile meets SSE requirements, so render it and load it with medium priority. reportTileAction(frameState, tile, 'meets SSE'); queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); @@ -767,6 +785,8 @@ define([ } function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile) { + tile._frameVisited = frameState.frameNumber; + if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { return visitTile(primitive, frameState, tile, nearestRenderableTile); } diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 58f0e5d06a0..9f642630359 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -71,6 +71,7 @@ define([ this._customData = []; this._frameUpdated = undefined; this._frameRendered = undefined; + this._frameVisited = undefined; this._loadedCallbacks = {}; /** @@ -394,6 +395,107 @@ define([ } }); + QuadtreeTile.prototype.findLevelZeroTile = function(levelZeroTiles, x, y) { + var xTiles = this.tilingScheme.getNumberOfXTilesAtLevel(0); + if (x < 0) { + x += xTiles; + } else if (x >= xTiles) { + x -= xTiles; + } + + if (y < 0 || y >= this.tilingScheme.getNumberOfYTilesAtLevel(0)) { + return undefined; + } + + return levelZeroTiles.filter(function(tile) { + return tile.x === x && tile.y === y; + })[0]; + }; + + QuadtreeTile.prototype.findTileToWest = function(levelZeroTiles) { + var parent = this.parent; + if (parent === undefined) { + return this.findLevelZeroTile(levelZeroTiles, this.x - 1, this.y); + } + + if (parent.southeastChild === this) { + return parent.southwestChild; + } else if (parent.northeastChild === this) { + return parent.northwestChild; + } + + var westOfParent = parent.findTileToWest(levelZeroTiles); + if (westOfParent === undefined) { + return undefined; + } else if (parent.southwestChild === this) { + return westOfParent.southeastChild; + } + return westOfParent.northeastChild; + }; + + QuadtreeTile.prototype.findTileToEast = function(levelZeroTiles) { + var parent = this.parent; + if (parent === undefined) { + return this.findLevelZeroTile(levelZeroTiles, this.x + 1, this.y); + } + + if (parent.southwestChild === this) { + return parent.southeastChild; + } else if (parent.northwestChild === this) { + return parent.northeastChild; + } + + var eastOfParent = parent.findTileToEast(levelZeroTiles); + if (eastOfParent === undefined) { + return undefined; + } else if (parent.southeastChild === this) { + return eastOfParent.southwestChild; + } + return eastOfParent.northwestChild; + }; + + QuadtreeTile.prototype.findTileToSouth = function(levelZeroTiles) { + var parent = this.parent; + if (parent === undefined) { + return this.findLevelZeroTile(levelZeroTiles, this.x, this.y + 1); + } + + if (parent.northwestChild === this) { + return parent.southwestChild; + } else if (parent.northeastChild === this) { + return parent.southeastChild; + } + + var southOfParent = parent.findTileToSouth(levelZeroTiles); + if (southOfParent === undefined) { + return undefined; + } else if (parent.southwestChild === this) { + return southOfParent.northwestChild; + } + return southOfParent.northeastChild; + }; + + QuadtreeTile.prototype.findTileToNorth = function(levelZeroTiles) { + var parent = this.parent; + if (parent === undefined) { + return this.findLevelZeroTile(levelZeroTiles, this.x, this.y - 1); + } + + if (parent.southwestChild === this) { + return parent.northwestChild; + } else if (parent.southeastChild === this) { + return parent.northeastChild; + } + + var northOfParent = parent.findTileToNorth(levelZeroTiles); + if (northOfParent === undefined) { + return undefined; + } else if (parent.northwestChild === this) { + return northOfParent.southwestChild; + } + return northOfParent.southeastChild; + }; + /** * Frees the resources associated with this tile and returns it to the START * {@link QuadtreeTileLoadState}. If the {@link QuadtreeTile#data} property is defined and it diff --git a/Source/Scene/TileImagery.js b/Source/Scene/TileImagery.js index 2e0d4a2a216..8da112b12ac 100644 --- a/Source/Scene/TileImagery.js +++ b/Source/Scene/TileImagery.js @@ -45,11 +45,13 @@ define([ * @param {FrameState} frameState The frameState. * @returns {Boolean} True if this instance is done loading; otherwise, false. */ - TileImagery.prototype.processStateMachine = function(tile, frameState) { + TileImagery.prototype.processStateMachine = function(tile, frameState, skipLoading) { var loadingImagery = this.loadingImagery; var imageryLayer = loadingImagery.imageryLayer; - loadingImagery.processStateMachine(frameState, !this.useWebMercatorT); + if (!skipLoading) { + loadingImagery.processStateMachine(frameState, !this.useWebMercatorT); + } if (loadingImagery.state === ImageryState.READY) { if (defined(this.readyImagery)) { @@ -91,7 +93,9 @@ define([ // Push the ancestor's load process along a bit. This is necessary because some ancestor imagery // tiles may not be attached directly to a terrain tile. Such tiles will never load if // we don't do it here. - closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT); + if (!skipLoading) { + closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT); + } return false; // not done loading } // This imagery tile is failed or invalid, and we have the "best available" substitute. diff --git a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js index edd2620b35c..a266481023e 100644 --- a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js +++ b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js @@ -104,8 +104,11 @@ define([ var maxLatitude = Number.NEGATIVE_INFINITY; for (var i = 0; i < quantizedVertexCount; ++i) { - var u = uBuffer[i] / maxShort; - var v = vBuffer[i] / maxShort; + var rawU = uBuffer[i]; + var rawV = vBuffer[i]; + + var u = rawU / maxShort; + var v = rawV / maxShort; var height = CesiumMath.lerp(minimumHeight, maximumHeight, heightBuffer[i] / maxShort); cartographicScratch.longitude = CesiumMath.lerp(west, east, u); @@ -133,6 +136,19 @@ define([ Cartesian3.maximumByComponent(cartesian3Scratch, maximum, maximum); } + var westIndicesSouthToNorth = parameters.westIndices.sort(function(a, b) { + return uvs[a].y - uvs[b].y; + }); + var eastIndicesNorthToSouth = parameters.eastIndices.sort(function(a, b) { + return uvs[b].y - uvs[a].y; + }); + var southIndicesEastToWest = parameters.southIndices.sort(function(a, b) { + return uvs[b].x - uvs[a].x; + }); + var northIndicesWestToEast = parameters.northIndices.sort(function(a, b) { + return uvs[a].x - uvs[b].x; + }); + var orientedBoundingBox; var boundingSphere; @@ -216,6 +232,10 @@ define([ return { vertices : vertexBuffer.buffer, indices : indexBuffer.buffer, + westIndicesSouthToNorth : westIndicesSouthToNorth, + southIndicesEastToWest : southIndicesEastToWest, + eastIndicesNorthToSouth : eastIndicesNorthToSouth, + northIndicesWestToEast : northIndicesWestToEast, vertexStride : vertexStride, center : center, minimumHeight : minimumHeight, diff --git a/Source/Workers/upsampleQuantizedTerrainMesh.js b/Source/Workers/upsampleQuantizedTerrainMesh.js index 3879e98ff95..20ca51b95fc 100644 --- a/Source/Workers/upsampleQuantizedTerrainMesh.js +++ b/Source/Workers/upsampleQuantizedTerrainMesh.js @@ -61,6 +61,9 @@ define([ // console.log('time until start: ' + (startTime - parameters.startTime)); // } + // var startTime = Date.now(); + // console.log('time until start: ' + (startTime - parameters.startTime)); + var isEastChild = parameters.isEastChild; var isNorthChild = parameters.isNorthChild; diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 99078336ab3..0774ebf6516 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -128,12 +128,6 @@ defineSuite([ expect(rootTile.state).toBe(QuadtreeTileLoadState.LOADING); }); - it('creates loadedTerrain but not upsampledTerrain for root tiles', function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - expect(rootTile.data.loadedTerrain).toBeDefined(); - expect(rootTile.data.upsampledTerrain).toBeUndefined(); - }); - it('non-root tiles get neither loadedTerrain nor upsampledTerrain when their parent is not loaded nor upsampled', function() { var children = rootTile.children; for (var i = 0; i < children.length; ++i) { diff --git a/Specs/Scene/QuadtreeTileSpec.js b/Specs/Scene/QuadtreeTileSpec.js index 7ff5af37bd7..6f01ff6d142 100644 --- a/Specs/Scene/QuadtreeTileSpec.js +++ b/Specs/Scene/QuadtreeTileSpec.js @@ -109,6 +109,198 @@ defineSuite([ }).toThrowDeveloperError(); }); + it('can get tiles around a root tile', function() { + var tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX : 3, + numberOfLevelZeroTilesY : 3 + }); + var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + + var L0X0Y0 = levelZeroTiles.filter(function(tile) { + return tile.x === 0 && tile.y === 0; + })[0]; + var L0X1Y0 = levelZeroTiles.filter(function(tile) { + return tile.x === 1 && tile.y === 0; + })[0]; + var L0X2Y0 = levelZeroTiles.filter(function(tile) { + return tile.x === 2 && tile.y === 0; + })[0]; + var L0X0Y1 = levelZeroTiles.filter(function(tile) { + return tile.x === 0 && tile.y === 1; + })[0]; + var L0X1Y1 = levelZeroTiles.filter(function(tile) { + return tile.x === 1 && tile.y === 1; + })[0]; + var L0X2Y1 = levelZeroTiles.filter(function(tile) { + return tile.x === 2 && tile.y === 1; + })[0]; + var L0X0Y2 = levelZeroTiles.filter(function(tile) { + return tile.x === 0 && tile.y === 2; + })[0]; + var L0X1Y2 = levelZeroTiles.filter(function(tile) { + return tile.x === 1 && tile.y === 2; + })[0]; + var L0X2Y2 = levelZeroTiles.filter(function(tile) { + return tile.x === 2 && tile.y === 2; + })[0]; + + expect(L0X0Y0.findTileToWest(levelZeroTiles)).toBe(L0X2Y0); + expect(L0X0Y0.findTileToEast(levelZeroTiles)).toBe(L0X1Y0); + expect(L0X0Y0.findTileToNorth(levelZeroTiles)).toBeUndefined(); + expect(L0X0Y0.findTileToSouth(levelZeroTiles)).toBe(L0X0Y1); + + expect(L0X1Y0.findTileToWest(levelZeroTiles)).toBe(L0X0Y0); + expect(L0X1Y0.findTileToEast(levelZeroTiles)).toBe(L0X2Y0); + expect(L0X1Y0.findTileToNorth(levelZeroTiles)).toBeUndefined(); + expect(L0X1Y0.findTileToSouth(levelZeroTiles)).toBe(L0X1Y1); + + expect(L0X2Y0.findTileToWest(levelZeroTiles)).toBe(L0X1Y0); + expect(L0X2Y0.findTileToEast(levelZeroTiles)).toBe(L0X0Y0); + expect(L0X2Y0.findTileToNorth(levelZeroTiles)).toBeUndefined(); + expect(L0X2Y0.findTileToSouth(levelZeroTiles)).toBe(L0X2Y1); + + expect(L0X0Y1.findTileToWest(levelZeroTiles)).toBe(L0X2Y1); + expect(L0X0Y1.findTileToEast(levelZeroTiles)).toBe(L0X1Y1); + expect(L0X0Y1.findTileToNorth(levelZeroTiles)).toBe(L0X0Y0); + expect(L0X0Y1.findTileToSouth(levelZeroTiles)).toBe(L0X0Y2); + + expect(L0X1Y1.findTileToWest(levelZeroTiles)).toBe(L0X0Y1); + expect(L0X1Y1.findTileToEast(levelZeroTiles)).toBe(L0X2Y1); + expect(L0X1Y1.findTileToNorth(levelZeroTiles)).toBe(L0X1Y0); + expect(L0X1Y1.findTileToSouth(levelZeroTiles)).toBe(L0X1Y2); + + expect(L0X2Y1.findTileToWest(levelZeroTiles)).toBe(L0X1Y1); + expect(L0X2Y1.findTileToEast(levelZeroTiles)).toBe(L0X0Y1); + expect(L0X2Y1.findTileToNorth(levelZeroTiles)).toBe(L0X2Y0); + expect(L0X2Y1.findTileToSouth(levelZeroTiles)).toBe(L0X2Y2); + + expect(L0X0Y2.findTileToWest(levelZeroTiles)).toBe(L0X2Y2); + expect(L0X0Y2.findTileToEast(levelZeroTiles)).toBe(L0X1Y2); + expect(L0X0Y2.findTileToNorth(levelZeroTiles)).toBe(L0X0Y1); + expect(L0X0Y2.findTileToSouth(levelZeroTiles)).toBeUndefined(); + + expect(L0X1Y2.findTileToWest(levelZeroTiles)).toBe(L0X0Y2); + expect(L0X1Y2.findTileToEast(levelZeroTiles)).toBe(L0X2Y2); + expect(L0X1Y2.findTileToNorth(levelZeroTiles)).toBe(L0X1Y1); + expect(L0X1Y2.findTileToSouth(levelZeroTiles)).toBeUndefined(); + + expect(L0X2Y2.findTileToWest(levelZeroTiles)).toBe(L0X1Y2); + expect(L0X2Y2.findTileToEast(levelZeroTiles)).toBe(L0X0Y2); + expect(L0X2Y2.findTileToNorth(levelZeroTiles)).toBe(L0X2Y1); + expect(L0X2Y2.findTileToSouth(levelZeroTiles)).toBeUndefined(); + }); + + it('can get tiles around a tile when they share a common parent', function() { + var tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX : 2, + numberOfLevelZeroTilesY : 1 + }); + + var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + var parent = levelZeroTiles[1]; + var sw = parent.southwestChild; + var se = parent.southeastChild; + var nw = parent.northwestChild; + var ne = parent.northeastChild; + + expect(sw.findTileToEast(levelZeroTiles)).toBe(se); + expect(sw.findTileToNorth(levelZeroTiles)).toBe(nw); + expect(se.findTileToWest(levelZeroTiles)).toBe(sw); + expect(se.findTileToNorth(levelZeroTiles)).toBe(ne); + expect(nw.findTileToEast(levelZeroTiles)).toBe(ne); + expect(nw.findTileToSouth(levelZeroTiles)).toBe(sw); + expect(ne.findTileToWest(levelZeroTiles)).toBe(nw); + expect(ne.findTileToSouth(levelZeroTiles)).toBe(se); + }); + + it('can get tiles around a tile when they do not share a common parent', function() { + var tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX : 2, + numberOfLevelZeroTilesY : 2 + }); + + var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + + var northwest = levelZeroTiles[0]; + var nwse = northwest.southeastChild; + var nwne = northwest.northeastChild; + var nwsw = northwest.southwestChild; + + var northeast = levelZeroTiles[1]; + var nesw = northeast.southwestChild; + var nenw = northeast.northwestChild; + var nese = northeast.southeastChild; + + var southwest = levelZeroTiles[2]; + var swne = southwest.northeastChild; + var swnw = southwest.northwestChild; + var swse = southwest.southeastChild; + + var southeast = levelZeroTiles[3]; + var senw = southeast.northwestChild; + var sene = southeast.northeastChild; + var sesw = southeast.southwestChild; + + expect(nwse.findTileToEast(levelZeroTiles)).toBe(nesw); + expect(nwse.findTileToSouth(levelZeroTiles)).toBe(swne); + expect(nwne.findTileToEast(levelZeroTiles)).toBe(nenw); + expect(nwsw.findTileToSouth(levelZeroTiles)).toBe(swnw); + + expect(nesw.findTileToWest(levelZeroTiles)).toBe(nwse); + expect(nesw.findTileToSouth(levelZeroTiles)).toBe(senw); + expect(nenw.findTileToWest(levelZeroTiles)).toBe(nwne); + expect(nese.findTileToSouth(levelZeroTiles)).toBe(sene); + + expect(swne.findTileToEast(levelZeroTiles)).toBe(senw); + expect(swne.findTileToNorth(levelZeroTiles)).toBe(nwse); + expect(swnw.findTileToNorth(levelZeroTiles)).toBe(nwsw); + expect(swse.findTileToEast(levelZeroTiles)).toBe(sesw); + + expect(senw.findTileToWest(levelZeroTiles)).toBe(swne); + expect(senw.findTileToNorth(levelZeroTiles)).toBe(nesw); + expect(sene.findTileToNorth(levelZeroTiles)).toBe(nese); + expect(sesw.findTileToWest(levelZeroTiles)).toBe(swse); + }); + + it('can get adjacent tiles wrapping around the anti-meridian', function() { + var tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX : 2, + numberOfLevelZeroTilesY : 1 + }); + + var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + + var west = levelZeroTiles[0]; + var wsw = west.southwestChild; + var wnw = west.northwestChild; + + var east = levelZeroTiles[1]; + var ene = east.northeastChild; + var ese = east.southeastChild; + + expect(wsw.findTileToWest(levelZeroTiles)).toBe(ese); + expect(wnw.findTileToWest(levelZeroTiles)).toBe(ene); + + expect(ene.findTileToEast(levelZeroTiles)).toBe(wnw); + expect(ese.findTileToEast(levelZeroTiles)).toBe(wsw); + }); + + it('returns undefined when asked for adjacent tiles north of the north pole or south of the south pole', function() { + var tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX : 2, + numberOfLevelZeroTilesY : 1 + }); + + var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + + var west = levelZeroTiles[0]; + var wnw = west.northwestChild; + var wsw = west.southwestChild; + + expect(wnw.findTileToNorth(levelZeroTiles)).toBeUndefined(); + expect(wsw.findTileToSouth(levelZeroTiles)).toBeUndefined(); + }); + describe('createLevelZeroTiles', function() { var tilingScheme1x1; var tilingScheme2x2; diff --git a/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js b/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js new file mode 100644 index 00000000000..cc71808ce67 --- /dev/null +++ b/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js @@ -0,0 +1,65 @@ +defineSuite([ + 'Workers/upsampleQuantizedTerrainMesh', + 'Core/createWorldTerrain' + ], function( + upsampleQuantizedTerrainMesh, + createWorldTerrain) { + 'use strict'; + + + it('time', function() { + var worldTerrain = createWorldTerrain({ + requestWaterMask: true, + requestVertexNormals: true + }); + + var total = 100; + var remaining = total; + var tiles = []; + function next() { + var current = total - remaining; + --remaining; + return worldTerrain.requestTileGeometry(1375 + current, 1189, 12).then(function(terrainData) { + return terrainData.createMesh(worldTerrain.tilingScheme, 1375, 1189, 12, 1.0).then(function(mesh) { + tiles[current] = { + terrainData: terrainData, + mesh: mesh + }; + if (remaining === 0) { + return tiles; + } + return next(); + }); + }); + } + + var promise = worldTerrain.readyPromise.then(function() { + return next(); + }); + + return promise.then(function(tiles) { + var childRectangle = worldTerrain.tilingScheme.tileXYToRectangle(2750, 2378, 13); + console.log(tiles.length); + var start = performance.now(); + for (var i = 0; i < tiles.length; ++i) { + var tile = tiles[i % tiles.length]; + upsampleQuantizedTerrainMesh._workerFunction({ + isEastChild: false, + isNorthChild: true, + vertices: tile.mesh.vertices, + indices: tile.mesh.indices, + skirtIndex: tile.terrainData._skirtIndex, + encoding: tile.mesh.encoding, + exaggeration: tile.mesh.exaggeration, + vertexCountWithoutSkirts: tile.terrainData._vertexCountWithoutSkirts, + minimumHeight: tile.terrainData._minimumHeight, + maximumHeight: tile.terrainData._maximumHeight, + ellipsoid: worldTerrain.tilingScheme.ellipsoid, + childRectangle: childRectangle + }, []); + } + var stop = performance.now(); + alert(stop - start); + }); + }); +}); diff --git a/terrainnotes.md b/terrainnotes.md index 15f07ad51e1..4d423e4882a 100644 --- a/terrainnotes.md +++ b/terrainnotes.md @@ -93,5 +93,21 @@ When the camera moves, new tiles become visible. If those tiles aren't loaded ye Number 5 is a tempting strategy, with one big problem: it can cause us to lose detail from the scene with small camera movements. For example, if we're looking at a detailed scene that shows 3 out of 4 children of a level 14 tile, and then move the camera so that the 4th is visible, suddenly that level 14 tile isn't refinable anymore. If we start rendering that level 14 tiles intead of its children, we'll lose detail from the scene for a second until that 4th tile loads. This looks really bad. -So, our rule is: if we rendered a tile last frame, and it's still visible and the correct SSE this frame, we must render it this frame. In the scenario above, we must use one of our other strategies to fill the space of that 4th child of the level 14 tiles. - +Important rules: +* if we rendered a tile last frame, and it's still visible and the correct SSE this frame, we must render it this frame. In the scenario above, we must use one of our other strategies to fill the space of that 4th child of the level 14 tiles. +* Detail should never disappear when zooming in. i.e. upsampling is ok, but creating fill tiles is not. +* We can create fill tiles for areas the user hasn't seen recently. + +Specifically: +* Ancestor rendered last frame -> upsample (e.g. zooming in) +* Descendants rendered last frame but now this tile meets SSE -> continue rendering descendants, create fill tiles as necessary for areas that weren't previously visible (e.g. zooming out) + * _Optionally_ we can preload ancestors to optimize the zoom out experience. + * Note that this applies to imagery too! Zooming out should never cause detail to disappear and then come back. +* Sub-tree was culled last frame but now it's visible -> create fill tiles as necessary (e.g. panning) + +## Min/max heights + +Tiles have min/max heights in a whole bunch of places 😨: +* GlobeSurfaceTile.tileBoundingRegion +* GlobeSurfaceTile.terrainData +* GlobeSurfaceTile.mesh.encoding From 4711ac87ed84b86fc1f2792533e604417ec9d5c7 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 27 Jul 2018 17:30:19 +1000 Subject: [PATCH 029/131] Don't load ancestors. Keep rendering higher-detail tiles when zooming out. --- Source/Scene/QuadtreePrimitive.js | 113 ++++++++++++++++++------------ 1 file changed, 68 insertions(+), 45 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index ead4cf3efdd..d987e003310 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -609,10 +609,12 @@ define([ * @param {FrameState} frameState The frame state. * @param {QuadtreeTile} tile The tile to visit * @param {QuadtreeTile} nearestRenderableTile The nearest ancestor tile for which the `renderable` property is true. + * @param {Boolean} ancestorMeetsSse True if a tile higher in the tile tree already met the SSE and we're refining further only + * to maintain detail while that higher tile loads. * @returns A bit mask where bit 1 is set if this tile or _any_ of its descendants are renderable, and bit 2 is * set if _all_ selected tiles starting with this tile are renderable. */ - function visitTile(primitive, frameState, tile, nearestRenderableTile) { + function visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse) { var debug = primitive._debug; ++debug.tilesVisited; @@ -628,37 +630,58 @@ define([ nearestRenderableTile = tile; } - if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) { - // TODO: This is the tile we want to render this frame, but if it's not renderable yet - // we'll do something different depending on what we did _last_ frame. - // 1. If any descendant tiles were rendered last frame: - // A. Continue rendering previously-rendered descendants, and - // B. Create fill tiles for previously-culled descendants. - // 2. If this tile's subtree was culled last frame: - // A. Generate a fill for this tile. - // 3. If an ancestor tile was rendered last frame: - // A. Continue rendering the ancestor until all its visible descendants are renderable. - // - // 3A means that we will sometimes wait a long time with no visible progress and then - // a bunch of detail will appear all at once. To mitigate this, count the number of - // visible-but-not-loaded descendants of a given tile. If it's higher than - // some threshold, load this tile's visible children instead of any of its - // deeper descendants. Then, for traversal back up, we say that the tile is - // waiting on (up to) 4 descendants instead of the total number its really - // waiting on. - - // This tile meets SSE requirements, so render it and load it with medium priority. - reportTileAction(frameState, tile, 'meets SSE'); - queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); - addTileToRenderList(primitive, tile, nearestRenderableTile); - return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); - } + var meetsSse = screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError; var southwestChild = tile.southwestChild; var southeastChild = tile.southeastChild; var northwestChild = tile.northwestChild; var northeastChild = tile.northeastChild; + var lastFrame = primitive._lastSelectionFrameNumber; + + if (meetsSse || ancestorMeetsSse) { + // Only load this tile if it (not just an ancestor) meets the SSE. + if (meetsSse) { + reportTileAction(frameState, tile, 'meets SSE'); + queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + } else { + reportTileAction(frameState, tile, 'ancestor meets SSE'); + } + + // This is the tile we want to render this frame, but we'll do different things depending + // on the state of this tile and on what we did _last_ frame. + + // 1. If it's completely loaded (terrain _and_ imagery), render it. + // TODO: technically we only need to ensure that the geometry and any imagery layers previously + // loaded on descendants are loaded on this tile. It doesn't need to be really, totally + // done loading. Hard to determine this though. + // 2. If it's renderable at all (even if not all imagery is loaded) and we were previously rendering it + // (even if we were rendering a fill), render it. + // 3. If this tile's children were not visited last frame because this tile was culled + // or because an ancestor tile met the SSE, then render this tile, or a fill if this tile isn't + // renderable yet. + var oneCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE; + var twoRenderableAndPreviouslyRendered = tile.renderable && tile._frameRendered === lastFrame; + var threeNoChildrenVisitedLastFrame = + southwestChild._frameVisited !== lastFrame && + southeastChild._frameVisited !== lastFrame && + northwestChild._frameVisited !== lastFrame && + northeastChild._frameVisited !== lastFrame; + + if (oneCompletelyLoaded || twoRenderableAndPreviouslyRendered || threeNoChildrenVisitedLastFrame) { + addTileToRenderList(primitive, tile, nearestRenderableTile); + return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); + } + + // 4. Otherwise, we can't render this tile (or its fill) because it would cause detail to disappear + // that was visible last frame. Instead, keep rendering any still-visible descendants that were rendered + // last frame and render fills for newly-visible descendants. E.g. if we were rendering level 15 last + // frame but this frame we want level 14 and the closest renderable level <= 14 is 0, rendering level + // zero would be pretty jarring so instead we keep rendering level 15 even though its SSE is better + // than required. So fall through to continue traversal... + ancestorMeetsSse = true; + } + var tileProvider = primitive.tileProvider; if (tileProvider.canRefine(tile)) { @@ -688,7 +711,7 @@ define([ var firstRenderedDescendantIndex = primitive._tilesToRender.length; // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. - var bitMask = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile); + var bitMask = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile, ancestorMeetsSse); var anyAreRenderable = isAnyAreRenderable(bitMask); var allAreRenderable = isAllAreRenderable(bitMask); var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(bitMask); @@ -724,7 +747,7 @@ define([ // to a) draw nothing, or b) draw level 0 tiles, because either strategy will likely // leave us with massive gaps visible in the globe. - queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + //queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } return createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); @@ -742,7 +765,7 @@ define([ return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); } - function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile) { + function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile, ancestorMeetsSse) { var cameraPosition = frameState.camera.positionCartographic; var tileProvider = primitive._tileProvider; var occluders = primitive._occluders; @@ -752,29 +775,29 @@ define([ if (cameraPosition.longitude < southwest.rectangle.east) { if (cameraPosition.latitude < southwest.rectangle.north) { // Camera in southwest quadrant - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); } else { // Camera in northwest quadrant - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); } } else if (cameraPosition.latitude < southwest.rectangle.north) { // Camera southeast quadrant - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); } else { // Camera in northeast quadrant - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile); - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile); - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile); - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile); + neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); } var anyAreRenderable = isAnyAreRenderable(swMask) || isAnyAreRenderable(seMask) || isAnyAreRenderable(nwMask) || isAnyAreRenderable(neMask); From db5f87d12387dfcd12b6955780ad56b0a42f8216 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 31 Jul 2018 15:43:43 +1000 Subject: [PATCH 030/131] Fix implementing new traversal. --- .../gallery/TerrainPerformance.html | 12 +- Source/Scene/GlobeSurfaceTileProvider.js | 87 +------- Source/Scene/QuadtreePrimitive.js | 186 +++++++++++++----- 3 files changed, 149 insertions(+), 136 deletions(-) diff --git a/Apps/Sandcastle/gallery/TerrainPerformance.html b/Apps/Sandcastle/gallery/TerrainPerformance.html index 64be27d730d..1b5b6fbf8a2 100644 --- a/Apps/Sandcastle/gallery/TerrainPerformance.html +++ b/Apps/Sandcastle/gallery/TerrainPerformance.html @@ -35,12 +35,12 @@ var globe = scene.globe; var statistics = Cesium.RequestScheduler.statistics; -var cesiumTerrainProviderMeshes = new Cesium.CesiumTerrainProvider({ - url : 'http://localhost:8081/2018-07-11b', - requestWaterMask : true, - requestVertexNormals : true -}); -viewer.terrainProvider = cesiumTerrainProviderMeshes; +// var cesiumTerrainProviderMeshes = new Cesium.CesiumTerrainProvider({ +// url : 'http://localhost:8081/2018-07-11b', +// requestWaterMask : true, +// requestVertexNormals : true +// }); +viewer.terrainProvider = Cesium.createWorldTerrain(); var startTime; var flightComplete; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index b6b7646d70c..d9a92153ec6 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1371,6 +1371,11 @@ define([ })(); function findRenderedTiles(startTile, currentFrameNumber, edge) { + if (startTile === undefined) { + // There are no tiles North or South of the poles. + return []; + } + if (startTile._frameRendered === currentFrameNumber) { return [startTile]; } @@ -1423,82 +1428,6 @@ define([ return result; } - function getAverageHeight(vertices, stride) { - var sum = 0; - for (var i = 3; i < vertices.length; i += stride) { - sum += vertices[i]; - } - return sum / (vertices.length / stride); - } - - function hasVertexAtStart(edge, edgeDetails, stride) { - var vertices = edgeDetails.vertices; - - if (vertices.length === 0) { - return false; - } - - var firstU = vertices[4]; - var firstV = vertices[5]; - - switch (edge) { - case TileEdge.WEST: - return firstV === 1.0; - case TileEdge.SOUTH: - return firstU === 0.0; - case TileEdge.EAST: - return firstV === 0.0; - case TileEdge.NORTH: - return firstU === 1.0; - default: - throw new DeveloperError('Unsupported case'); - } - } - - function hasVertexAtEnd(edge, edgeDetails, stride) { - var vertices = edgeDetails.vertices; - - if (vertices.length === 0) { - return false; - } - - var lastVertexStart = (vertices.length - 1) * stride; - var lastU = vertices[lastVertexStart + 4]; - var lastV = vertices[lastVertexStart + 5]; - - switch (edge) { - case TileEdge.WEST: - return lastV === 0.0; - case TileEdge.SOUTH: - return lastU === 1.0; - case TileEdge.EAST: - return lastV === 1.0; - case TileEdge.NORTH: - return lastU === 0.0; - default: - throw new DeveloperError('Unsupported case'); - } - } - - function countOutputVertices(edge, edgeDetails, stride) { - // Don't count the last vertex, because that is shared with the next edge - var vertices = edgeDetails.vertices; - var vertexCount = vertices.length - 1; - - if (!hasVertexAtStart(edge, edgeDetails, stride)) { - // First vertex is not at the start of the edge, so we'll have to insert an extra vertex there. - ++vertexCount; - } - - if (!hasVertexAtEnd(edge, edgeDetails, stride)) { - // Normally the vertex at the end of the edge is included in the _next_ edge. But in this case, - // the last vertex falls short of the end of the edge, so include it. - ++vertexCount; - } - - return vertexCount; - } - var cartographicScratch = new Cartographic(); var cartesianScratch = new Cartesian3(); var normalScratch = new Cartesian3(); @@ -1708,12 +1637,6 @@ define([ northIndicesWestToEast.push(i); } - // var westVertexCount = countOutputVertices(west, stride); - // var southVertexCount = countOutputVertices(west, stride); - // var eastVertexCount = countOutputVertices(east, stride); - // var northVertexCount = countOutputVertices(north, stride); - // var vertexCount = westVertexCount + southVertexCount + eastVertexCount + northVertexCount; - var packedStride = hasVertexNormals ? stride - 1 : stride; // normal is packed into 1 float typedArray = new Float32Array(vertexCount * packedStride); diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index d987e003310..396ac7af4bd 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -139,7 +139,7 @@ define([ * @type {Boolean} * @default false */ - this.revealInChunks = false; + this.revealInChunks = true; this._occluders = new QuadtreeOccluders({ ellipsoid : ellipsoid @@ -455,6 +455,8 @@ define([ return (alon * alon + alat * alat) - (blon * blon + blat * blat); } + var rootTraversalDetails = []; + function selectTilesForRendering(primitive, frameState) { var debug = primitive._debug; if (debug.suspendLodUpdate) { @@ -467,11 +469,21 @@ define([ primitive._nearestRenderableTiles.length = 0; // We can't render anything before the level zero tiles exist. + var i; var tileProvider = primitive._tileProvider; if (!defined(primitive._levelZeroTiles)) { if (tileProvider.ready) { var tilingScheme = tileProvider.tilingScheme; primitive._levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + var numberOfRootTiles = primitive._levelZeroTiles.length; + if (rootTraversalDetails.length < numberOfRootTiles) { + rootTraversalDetails = new Array(numberOfRootTiles); + for (i = 0; i < numberOfRootTiles; ++i) { + if (rootTraversalDetails[i] === undefined) { + rootTraversalDetails[i] = new TraversalDetails(); + } + } + } } else { // Nothing to do until the provider is ready. return; @@ -494,7 +506,6 @@ define([ var customDataRemoved = primitive._removeHeightCallbacks; var frameNumber = frameState.frameNumber; - var i; var len; if (customDataAdded.length > 0 || customDataRemoved.length > 0) { for (i = 0, len = levelZeroTiles.length; i < len; ++i) { @@ -536,7 +547,7 @@ define([ queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); ++debug.tilesWaitingForChildren; } else { - visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile); + visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile, false, rootTraversalDetails[i]); } } @@ -596,6 +607,44 @@ define([ return (anyAreRenderable ? 1 : 0) | (allAreRenderable ? 2 : 0) | (anyWereRenderedLastFrame ? 4 : 0); } + function TraversalDetails() { + this.allAreRenderable = true; + this.anyWereRenderedLastFrame = false; + this.anyAreRenderable = false; + this.notYetRenderableCount = 0; + } + + // TraversalDetails.prototype.clear = function() { + // this.allAreRenderable = true; + // this.anyWereRenderedLastFrame = false; + // this.anyAreRenderable = false; + // this.notYetRenderableCount = 0; + // }; + + function TraversalQuadDetails() { + this.southwest = new TraversalDetails(); + this.southeast = new TraversalDetails(); + this.northwest = new TraversalDetails(); + this.northeast = new TraversalDetails(); + } + + TraversalQuadDetails.prototype.combine = function(result) { + var southwest = this.southwest; + var southeast = this.southeast; + var northwest = this.northwest; + var northeast = this.northeast; + + result.allAreRenderable = southwest.allAreRenderable && southeast.allAreRenderable && northwest.allAreRenderable && northeast.allAreRenderable; + result.anyWereRenderedLastFrame = southwest.anyWereRenderedLastFrame || southeast.anyWereRenderedLastFrame || northwest.anyWereRenderedLastFrame || northeast.anyWereRenderedLastFrame; + result.anyAreRenderable = southwest.anyAreRenderable || southeast.anyAreRenderable || northwest.anyAreRenderable || northeast.anyAreRenderable; + result.notYetRenderableCount = southwest.notYetRenderableCount + southeast.notYetRenderableCount + northwest.notYetRenderableCount + northeast.notYetRenderableCount; + }; + + var traversalQuadsByLevel = new Array(30); // level 30 tiles are ~2cm wide at the equator, should be good enough. + for (var i = 0; i < traversalQuadsByLevel.length; ++i) { + traversalQuadsByLevel[i] = new TraversalQuadDetails(); + } + /** * Visits a tile for possible rendering. When we call this function with a tile: * @@ -614,7 +663,7 @@ define([ * @returns A bit mask where bit 1 is set if this tile or _any_ of its descendants are renderable, and bit 2 is * set if _all_ selected tiles starting with this tile are renderable. */ - function visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse) { + function visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { var debug = primitive._debug; ++debug.tilesVisited; @@ -640,10 +689,8 @@ define([ var lastFrame = primitive._lastSelectionFrameNumber; if (meetsSse || ancestorMeetsSse) { - // Only load this tile if it (not just an ancestor) meets the SSE. if (meetsSse) { reportTileAction(frameState, tile, 'meets SSE'); - queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); } else { reportTileAction(frameState, tile, 'ancestor meets SSE'); } @@ -669,8 +716,17 @@ define([ northeastChild._frameVisited !== lastFrame; if (oneCompletelyLoaded || twoRenderableAndPreviouslyRendered || threeNoChildrenVisitedLastFrame) { + // Only load this tile if it (not just an ancestor) meets the SSE. + if (meetsSse) { + queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + } addTileToRenderList(primitive, tile, nearestRenderableTile); - return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); + + traversalDetails.anyAreRenderable = tile.renderable; + traversalDetails.allAreRenderable = tile.renderable; + traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; + return; } // 4. Otherwise, we can't render this tile (or its fill) because it would cause detail to disappear @@ -680,6 +736,11 @@ define([ // zero would be pretty jarring so instead we keep rendering level 15 even though its SSE is better // than required. So fall through to continue traversal... ancestorMeetsSse = true; + + // Load this blocker tile with high priority, but only if this tile (not just an ancestor) meets the SSE. + if (meetsSse) { + queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); + } } var tileProvider = primitive.tileProvider; @@ -702,37 +763,59 @@ define([ primitive._tileReplacementQueue.markTileRendered(northwestChild); primitive._tileReplacementQueue.markTileRendered(northeastChild); - return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); + traversalDetails.anyAreRenderable = tile.renderable; + traversalDetails.allAreRenderable = tile.renderable; + traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; + return; } // SSE is not good enough, so refine. reportTileAction(frameState, tile, 'refine'); var firstRenderedDescendantIndex = primitive._tilesToRender.length; + var loadIndexLow = primitive._tileLoadQueueLow.length; + var loadIndexMedium = primitive._tileLoadQueueMedium.length; + var loadIndexHigh = primitive._tileLoadQueueHigh.length; // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. - var bitMask = visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile, ancestorMeetsSse); - var anyAreRenderable = isAnyAreRenderable(bitMask); - var allAreRenderable = isAllAreRenderable(bitMask); - var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(bitMask); + visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile, ancestorMeetsSse, traversalDetails); if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) { // If no descendant tiles were added to the render list, it means they were all // culled even though this tile was deemed visible. That's pretty common. // Since we're in this `if` block though, at least one descendant tile _was_ added to the render list! + var allAreRenderable = traversalDetails.allAreRenderable; + var anyWereRenderedLastFrame = traversalDetails.anyWereRenderedLastFrame; + var notYetRenderableCount = traversalDetails.notYetRenderableCount; - var revealInChunks = primitive.revealInChunks; - if (revealInChunks && !allAreRenderable && !anyWereRenderedLastFrame || - !revealInChunks && !anyAreRenderable) { - // But none of our descendants are renderable, so they'll all end up rendering an ancestor. - // That's a big waste of time, so kick them all out of the render list and render this tile instead. + if (!allAreRenderable && !anyWereRenderedLastFrame) { + // Some of our descendants aren't ready to render yet, and none were rendered last frame, + // so kick them all out of the render list and render this tile instead. Continue to load them though! primitive._tilesToRender.length = firstRenderedDescendantIndex; primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; addTileToRenderList(primitive, tile, nearestRenderableTile); - anyAreRenderable = tile.renderable; - allAreRenderable = tile.renderable; - anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + + // EXCEPT if we're waiting heaps of descendants, the above will take too long. So in that case, + // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting + // on this tile. + var heapsOfDescendants = 10; + var wasRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + if (!wasRenderedLastFrame && notYetRenderableCount > heapsOfDescendants) { + // TODO: load the visible _children_ of this tile, not this tile. + primitive._tileLoadQueueLow.length = loadIndexLow; + primitive._tileLoadQueueMedium.length = loadIndexMedium; + primitive._tileLoadQueueHigh.length = loadIndexHigh; + queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + traversalDetails.notYetRenderableCount = 1; + } + + traversalDetails.anyAreRenderable = tile.renderable; + traversalDetails.allAreRenderable = tile.renderable; + traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + + ++debug.tilesWaitingForChildren; } // We'd like to be rendering one or more descendents, and we're either not rendering this @@ -750,7 +833,7 @@ define([ //queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } - return createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); + return; } reportTileAction(frameState, tile, 'can\'t refine'); @@ -762,62 +845,69 @@ define([ addTileToRenderList(primitive, tile, nearestRenderableTile); queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); - return createBitMask(tile.renderable, tile.renderable, tile._frameRendered === primitive._lastSelectionFrameNumber); + traversalDetails.anyAreRenderable = tile.renderable; + traversalDetails.allAreRenderable = tile.renderable; + traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; } - function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile, ancestorMeetsSse) { + function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { var cameraPosition = frameState.camera.positionCartographic; var tileProvider = primitive._tileProvider; var occluders = primitive._occluders; - var swMask, seMask, nwMask, neMask; + var quadDetails = traversalQuadsByLevel[southwest.level]; + var southwestDetails = quadDetails.southwest; + var southeastDetails = quadDetails.southeast; + var northwestDetails = quadDetails.northwest; + var northeastDetails = quadDetails.northeast; if (cameraPosition.longitude < southwest.rectangle.east) { if (cameraPosition.latitude < southwest.rectangle.north) { // Camera in southwest quadrant - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); } else { // Camera in northwest quadrant - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); } } else if (cameraPosition.latitude < southwest.rectangle.north) { // Camera southeast quadrant - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); } else { // Camera in northeast quadrant - neMask = visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - nwMask = visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - seMask = visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); - swMask = visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); } - var anyAreRenderable = isAnyAreRenderable(swMask) || isAnyAreRenderable(seMask) || isAnyAreRenderable(nwMask) || isAnyAreRenderable(neMask); - var allAreRenderable = isAllAreRenderable(swMask) && isAllAreRenderable(seMask) && isAllAreRenderable(nwMask) && isAllAreRenderable(neMask); - var anyWereRenderedLastFrame = isAnyWereRenderedLastFrame(swMask) || isAnyWereRenderedLastFrame(seMask) || isAnyWereRenderedLastFrame(nwMask) || isAnyWereRenderedLastFrame(neMask); - var mask = createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame); - return mask; + quadDetails.combine(traversalDetails); } - function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile) { + function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { tile._frameVisited = frameState.frameNumber; if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { - return visitTile(primitive, frameState, tile, nearestRenderableTile); + return visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails); } reportTileAction(frameState, tile, 'culled'); ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); - return createBitMask(false, true, false); + + traversalDetails.anyAreRenderable = false; + traversalDetails.allAreRenderable = true; + traversalDetails.anyWereRenderedLastFrame = false; + traversalDetails.notYetRenderableCount = 0; } function screenSpaceError(primitive, frameState, tile) { From f9546e0d71885d4e71df25bd0b39a57d8a2f3c3b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 31 Jul 2018 20:15:34 +1000 Subject: [PATCH 031/131] Fix a bug, add some options. --- Source/Core/TerrainMesh.js | 3 +- Source/Scene/QuadtreePrimitive.js | 58 +++++++++++-------------------- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js index 95cf942f793..b170adec9c0 100644 --- a/Source/Core/TerrainMesh.js +++ b/Source/Core/TerrainMesh.js @@ -301,7 +301,8 @@ define([ var currentVertex = currentVertexScratch; if (crossedFirst || crossedSecond) { - getVertex(encoding, vertices, index - 1, previousVertex, 0); + var lastIndex = indices[i - 1]; + getVertex(encoding, vertices, lastIndex, previousVertex, 0); getVertex(encoding, vertices, index, currentVertex, 0); } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 396ac7af4bd..8406577bc1f 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -132,14 +132,26 @@ define([ this.tileCacheSize = defaultValue(options.tileCacheSize, 100); /** - * Gets or sets whether to reveal tiles in chunks while loading. If true, tiles - * will be shown as they become renderable. If false, tiles will only be shown - * when all visible siblings in the same quad are renderable, OR if they were - * shown in the last frame. Detail will not flicker out with either settings. + * Gets or sets the number of loading descendant tiles that is considered "too many". + * If a tile has too many loading descendants, that tile will be loaded and rendered before any of + * its descendants are loaded and rendered. This means more feedback for the user that something + * is happening at the cost of a longer overall load time. Setting this to 0 will cause each + * tile level to be loaded successively, significantly increasing load time. Setting it to a large + * number (e.g. 100000) will minimize the number of tiles that are loaded but tend to make + * detail appear all at once after a long wait. + * @type {Number} + * @default 20 + */ + this.loadingDescendantLimit = 20; + + /** + * Gets or sets a value indicating whether the ancestors of rendered tiles should be preloaded. + * Setting this to true optimizes the zoom-out experience and provides more detail in + * newly-exposed areas when panning. The down side is that is requires loading more tiles. * @type {Boolean} * @default false */ - this.revealInChunks = true; + this.preloadAncestors = false; this._occluders = new QuadtreeOccluders({ ellipsoid : ellipsoid @@ -591,22 +603,6 @@ define([ // } } - function isAnyAreRenderable(bitMask) { - return (bitMask & 1) === 1; - } - - function isAllAreRenderable(bitMask) { - return (bitMask & 2) === 2; - } - - function isAnyWereRenderedLastFrame(bitMask) { - return (bitMask & 4) === 4; - } - - function createBitMask(anyAreRenderable, allAreRenderable, anyWereRenderedLastFrame) { - return (anyAreRenderable ? 1 : 0) | (allAreRenderable ? 2 : 0) | (anyWereRenderedLastFrame ? 4 : 0); - } - function TraversalDetails() { this.allAreRenderable = true; this.anyWereRenderedLastFrame = false; @@ -800,10 +796,8 @@ define([ // EXCEPT if we're waiting heaps of descendants, the above will take too long. So in that case, // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting // on this tile. - var heapsOfDescendants = 10; var wasRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; - if (!wasRenderedLastFrame && notYetRenderableCount > heapsOfDescendants) { - // TODO: load the visible _children_ of this tile, not this tile. + if (!wasRenderedLastFrame && notYetRenderableCount > primitive.loadingDescendantLimit) { primitive._tileLoadQueueLow.length = loadIndexLow; primitive._tileLoadQueueMedium.length = loadIndexMedium; primitive._tileLoadQueueHigh.length = loadIndexHigh; @@ -818,19 +812,9 @@ define([ ++debug.tilesWaitingForChildren; } - // We'd like to be rendering one or more descendents, and we're either not rendering this - // tile at all, or we're only rendering it temporarily because _none_ of our children - // are renderable. In either case, load this tile with low priority so that zooming - // out or panning quickly doesn't leave us with huge holes in the terrain surface. - - // It'd be nice if we didn't need to do this; we could avoid loading heaps of tiles. - // But first we'd need a way to fill those holes. If we're showing a bunch of level 15 - // tiles and the user zooms out so we want to be showing level 14 but none of those - // level 14 tiles are loaded because we removed this line below, it's not really acceptable - // to a) draw nothing, or b) draw level 0 tiles, because either strategy will likely - // leave us with massive gaps visible in the globe. - - //queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + if (primitive.preloadAncestors) { + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + } } return; From aa4b8063299f3c7f7ae4424031d0b5ebd7b6b857 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 7 Aug 2018 17:18:19 +1000 Subject: [PATCH 032/131] Much better (but still imperfect) fill tiling. --- Source/Scene/GlobeSurfaceShaderSet.js | 11 +- Source/Scene/GlobeSurfaceTile.js | 55 +---- Source/Scene/GlobeSurfaceTileProvider.js | 224 +++++++++++------- Source/Scene/QuadtreePrimitive.js | 35 ++- Source/Scene/QuadtreeTile.js | 8 +- Source/Scene/TileSelectionResult.js | 33 +++ .../createVerticesFromQuantizedTerrainMesh.js | 8 +- 7 files changed, 225 insertions(+), 149 deletions(-) create mode 100644 Source/Scene/TileSelectionResult.js diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index b0ebe8fe39e..b4a0c20abe1 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -70,7 +70,8 @@ define([ var quantization = 0; var quantizationDefine = ''; - var terrainEncoding = surfaceTile.mesh.encoding; + var mesh = surfaceTile.mesh || surfaceTile.fillMesh; + var terrainEncoding = mesh.encoding; var quantizationMode = terrainEncoding.quantization; if (quantizationMode === TerrainQuantization.BITS12) { quantization = 1; @@ -79,10 +80,10 @@ define([ var vertexLogDepth = 0; var vertexLogDepthDefine = ''; - // if (surfaceTile.terrainData._createdByUpsampling) { - // vertexLogDepth = 1; - // vertexLogDepthDefine = 'DISABLE_GL_POSITION_LOG_DEPTH'; - // } + if (surfaceTile.terrainData !== undefined && surfaceTile.terrainData._createdByUpsampling) { + vertexLogDepth = 1; + vertexLogDepthDefine = 'DISABLE_GL_POSITION_LOG_DEPTH'; + } var sceneMode = frameState.mode; var flags = sceneMode | diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 825df49c112..d7e5efa2330 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -98,7 +98,9 @@ define([ this.terrainState = TerrainState.UNLOADED; this.mesh = undefined; + this.fillMesh = undefined; this.vertexArray = undefined; + this.fillVertexArray = undefined; // TODO: probably better to have a bounding sphere for 2D rather than one for picking. this.pickBoundingSphere = new BoundingSphere(); @@ -180,7 +182,7 @@ define([ var scratchResult = new Cartesian3(); GlobeSurfaceTile.prototype.pick = function(ray, mode, projection, cullBackFaces, result) { - var mesh = this.mesh; + var mesh = this.mesh || this.fillMesh; if (!defined(mesh)) { return undefined; } @@ -221,25 +223,10 @@ define([ this.terrainState = TerrainState.UNLOADED; this.mesh = undefined; - - if (defined(this.vertexArray)) { - var indexBuffer = this.vertexArray.indexBuffer; - - this.vertexArray.destroy(); - this.vertexArray = undefined; - - if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { - --indexBuffer.referenceCount; - if (indexBuffer.referenceCount === 0) { - indexBuffer.destroy(); - } - } - } - - var i, len; + this.fillMesh = undefined; var imageryList = this.imagery; - for (i = 0, len = imageryList.length; i < len; ++i) { + for (var i = 0, len = imageryList.length; i < len; ++i) { imageryList[i].freeResources(); } this.imagery.length = 0; @@ -263,6 +250,11 @@ define([ } } + if (defined(this.fillVertexArray)) { + this.fillVertexArray.destroy(); + this.fillVertexArray = undefined; + } + if (defined(this.wireframeVertexArray)) { indexBuffer = this.wireframeVertexArray.indexBuffer; @@ -277,22 +269,6 @@ define([ } }; - // var renderedJson = [{"level":18,"x":87944,"y":76105},{"level":18,"x":87945,"y":76105},{"level":19,"x":175890,"y":152209},{"level":19,"x":175891,"y":152209},{"level":19,"x":175892,"y":152210},{"level":19,"x":175892,"y":152208},{"level":18,"x":87947,"y":76104},{"level":18,"x":87948,"y":76103},{"level":18,"x":87949,"y":76102},{"level":17,"x":43975,"y":38051},{"level":17,"x":43975,"y":38050},{"level":16,"x":21991,"y":19026},{"level":16,"x":21991,"y":19027},{"level":16,"x":21989,"y":19025},{"level":17,"x":43977,"y":38049},{"level":16,"x":21989,"y":19024},{"level":16,"x":21990,"y":19025},{"level":16,"x":21991,"y":19025},{"level":16,"x":21990,"y":19024},{"level":16,"x":21991,"y":19024},{"level":16,"x":21991,"y":19028},{"level":16,"x":21991,"y":19029},{"level":16,"x":21991,"y":19030},{"level":16,"x":21992,"y":19026},{"level":16,"x":21992,"y":19027},{"level":16,"x":21993,"y":19026},{"level":16,"x":21993,"y":19027},{"level":15,"x":10997,"y":9513},{"level":15,"x":10996,"y":9512},{"level":15,"x":10997,"y":9512},{"level":16,"x":21992,"y":19028},{"level":16,"x":21992,"y":19029},{"level":16,"x":21993,"y":19028},{"level":16,"x":21993,"y":19029},{"level":15,"x":10996,"y":9515},{"level":15,"x":10997,"y":9514},{"level":15,"x":10997,"y":9515},{"level":15,"x":10998,"y":9513},{"level":15,"x":10999,"y":9513},{"level":15,"x":10998,"y":9512},{"level":15,"x":10999,"y":9512},{"level":15,"x":10998,"y":9514},{"level":15,"x":10998,"y":9515},{"level":15,"x":10999,"y":9514},{"level":15,"x":10999,"y":9515},{"level":15,"x":10998,"y":9516},{"level":15,"x":10999,"y":9516},{"level":14,"x":5500,"y":4756},{"level":14,"x":5500,"y":4757},{"level":14,"x":5501,"y":4756},{"level":14,"x":5501,"y":4757},{"level":14,"x":5500,"y":4758},{"level":14,"x":5501,"y":4758},{"level":14,"x":5501,"y":4759},{"level":14,"x":5502,"y":4756},{"level":14,"x":5502,"y":4757},{"level":14,"x":5503,"y":4756},{"level":14,"x":5503,"y":4757},{"level":14,"x":5502,"y":4758},{"level":14,"x":5502,"y":4759},{"level":14,"x":5503,"y":4758},{"level":14,"x":5503,"y":4759},{"level":16,"x":21991,"y":19023},{"level":16,"x":21993,"y":19023},{"level":16,"x":21993,"y":19022},{"level":15,"x":10997,"y":9511},{"level":15,"x":10998,"y":9511},{"level":15,"x":10999,"y":9511},{"level":15,"x":10998,"y":9510},{"level":15,"x":10999,"y":9510},{"level":14,"x":5500,"y":4755},{"level":14,"x":5501,"y":4755},{"level":14,"x":5500,"y":4754},{"level":14,"x":5501,"y":4754},{"level":14,"x":5502,"y":4755},{"level":14,"x":5503,"y":4755},{"level":14,"x":5502,"y":4754},{"level":14,"x":5503,"y":4754},{"level":14,"x":5503,"y":4753},{"level":13,"x":2751,"y":2380},{"level":13,"x":2752,"y":2378},{"level":13,"x":2752,"y":2379},{"level":13,"x":2753,"y":2378},{"level":13,"x":2753,"y":2379},{"level":13,"x":2754,"y":2378},{"level":13,"x":2754,"y":2379},{"level":13,"x":2755,"y":2378},{"level":13,"x":2755,"y":2379},{"level":13,"x":2752,"y":2377},{"level":13,"x":2753,"y":2377},{"level":13,"x":2752,"y":2376},{"level":13,"x":2753,"y":2376},{"level":13,"x":2754,"y":2377},{"level":13,"x":2755,"y":2377},{"level":13,"x":2754,"y":2376},{"level":13,"x":2752,"y":2380},{"level":13,"x":2753,"y":2380},{"level":13,"x":2753,"y":2381},{"level":12,"x":1377,"y":1190},{"level":12,"x":1377,"y":1191},{"level":12,"x":1378,"y":1189},{"level":12,"x":1379,"y":1189},{"level":12,"x":1378,"y":1188},{"level":12,"x":1379,"y":1188},{"level":12,"x":1378,"y":1190},{"level":12,"x":1378,"y":1191},{"level":12,"x":1379,"y":1190},{"level":11,"x":690,"y":594},{"level":11,"x":690,"y":595},{"level":11,"x":691,"y":594},{"level":11,"x":691,"y":595},{"level":12,"x":1377,"y":1187},{"level":12,"x":1379,"y":1187},{"level":12,"x":1379,"y":1186},{"level":11,"x":690,"y":593},{"level":11,"x":691,"y":593},{"level":11,"x":691,"y":592},{"level":11,"x":689,"y":596},{"level":11,"x":690,"y":596},{"level":11,"x":691,"y":596},{"level":11,"x":691,"y":597},{"level":10,"x":346,"y":297},{"level":10,"x":347,"y":297},{"level":10,"x":346,"y":296},{"level":10,"x":347,"y":296},{"level":10,"x":346,"y":298},{"level":10,"x":346,"y":299},{"level":10,"x":347,"y":298},{"level":10,"x":347,"y":299},{"level":9,"x":174,"y":148},{"level":9,"x":174,"y":149},{"level":9,"x":175,"y":148},{"level":9,"x":175,"y":149},{"level":9,"x":174,"y":150},{"level":9,"x":175,"y":150},{"level":10,"x":346,"y":295},{"level":10,"x":347,"y":295},{"level":9,"x":174,"y":147},{"level":9,"x":175,"y":147}]; - // var expectedTiles = {}; - // var unexpectedTiles = {}; - - // function tileID(level, x, y) { - // return `L${level}X${x}Y${y}`; - // } - // renderedJson.forEach(tile => { - // while (tile.level >= 0) { - // expectedTiles[tileID(tile.level, tile.x, tile.y)] = true; - // --tile.level; - // tile.x >>= 1; - // tile.y >>= 1; - // } - // }); - GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, terrainOnly) { var surfaceTile = tile.data; if (!defined(surfaceTile)) { @@ -339,14 +315,6 @@ define([ return; } - // var id = tileID(tile.level, tile.x, tile.y); - // if (!expectedTiles[id]) { - // if (!unexpectedTiles[id]) { - // unexpectedTiles[id] = true; - // console.log('Unexpected: ' + id); - // } - // } - // The terrain is renderable as soon as we have a valid vertex array. var isRenderable = defined(surfaceTile.vertexArray); @@ -487,9 +455,6 @@ define([ } } - // var startTime; - // var stopTime; - function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { var surfaceTile = tile.data; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index d9a92153ec6..eadf2cee5ac 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -41,12 +41,13 @@ define([ '../Renderer/Pass', '../Renderer/RenderState', '../Renderer/VertexArray', - '../Scene/BlendingState', - '../Scene/DepthFunction', - '../Scene/ImageryState', - '../Scene/PerInstanceColorAppearance', - '../Scene/Primitive', - '../Scene/TileBoundingRegion', + './BlendingState', + './DepthFunction', + './ImageryState', + './PerInstanceColorAppearance', + './Primitive', + './TileBoundingRegion', + './TileSelectionResult', './ClippingPlaneCollection', './GlobeSurfaceTile', './ImageryLayer', @@ -102,6 +103,7 @@ define([ PerInstanceColorAppearance, Primitive, TileBoundingRegion, + TileSelectionResult, ClippingPlaneCollection, GlobeSurfaceTile, ImageryLayer, @@ -1258,15 +1260,32 @@ define([ function createWireframeVertexArrayIfNecessary(context, provider, tile) { var surfaceTile = tile.data; - if (defined(surfaceTile.wireframeVertexArray)) { - return; + var mesh; + var vertexArray; + + if (surfaceTile.vertexArray !== undefined) { + mesh = surfaceTile.mesh; + vertexArray = surfaceTile.vertexArray; + } else if (surfaceTile.fillVertexArray !== undefined) { + mesh = surfaceTile.fillMesh; + vertexArray = surfaceTile.fillVertexArray; } - if (!defined(surfaceTile.terrainData) || !defined(surfaceTile.terrainData._mesh)) { + if (!defined(mesh) || !defined(vertexArray)) { return; } - surfaceTile.wireframeVertexArray = createWireframeVertexArray(context, surfaceTile.vertexArray, surfaceTile.terrainData._mesh); + if (defined(surfaceTile.wireframeVertexArray)) { + if (surfaceTile.wireframeVertexArray.mesh === mesh) { + return; + } + + surfaceTile.wireframeVertexArray.destroy(); + surfaceTile.wireframeVertexArray = undefined; + } + + surfaceTile.wireframeVertexArray = createWireframeVertexArray(context, vertexArray, mesh); + surfaceTile.wireframeVertexArray.mesh = mesh; } /** @@ -1370,42 +1389,50 @@ define([ }; })(); - function findRenderedTiles(startTile, currentFrameNumber, edge) { + function findRenderedTiles(startTile, currentFrameNumber, edge, downOnly) { if (startTile === undefined) { // There are no tiles North or South of the poles. return []; } - if (startTile._frameRendered === currentFrameNumber) { - return [startTile]; - } + if (startTile._lastSelectionResultFrame !== currentFrameNumber || startTile._lastSelectionResult === TileSelectionResult.KICKED) { + if (downOnly) { + return []; + } - if (startTile._frameVisited !== currentFrameNumber) { - // This tile wasn't even visited, so find the closest ancestor that was. + // This wasn't visited or was visited and then kicked, so walk up to find the closest ancestor that was rendered. var tile = startTile.parent; - while (tile && tile._frameVisited !== currentFrameNumber) { + while (tile && tile._lastSelectionResultFrame !== currentFrameNumber) { tile = tile.parent; } - if (!tile || tile._frameRendered !== currentFrameNumber) { - // No ancestor was visited, or the closest visited ancestor was culled. - return []; + if (tile !== undefined && tile._lastSelectionResult === TileSelectionResult.RENDERED) { + return [tile]; } - return [tile]; + // No ancestor was rendered. + return []; + } + + if (startTile._lastSelectionResult === TileSelectionResult.RENDERED) { + return [startTile]; + } + + if (startTile._lastSelectionResult === TileSelectionResult.CULLED) { + return []; } - // This tile was visited but not rendered, so find rendered children, if any. + // This tile was refined, so find rendered children, if any. // Return the tiles in clockwise order. switch (edge) { case TileEdge.WEST: - return findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge)); + return findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge, true)); case TileEdge.EAST: - return findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge)); + return findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge, true)); case TileEdge.SOUTH: - return findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge)); + return findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge, true)); case TileEdge.NORTH: - return findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge).concat(findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge)); + return findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge, true)); default: throw new DeveloperError('Invalid edge'); } @@ -1415,13 +1442,31 @@ define([ var ellipsoid = tile.tilingScheme.ellipsoid; var edgeTiles = findRenderedTiles(startingTile, currentFrameNumber, tileEdge); + tile.edgeTiles = tile.edgeTiles || []; + tile.edgeTiles[tileEdge] = edgeTiles; + result.clear(); for (var i = 0; i < edgeTiles.length; ++i) { var edgeTile = edgeTiles[i]; var surfaceTile = edgeTile.data; - if (surfaceTile && surfaceTile.mesh) { - surfaceTile.mesh.getEdgeVertices(tileEdge, edgeTile.rectangle, tile.rectangle, ellipsoid, result); + if (surfaceTile === undefined) { + continue; + } + + var mesh = surfaceTile.fillMesh; + if (surfaceTile.mesh !== undefined && surfaceTile.vertexArray !== undefined) { + mesh = surfaceTile.mesh; + } + + if (mesh !== undefined) { + var beforeLength = result.vertices.length; + mesh.getEdgeVertices(tileEdge, edgeTile.rectangle, tile.rectangle, ellipsoid, result); + var afterLength = result.vertices.length; + var numberOfVertices = afterLength - beforeLength; + if (surfaceTile.mesh === undefined && numberOfVertices > 27) { + console.log(`${numberOfVertices} from L${edgeTile.level}X${edgeTile.x}Y${edgeTile.y}`); + } } } @@ -1480,15 +1525,38 @@ define([ // Copy all but the last vertex. var i; - for (i = 0; i < vertices.length - stride; ++i) { - tileVertices.push(vertices[i]); + var u; + var v; + var lastU; + var lastV; + for (i = 0; i < vertices.length - stride; i += stride) { + u = vertices[i + 4]; + v = vertices[i + 5]; + if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { + // Vertex is very close to the previous one, so skip it. + continue; + } + + var end = i + stride; + for (var j = i; j < end; ++j) { + tileVertices.push(vertices[j]); + } + + lastU = u; + lastV = v; } // Copy the last vertex too if it's _not_ a corner vertex. var lastVertexStart = i; - var u = vertices[lastVertexStart + 4]; - var v = vertices[lastVertexStart + 5]; + u = vertices[lastVertexStart + 4]; + v = vertices[lastVertexStart + 5]; + if (lastVertexStart < vertices.length && ((u !== 0.0 && u !== 1.0) || (v !== 0.0 && v !== 1.0))) { + if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { + // Overwrite the previous vertex because it's very close to the last one. + tileVertices.length -= stride; + } + for (; i < vertices.length; ++i) { tileVertices.push(vertices[i]); } @@ -1502,7 +1570,7 @@ define([ var tileVerticesScratch = []; function createFillTile(tileProvider, tile, frameState) { - console.log('L' + tile.level + 'X' + tile.x + 'Y' + tile.y); + //console.log('L' + tile.level + 'X' + tile.x + 'Y' + tile.y); var start = performance.now(); var mesh; @@ -1510,12 +1578,14 @@ define([ var indices; var surfaceTile = tile.data; // if (surfaceTile.mesh === undefined) { - var levelZeroTiles = tileProvider._quadtree._levelZeroTiles; + var quadtree = tileProvider._quadtree; + var levelZeroTiles = quadtree._levelZeroTiles; + var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber; - var west = getEdgeVertices(tile, tile.findTileToWest(levelZeroTiles), frameState.frameNumber, TileEdge.EAST, westScratch); - var south = getEdgeVertices(tile, tile.findTileToSouth(levelZeroTiles), frameState.frameNumber, TileEdge.NORTH, southScratch); - var east = getEdgeVertices(tile, tile.findTileToEast(levelZeroTiles), frameState.frameNumber, TileEdge.WEST, eastScratch); - var north = getEdgeVertices(tile, tile.findTileToNorth(levelZeroTiles), frameState.frameNumber, TileEdge.SOUTH, northScratch); + var west = getEdgeVertices(tile, tile.findTileToWest(levelZeroTiles), lastSelectionFrameNumber, TileEdge.EAST, westScratch); + var south = getEdgeVertices(tile, tile.findTileToSouth(levelZeroTiles), lastSelectionFrameNumber, TileEdge.NORTH, southScratch); + var east = getEdgeVertices(tile, tile.findTileToEast(levelZeroTiles), lastSelectionFrameNumber, TileEdge.WEST, eastScratch); + var north = getEdgeVertices(tile, tile.findTileToNorth(levelZeroTiles), lastSelectionFrameNumber, TileEdge.SOUTH, northScratch); var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; var hasWebMercatorT = true; // TODO @@ -1680,8 +1750,7 @@ define([ northIndicesWestToEast ); - var oldMesh = surfaceTile.mesh; - surfaceTile.mesh = mesh; + surfaceTile.fillMesh = mesh; // } else { // mesh = surfaceTile.mesh; // typedArray = mesh.vertices; @@ -1690,6 +1759,11 @@ define([ var context = frameState.context; + if (surfaceTile.fillVertexArray !== undefined) { + surfaceTile.fillVertexArray.destroy(); + surfaceTile.fillVertexArray = undefined; + } + var buffer = Buffer.createVertexBuffer({ context : context, typedArray : typedArray, @@ -1697,26 +1771,15 @@ define([ }); var attributes = mesh.encoding.getAttributes(buffer); - var indexBuffers = mesh.indices.indexBuffers || {}; - var indexBuffer = indexBuffers[context.id]; - if (!defined(indexBuffer) || indexBuffer.isDestroyed()) { - var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; - indexBuffer = Buffer.createIndexBuffer({ - context : context, - typedArray : surfaceTile.mesh.indices, - usage : BufferUsage.STATIC_DRAW, - indexDatatype : indexDatatype - }); - indexBuffer.vertexArrayDestroyable = false; - indexBuffer.referenceCount = 1; - indexBuffers[context.id] = indexBuffer; - surfaceTile.mesh.indices.indexBuffers = indexBuffers; - } else { - ++indexBuffer.referenceCount; - } + var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; + var indexBuffer = Buffer.createIndexBuffer({ + context : context, + typedArray : mesh.indices, + usage : BufferUsage.STATIC_DRAW, + indexDatatype : indexDatatype + }); - var oldVertexArray = surfaceTile.vertexArray; - surfaceTile.vertexArray = new VertexArray({ + surfaceTile.fillVertexArray = new VertexArray({ context : context, attributes : attributes, indexBuffer : indexBuffer @@ -1759,22 +1822,9 @@ define([ tileImagery.processStateMachine(tile, frameState, true); } - var oldRenderable = tile.renderable; - tile.renderable = true; - var stop = performance.now(); - console.log('fill: ' + (stop - start)); - - var oldRenderableTile = surfaceTile.renderableTile; - surfaceTile.renderableTile = undefined; - - addDrawCommandsForTile(tileProvider, tile, frameState, undefined); - - surfaceTile.renderableTile = oldRenderableTile; - tile.renderable = oldRenderable; - surfaceTile.vertexArray = oldVertexArray; // TODO: free it - surfaceTile.mesh = oldMesh; + //console.log('fill: ' + (stop - start)); } var otherPassesInitialColor = new Cartesian4(0.0, 0.0, 0.0, 0.0); @@ -1789,11 +1839,7 @@ define([ addDrawCommandsForTile(tileProvider, surfaceTile.renderableTile, frameState, surfaceTile.renderableTileSubset); return; } else if (missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE) { - //if (tile.level===11&&tile.x===3037&&tile.y===705) { - createFillTile(tileProvider, tile, frameState); - //} - return; - // TODO: get ancestor imagery if necessary + createFillTile(tileProvider, tile, frameState); } } @@ -1832,8 +1878,9 @@ define([ --maxTextures; } - var rtc = surfaceTile.mesh.center; - var encoding = surfaceTile.mesh.encoding; + var mesh = surfaceTile.vertexArray ? surfaceTile.mesh : surfaceTile.fillMesh; + var rtc = mesh.center; + var encoding = mesh.encoding; // Not used in 3D. var tileRectangle = tileRectangleScratch; @@ -1937,17 +1984,14 @@ define([ ++tileProvider._usedDrawCommands; if (tile === tileProvider._debug.boundingSphereTile) { - //var obb = surfaceTile.orientedBoundingBox; - var obb = new OrientedBoundingBox( - new Cartesian3(296241.1872327779, 5633328.627100673, 2981274.607864871), - Matrix3.unpack([-2161.393502911665, 113.66171224228782, 0, -60.148176278433304, -1143.778981114305, 2152.7355430938214, 82.359194412753, 1566.144167609453, 834.4157931580422], 0)); + var obb = surfaceTile.orientedBoundingBox; // If a debug primitive already exists for this tile, it will not be // re-created, to avoid allocation every frame. If it were possible // to have more than one selected tile, this would have to change. if (defined(obb)) { getDebugOrientedBoundingBox(obb, Color.RED).update(frameState); - } else if (defined(surfaceTile.mesh) && defined(surfaceTile.mesh.boundingSphere3D)) { - getDebugBoundingSphere(surfaceTile.mesh.boundingSphere3D, Color.RED).update(frameState); + } else if (defined(mesh) && defined(mesh.boundingSphere3D)) { + getDebugBoundingSphere(mesh.boundingSphere3D, Color.RED).update(frameState); } } @@ -1958,7 +2002,7 @@ define([ uniformMapProperties.lightingFadeDistance.y = tileProvider.lightingFadeInDistance; uniformMapProperties.zoomedOutOceanSpecularIntensity = tileProvider.zoomedOutOceanSpecularIntensity; - uniformMapProperties.center3D = surfaceTile.mesh.center; + uniformMapProperties.center3D = mesh.center; Cartesian3.clone(rtc, uniformMapProperties.rtc); Cartesian4.clone(tileRectangle, uniformMapProperties.tileRectangle); @@ -2078,7 +2122,7 @@ define([ command.receiveShadows = receiveShadows; command.renderState = renderState; command.primitiveType = PrimitiveType.TRIANGLES; - command.vertexArray = surfaceTile.vertexArray; + command.vertexArray = surfaceTile.vertexArray || surfaceTile.fillVertexArray; command.uniformMap = uniformMap; command.pass = Pass.GLOBE; @@ -2099,10 +2143,10 @@ define([ Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); if (frameState.mode === SceneMode.MORPHING) { - boundingVolume = BoundingSphere.union(surfaceTile.mesh.boundingSphere3D, boundingVolume, boundingVolume); + boundingVolume = BoundingSphere.union(mesh.boundingSphere3D, boundingVolume, boundingVolume); } } else { - command.boundingVolume = BoundingSphere.clone(surfaceTile.mesh.boundingSphere3D, boundingVolume); + command.boundingVolume = BoundingSphere.clone(mesh.boundingSphere3D, boundingVolume); command.orientedBoundingBox = OrientedBoundingBox.clone(surfaceTile.orientedBoundingBox, orientedBoundingBox); } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 8406577bc1f..2997f2358be 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -17,7 +17,8 @@ define([ './QuadtreeTile', './QuadtreeTileLoadState', './SceneMode', - './TileReplacementQueue' + './TileReplacementQueue', + './TileSelectionResult' ], function( Cartesian3, Cartographic, @@ -37,7 +38,8 @@ define([ QuadtreeTile, QuadtreeTileLoadState, SceneMode, - TileReplacementQueue) { + TileReplacementQueue, + TileSelectionResult) { 'use strict'; /** @@ -718,6 +720,9 @@ define([ } addTileToRenderList(primitive, tile, nearestRenderableTile); + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.RENDERED; + traversalDetails.anyAreRenderable = tile.renderable; traversalDetails.allAreRenderable = tile.renderable; traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; @@ -725,7 +730,7 @@ define([ return; } - // 4. Otherwise, we can't render this tile (or its fill) because it would cause detail to disappear + // 4. Otherwise, we can't render this tile (or its fill) because doing so would cause detail to disappear // that was visible last frame. Instead, keep rendering any still-visible descendants that were rendered // last frame and render fills for newly-visible descendants. E.g. if we were rendering level 15 last // frame but this frame we want level 14 and the closest renderable level <= 14 is 0, rendering level @@ -747,6 +752,10 @@ define([ if (allAreUpsampled) { reportTileAction(frameState, tile, 'all upsampled'); + + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.RENDERED; + // No point in rendering the children because they're all upsampled. Render this tile instead. addTileToRenderList(primitive, tile, nearestRenderableTile); @@ -769,6 +778,9 @@ define([ // SSE is not good enough, so refine. reportTileAction(frameState, tile, 'refine'); + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.REFINED; + var firstRenderedDescendantIndex = primitive._tilesToRender.length; var loadIndexLow = primitive._tileLoadQueueLow.length; var loadIndexMedium = primitive._tileLoadQueueMedium.length; @@ -789,10 +801,22 @@ define([ if (!allAreRenderable && !anyWereRenderedLastFrame) { // Some of our descendants aren't ready to render yet, and none were rendered last frame, // so kick them all out of the render list and render this tile instead. Continue to load them though! + + var renderList = primitive._tilesToRender; + for (var i = firstRenderedDescendantIndex; i < renderList.length; ++i) { + var workTile = renderList[i]; + while (workTile !== undefined && workTile._lastSelectionResult !== TileSelectionResult.KICKED && workTile !== tile) { + workTile._lastSelectionResult = TileSelectionResult.KICKED; + workTile = workTile.parent; + } + } + primitive._tilesToRender.length = firstRenderedDescendantIndex; primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; addTileToRenderList(primitive, tile, nearestRenderableTile); + tile._lastSelectionResult = TileSelectionResult.RENDERED; + // EXCEPT if we're waiting heaps of descendants, the above will take too long. So in that case, // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting // on this tile. @@ -822,6 +846,9 @@ define([ reportTileAction(frameState, tile, 'can\'t refine'); + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.RENDERED; + // We'd like to refine but can't because we have no availability data for this tile's children, // so we have no idea if refinining would involve a load or an upsample. We'll have to finish // loading this tile first in order to find that out, so load this refinement blocker with @@ -885,6 +912,8 @@ define([ } reportTileAction(frameState, tile, 'culled'); + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.CULLED; ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 9f642630359..222bfd03481 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -3,13 +3,15 @@ define([ '../Core/defineProperties', '../Core/DeveloperError', '../Core/Rectangle', - './QuadtreeTileLoadState' + './QuadtreeTileLoadState', + './TileSelectionResult' ], function( defined, defineProperties, DeveloperError, Rectangle, - QuadtreeTileLoadState) { + QuadtreeTileLoadState, + TileSelectionResult) { 'use strict'; /** @@ -72,6 +74,8 @@ define([ this._frameUpdated = undefined; this._frameRendered = undefined; this._frameVisited = undefined; + this._lastSelectionResult = TileSelectionResult.CULLED; + this._lastSelectionResultFrame = undefined; this._loadedCallbacks = {}; /** diff --git a/Source/Scene/TileSelectionResult.js b/Source/Scene/TileSelectionResult.js new file mode 100644 index 00000000000..1caf42a68dd --- /dev/null +++ b/Source/Scene/TileSelectionResult.js @@ -0,0 +1,33 @@ +define([ + ], function() { + 'use strict'; + + /** + * Indicates what happened the last time this tile was visited for selection. + * @private + */ + var TileSelectionResult = { + /** + * This tile was deemed not visible and culled. + */ + CULLED: 0, + + /** + * The tile was selected for rendering. + */ + RENDERED: 1, + + /** + * This tile did not meet the required screen-space error and was refined. + */ + REFINED: 2, + + /** + * This tile was originally refined or rendered, but it or its descendants got kicked out of the render list + * in favor of an ancestor because it is not yet renderable. + */ + KICKED: 3 + }; + + return TileSelectionResult; +}); diff --git a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js index a266481023e..fa863943249 100644 --- a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js +++ b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js @@ -136,16 +136,16 @@ define([ Cartesian3.maximumByComponent(cartesian3Scratch, maximum, maximum); } - var westIndicesSouthToNorth = parameters.westIndices.sort(function(a, b) { + var westIndicesSouthToNorth = parameters.westIndices.slice().sort(function(a, b) { return uvs[a].y - uvs[b].y; }); - var eastIndicesNorthToSouth = parameters.eastIndices.sort(function(a, b) { + var eastIndicesNorthToSouth = parameters.eastIndices.slice().sort(function(a, b) { return uvs[b].y - uvs[a].y; }); - var southIndicesEastToWest = parameters.southIndices.sort(function(a, b) { + var southIndicesEastToWest = parameters.southIndices.slice().sort(function(a, b) { return uvs[b].x - uvs[a].x; }); - var northIndicesWestToEast = parameters.northIndices.sort(function(a, b) { + var northIndicesWestToEast = parameters.northIndices.slice().sort(function(a, b) { return uvs[a].x - uvs[b].x; }); From 237f8deccf89a298c08237344996625fdad7d097 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 8 Aug 2018 22:01:17 +1000 Subject: [PATCH 033/131] Fix a bug that made fill tiles flicker out after partial load. --- Source/Scene/GlobeSurfaceShaderSet.js | 2 +- Source/Scene/GlobeSurfaceTileProvider.js | 5 +++++ Source/Scene/QuadtreePrimitive.js | 2 +- Source/Shaders/GlobeFS.glsl | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index b4a0c20abe1..88e35e780ca 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -70,7 +70,7 @@ define([ var quantization = 0; var quantizationDefine = ''; - var mesh = surfaceTile.mesh || surfaceTile.fillMesh; + var mesh = surfaceTile.vertexArray !== undefined ? surfaceTile.mesh : surfaceTile.fillMesh; var terrainEncoding = mesh.encoding; var quantizationMode = terrainEncoding.quantization; if (quantizationMode === TerrainQuantization.BITS12) { diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index eadf2cee5ac..d56404c9f1d 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1107,6 +1107,9 @@ define([ u_initialColor : function() { return this.properties.initialColor; }, + u_isFill : function() { + return this.properties.isFill; + }, u_zoomedOutOceanSpecularIntensity : function() { return this.properties.zoomedOutOceanSpecularIntensity; }, @@ -1218,6 +1221,7 @@ define([ // derived commands that combine another uniform map with this one. properties : { initialColor : new Cartesian4(0.0, 0.0, 0.5, 1.0), + isFill : false, zoomedOutOceanSpecularIntensity : 0.5, oceanNormalMap : undefined, lightingFadeDistance : new Cartesian2(6500000.0, 9000000.0), @@ -1997,6 +2001,7 @@ define([ var uniformMapProperties = uniformMap.properties; Cartesian4.clone(initialColor, uniformMapProperties.initialColor); + uniformMapProperties.isFill = surfaceTile.vertexArray === undefined; uniformMapProperties.oceanNormalMap = oceanNormalMap; uniformMapProperties.lightingFadeDistance.x = tileProvider.lightingFadeOutDistance; uniformMapProperties.lightingFadeDistance.y = tileProvider.lightingFadeInDistance; diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 2997f2358be..005eeead955 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -817,7 +817,7 @@ define([ tile._lastSelectionResult = TileSelectionResult.RENDERED; - // EXCEPT if we're waiting heaps of descendants, the above will take too long. So in that case, + // EXCEPT if we're waiting on heaps of descendants, the above will take too long. So in that case, // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting // on this tile. var wasRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index b11715a13e9..6b9190d103c 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -1,5 +1,6 @@ //#define SHOW_TILE_BOUNDARIES uniform vec4 u_initialColor; +uniform bool u_isFill; #if TEXTURE_UNITS > 0 uniform sampler2D u_dayTextures[TEXTURE_UNITS]; @@ -249,6 +250,11 @@ void main() } #endif + if (u_isFill) + { + finalColor = vec4(mix(vec3(1.0, 0.0, 0.0), finalColor.rgb, 0.25), finalColor.a); + } + #ifdef FOG const float fExposure = 2.0; vec3 fogColor = v_mieColor + finalColor.rgb * v_rayleighColor; From 0328cc917dfd001f6a8ac0cb27e265635263d47f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 5 Sep 2018 14:09:32 +1000 Subject: [PATCH 034/131] Don't regenerate fill tiles every frame. --- Source/Core/TileEdge.js | 6 +- Source/Scene/GlobeSurfaceShaderSet.js | 4 +- Source/Scene/GlobeSurfaceTile.js | 12 +- Source/Scene/GlobeSurfaceTileProvider.js | 63 +- Source/Scene/Imagery.js | 4 +- Source/Scene/QuadtreePrimitive.js | 16 +- Source/Scene/TerrainFillMesh.js | 797 +++++++++++++++++++++++ Source/Scene/TileImagery.js | 9 +- Source/Shaders/GlobeFS.glsl | 2 +- 9 files changed, 881 insertions(+), 32 deletions(-) create mode 100644 Source/Scene/TerrainFillMesh.js diff --git a/Source/Core/TileEdge.js b/Source/Core/TileEdge.js index 9fd47013365..979d1f7ec13 100644 --- a/Source/Core/TileEdge.js +++ b/Source/Core/TileEdge.js @@ -6,7 +6,11 @@ define([ WEST: 0, NORTH: 1, EAST: 2, - SOUTH: 3 + SOUTH: 3, + NORTHWEST: 4, + NORTHEAST: 5, + SOUTHWEST: 6, + SOUTHEAST: 7 }; return TileEdge; diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 88e35e780ca..d897d4b6f62 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -70,7 +70,7 @@ define([ var quantization = 0; var quantizationDefine = ''; - var mesh = surfaceTile.vertexArray !== undefined ? surfaceTile.mesh : surfaceTile.fillMesh; + var mesh = surfaceTile.vertexArray !== undefined ? surfaceTile.mesh : surfaceTile.fill.mesh; var terrainEncoding = mesh.encoding; var quantizationMode = terrainEncoding.quantization; if (quantizationMode === TerrainQuantization.BITS12) { @@ -80,7 +80,7 @@ define([ var vertexLogDepth = 0; var vertexLogDepthDefine = ''; - if (surfaceTile.terrainData !== undefined && surfaceTile.terrainData._createdByUpsampling) { + if (surfaceTile.terrainData === undefined || surfaceTile.terrainData._createdByUpsampling) { vertexLogDepth = 1; vertexLogDepthDefine = 'DISABLE_GL_POSITION_LOG_DEPTH'; } diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index d7e5efa2330..f4059ec25f3 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -98,9 +98,8 @@ define([ this.terrainState = TerrainState.UNLOADED; this.mesh = undefined; - this.fillMesh = undefined; + this.fill = undefined; this.vertexArray = undefined; - this.fillVertexArray = undefined; // TODO: probably better to have a bounding sphere for 2D rather than one for picking. this.pickBoundingSphere = new BoundingSphere(); @@ -182,7 +181,7 @@ define([ var scratchResult = new Cartesian3(); GlobeSurfaceTile.prototype.pick = function(ray, mode, projection, cullBackFaces, result) { - var mesh = this.mesh || this.fillMesh; + var mesh = this.mesh || this.fill.mesh; if (!defined(mesh)) { return undefined; } @@ -223,7 +222,7 @@ define([ this.terrainState = TerrainState.UNLOADED; this.mesh = undefined; - this.fillMesh = undefined; + this.fill = this.fill && this.fill.destroy(); var imageryList = this.imagery; for (var i = 0, len = imageryList.length; i < len; ++i) { @@ -250,11 +249,6 @@ define([ } } - if (defined(this.fillVertexArray)) { - this.fillVertexArray.destroy(); - this.fillVertexArray = undefined; - } - if (defined(this.wireframeVertexArray)) { indexBuffer = this.wireframeVertexArray.indexBuffer; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index d56404c9f1d..343acae5924 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -53,7 +53,8 @@ define([ './ImageryLayer', './QuadtreeTileLoadState', './SceneMode', - './ShadowMode' + './ShadowMode', + './TerrainFillMesh' ], function( AttributeCompression, BoundingSphere, @@ -109,7 +110,8 @@ define([ ImageryLayer, QuadtreeTileLoadState, SceneMode, - ShadowMode) { + ShadowMode, + TerrainFillMesh) { 'use strict'; /** @@ -224,6 +226,9 @@ define([ * @private */ this._clippingPlanes = undefined; + + this._hasLoadedTilesThisFrame = false; + this._hasFillTilesThisFrame = false; } defineProperties(GlobeSurfaceTileProvider.prototype, { @@ -468,6 +473,9 @@ define([ clippingPlanes.update(frameState); } this._usedDrawCommands = 0; + + this._hasLoadedTilesThisFrame = false; + this._hasFillTilesThisFrame = false; }; /** @@ -500,6 +508,12 @@ define([ }); } + // If this frame has a mix of loaded and fill tiles, we need to propagate + // loaded heights to the fill tiles. + if (this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) { + TerrainFillMesh.updateFillTiles(this, this._quadtree._tilesToRender, frameState); + } + // Add the tile render commands to the command list, sorted by texture count. var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount; for (var textureCountIndex = 0, textureCountLength = tilesToRenderByTextureCount.length; textureCountIndex < textureCountLength; ++textureCountIndex) { @@ -723,6 +737,8 @@ define([ var surfaceTile = tile.data; if (nearestRenderableTile !== undefined && nearestRenderableTile !== tile) { + this._hasFillTilesThisFrame = true; + surfaceTile.renderableTile = nearestRenderableTile; // The renderable tile may have previously deferred to an ancestor. @@ -738,6 +754,7 @@ define([ ancestorSubset.z = (myRectangle.east - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); ancestorSubset.w = (myRectangle.north - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); } else { + this._hasLoadedTilesThisFrame = true; surfaceTile.renderableTile = undefined; } @@ -1270,9 +1287,9 @@ define([ if (surfaceTile.vertexArray !== undefined) { mesh = surfaceTile.mesh; vertexArray = surfaceTile.vertexArray; - } else if (surfaceTile.fillVertexArray !== undefined) { - mesh = surfaceTile.fillMesh; - vertexArray = surfaceTile.fillVertexArray; + } else if (surfaceTile.fill !== undefined && surfaceTile.fill.vertexArray !== undefined) { + mesh = surfaceTile.fill.mesh; + vertexArray = surfaceTile.fill.vertexArray; } if (!defined(mesh) || !defined(vertexArray)) { @@ -1843,7 +1860,37 @@ define([ addDrawCommandsForTile(tileProvider, surfaceTile.renderableTile, frameState, surfaceTile.renderableTileSubset); return; } else if (missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE) { - createFillTile(tileProvider, tile, frameState); + // if (surfaceTile.fillMesh === undefined) { + // if (tile.renderable) { + // console.log(`Frame ${frameState.frameNumber} L${tile.level}X${tile.x}Y${tile.y} renderable`); + // } else { + // console.log(`Frame ${frameState.frameNumber} L${tile.level}X${tile.x}Y${tile.y} no mesh`); + // } + + // for (var i = 0; i < tileProvider._quadtree._tilesToRender.length; ++i) { + // var foo = tileProvider._quadtree._tilesToRender[i]; + // if (foo.data && foo.data.fillMesh) { + // foo.data.fillMesh.visitedFrame = undefined; + // } + // } + + // TerrainFillMesh.updateFillTiles(tileProvider, tileProvider._quadtree._tilesToRender, frameState); + // } else if (surfaceTile.fillMesh.changedThisFrame) { + // console.log(`Frame ${frameState.frameNumber} L${tile.level}X${tile.x}Y${tile.y} changed`); + // } + if (surfaceTile.fill == undefined) { + // No fill was created for this tile, probably because this tile is not connected to + // any renderable tiles. So create a simple tile in the middle of the tile's possible + // height range. + surfaceTile.fill = new TerrainFillMesh(); + surfaceTile.fill.tile = tile; + return; // TODO + } + surfaceTile.fill.update(tileProvider, frameState); + //return; + //createFillTile(tileProvider, tile, frameState); + } else { + return; } } @@ -1882,7 +1929,7 @@ define([ --maxTextures; } - var mesh = surfaceTile.vertexArray ? surfaceTile.mesh : surfaceTile.fillMesh; + var mesh = surfaceTile.vertexArray ? surfaceTile.mesh : surfaceTile.fill.mesh; var rtc = mesh.center; var encoding = mesh.encoding; @@ -2127,7 +2174,7 @@ define([ command.receiveShadows = receiveShadows; command.renderState = renderState; command.primitiveType = PrimitiveType.TRIANGLES; - command.vertexArray = surfaceTile.vertexArray || surfaceTile.fillVertexArray; + command.vertexArray = surfaceTile.vertexArray || surfaceTile.fill.vertexArray; command.uniformMap = uniformMap; command.pass = Pass.GLOBE; diff --git a/Source/Scene/Imagery.js b/Source/Scene/Imagery.js index 40520117dfe..75c9c216758 100644 --- a/Source/Scene/Imagery.js +++ b/Source/Scene/Imagery.js @@ -84,8 +84,8 @@ define([ return this.referenceCount; }; - Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection) { - if (this.state === ImageryState.UNLOADED) { + Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, skipLoading) { + if (this.state === ImageryState.UNLOADED && !skipLoading) { this.state = ImageryState.TRANSITIONING; this.imageryLayer._requestImagery(this); } diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 005eeead955..a5a5833f753 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -149,12 +149,18 @@ define([ /** * Gets or sets a value indicating whether the ancestors of rendered tiles should be preloaded. * Setting this to true optimizes the zoom-out experience and provides more detail in - * newly-exposed areas when panning. The down side is that is requires loading more tiles. + * newly-exposed areas when panning. The down side is that it requires loading more tiles. * @type {Boolean} * @default false */ this.preloadAncestors = false; + /** + * Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded. + * Setting this to true + */ + this.preloadSiblings = false; + this._occluders = new QuadtreeOccluders({ ellipsoid : ellipsoid }); @@ -819,14 +825,14 @@ define([ // EXCEPT if we're waiting on heaps of descendants, the above will take too long. So in that case, // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting - // on this tile. + // on this tile. Keep doing this until we actually manage to render this tile. var wasRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; if (!wasRenderedLastFrame && notYetRenderableCount > primitive.loadingDescendantLimit) { primitive._tileLoadQueueLow.length = loadIndexLow; primitive._tileLoadQueueMedium.length = loadIndexMedium; primitive._tileLoadQueueHigh.length = loadIndexHigh; queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); - traversalDetails.notYetRenderableCount = 1; + traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; } traversalDetails.anyAreRenderable = tile.renderable; @@ -921,6 +927,10 @@ define([ traversalDetails.allAreRenderable = true; traversalDetails.anyWereRenderedLastFrame = false; traversalDetails.notYetRenderableCount = 0; + + if (primitive.preloadSiblings) { + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + } } function screenSpaceError(primitive, frameState, tile) { diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js new file mode 100644 index 00000000000..d0b9870e79b --- /dev/null +++ b/Source/Scene/TerrainFillMesh.js @@ -0,0 +1,797 @@ +define([ + '../Core/AttributeCompression', + '../Core/BoundingSphere', + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Cartographic', + '../Core/defined', + '../Core/Math', + '../Core/DeveloperError', + '../Core/IndexDatatype', + '../Core/OrientedBoundingBox', + '../Core/Queue', + '../Core/TileEdge', + '../Core/TerrainEncoding', + '../Core/TerrainMesh', + '../Core/TerrainTileEdgeDetails', + '../Core/WebMercatorProjection', + '../Renderer/Buffer', + '../Renderer/BufferUsage', + '../Renderer/VertexArray', + './ImageryState', + './TileSelectionResult' + ], function( + AttributeCompression, + BoundingSphere, + Cartesian2, + Cartesian3, + Cartographic, + defined, + CesiumMath, + DeveloperError, + IndexDatatype, + OrientedBoundingBox, + Queue, + TileEdge, + TerrainEncoding, + TerrainMesh, + TerrainTileEdgeDetails, + WebMercatorProjection, + Buffer, + BufferUsage, + VertexArray, + ImageryState, + TileSelectionResult) { + 'use strict'; + + function TerrainFillMesh() { + this.tile = undefined; + this.frameLastUpdated = undefined; + this.westMeshes = []; + this.westTiles = []; + this.southMeshes = []; + this.southTiles = []; + this.eastMeshes = []; + this.eastTiles = []; + this.northMeshes = []; + this.northTiles = []; + this.southwestMesh = undefined; + this.southwestTile = undefined; + this.southeastMesh = undefined; + this.southeastTile = undefined; + this.northwestMesh = undefined; + this.northwestTile = undefined; + this.northeastMesh = undefined; + this.northeastTile = undefined; + this.changedThisFrame = false; + this.visitedFrame = undefined; + this.mesh = undefined; + this.vertexArray = undefined; + } + + TerrainFillMesh.prototype.update = function(tileProvider, frameState) { + if (this.changedThisFrame) { + createFillMesh(tileProvider, frameState, this.tile); + this.changedThisFrame = false; + } + }; + + TerrainFillMesh.prototype.destroy = function() { + this.vertexArray = this.vertexArray && this.vertexArray.destroy(); + }; + + TerrainFillMesh.updateFillTiles = function(tileProvider, renderedTiles, frameState) { + // We want our fill tiles to look natural, which means they should align perfectly with + // adjacent loaded tiles, and their edges that are not adjacent to loaded tiles should have + // sensible heights (e.g. the average of the heights of loaded edges). Some fill tiles may + // be adjacent only to other fill tiles, and in that case heights should be assigned fanning + // outward from the loaded tiles so that there are no sudden changes in height. + + // We do this with a breadth-first traversal of the rendered tiles, starting with the loaded + // ones. Graph nodes are tiles and graph edges connect to other rendered tiles that are spatially adjacent + // to those tiles. As we visit each node, we propagate tile edges to adjacent tiles. If there's no data + // for a tile edge, we create an edge with an average height and then propagate it. If an edge is partially defined + // (e.g. an edge is adjacent to multiple more-detailed tiles and only some of them are loaded), we + // fill in the rest of the edge with the same height. + var quadtree = tileProvider._quadtree; + var levelZeroTiles = quadtree._levelZeroTiles; + var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber; + + var traversalQueue = new Queue(); + + for (var i = 0; i < renderedTiles.length; ++i) { + var renderedTile = renderedTiles[i]; + if (renderedTile.renderable) { + traversalQueue.enqueue(renderedTiles[i]); + } + } + + var tile = traversalQueue.dequeue(); + + while (tile !== undefined) { + var tileToWest = tile.findTileToWest(levelZeroTiles); + var tileToSouth = tile.findTileToSouth(levelZeroTiles) + var tileToEast = tile.findTileToEast(levelZeroTiles); + var tileToNorth = tile.findTileToNorth(levelZeroTiles); + visitRenderedTiles(tileProvider, frameState, tile, tileToWest, lastSelectionFrameNumber, TileEdge.EAST, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToSouth, lastSelectionFrameNumber, TileEdge.NORTH, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToEast, lastSelectionFrameNumber, TileEdge.WEST, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToNorth, lastSelectionFrameNumber, TileEdge.SOUTH, false, traversalQueue); + + var tileToNorthwest = tileToWest.findTileToNorth(levelZeroTiles); + var tileToSouthwest = tileToWest.findTileToSouth(levelZeroTiles); + var tileToNortheast = tileToEast.findTileToNorth(levelZeroTiles); + var tileToSoutheast = tileToEast.findTileToSouth(levelZeroTiles); + visitRenderedTiles(tileProvider, frameState, tile, tileToNorthwest, lastSelectionFrameNumber, TileEdge.SOUTHEAST, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToNortheast, lastSelectionFrameNumber, TileEdge.SOUTHWEST, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToSouthwest, lastSelectionFrameNumber, TileEdge.NORTHEAST, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToSoutheast, lastSelectionFrameNumber, TileEdge.NORTHWEST, false, traversalQueue); + + tile = traversalQueue.dequeue(); + } + }; + + function visitRenderedTiles(tileProvider, frameState, sourceTile, startTile, currentFrameNumber, tileEdge, downOnly, traversalQueue) { + if (startTile === undefined) { + // There are no tiles North or South of the poles. + return; + } + + var tile = startTile; + while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || tile._lastSelectionResult === TileSelectionResult.KICKED)) { + // This wasn't visited or it was visited and then kicked, so walk up to find the closest ancestor that was rendered. + if (downOnly) { + return; + } + + var parent = tile.parent; + if (tileEdge >= TileEdge.NORTHWEST && parent !== undefined) { + // When we're looking for a corner, verify that the parent tile is still relevant. + // That is, the parent and child must share the corner in question. + switch (tileEdge) { + case TileEdge.NORTHWEST: + tile = tile === parent.northwestChild ? parent : undefined; + break; + case TileEdge.NORTHEAST: + tile = tile === parent.northeastChild ? parent : undefined; + break; + case TileEdge.SOUTHWEST: + tile = tile === parent.southwestChild ? parent : undefined; + break; + case TileEdge.SOUTHEAST: + tile = tile === parent.southeastChild ? parent : undefined; + break; + } + } else { + tile = parent; + } + } + + if (tile === undefined) { + return; + } + + if (tile._lastSelectionResult === TileSelectionResult.RENDERED) { + visitTile(tileProvider, frameState, sourceTile, tile, tileEdge, currentFrameNumber, traversalQueue); + return; + } + + if (startTile._lastSelectionResult === TileSelectionResult.CULLED) { + return; + } + + // This tile was refined, so find rendered children, if any. + // Return the tiles in clockwise order. + switch (tileEdge) { + case TileEdge.WEST: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + case TileEdge.EAST: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + case TileEdge.SOUTH: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + case TileEdge.NORTH: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + case TileEdge.NORTHWEST: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + case TileEdge.NORTHEAST: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + case TileEdge.SOUTHWEST: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + case TileEdge.SOUTHEAST: + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + break; + default: + throw new DeveloperError('Invalid edge'); + } + } + + function visitTile(tileProvider, frameState, sourceTile, destinationTile, tileEdge, frameNumber, traversalQueue) { + if (destinationTile.renderable) { + // No further processing necessary for renderable tiles. + return; + } + + var destinationSurfaceTile = destinationTile.data; + + if (destinationSurfaceTile.fill === undefined) { + destinationSurfaceTile.fill = new TerrainFillMesh(); + destinationSurfaceTile.fill.tile = destinationTile; + } + + if (destinationSurfaceTile.fill.visitedFrame !== frameNumber) { + // First time visiting this tile this frame, add it to the traversal queue. + destinationSurfaceTile.fill.visitedFrame = frameNumber; + destinationSurfaceTile.fill.changedThisFrame = false; + traversalQueue.enqueue(destinationTile); + } + + propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge); + } + + function propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge) { + var destinationFill = destinationTile.data.fill; + var sourceMesh = sourceTile.data.mesh; + + if (sourceMesh === undefined) { + var sourceFill = sourceTile.data.fill; + if (sourceFill.changedThisFrame) { + createFillMesh(tileProvider, frameState, sourceTile); + sourceTile.data.fill.changedThisFrame = false; + sourceMesh = sourceTile.data.fill.mesh; + } + } + + var edgeMeshes; + var edgeTiles; + + switch (tileEdge) { + case TileEdge.WEST: + edgeMeshes = destinationFill.westMeshes; + edgeTiles = destinationFill.westTiles; + break; + case TileEdge.SOUTH: + edgeMeshes = destinationFill.southMeshes; + edgeTiles = destinationFill.southTiles; + break; + case TileEdge.EAST: + edgeMeshes = destinationFill.eastMeshes; + edgeTiles = destinationFill.eastTiles; + break; + case TileEdge.NORTH: + edgeMeshes = destinationFill.northMeshes; + edgeTiles = destinationFill.northTiles; + break; + // Corners are simpler. + case TileEdge.NORTHWEST: + // if (destinationFill.northwestTile !== sourceTile) { + // const from = destinationFill.northwestTile || {}; + // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because northwest tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); + // } + destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.northwestMesh !== sourceMesh; + destinationFill.northwestMesh = sourceMesh; + destinationFill.northwestTile = sourceTile; + return; + case TileEdge.NORTHEAST: + // if (destinationFill.northeastTile !== sourceTile) { + // const from = destinationFill.northeastTile || {}; + // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because northeast tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); + // } + destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.northeastMesh !== sourceMesh; + destinationFill.northeastMesh = sourceMesh; + destinationFill.northeastTile = sourceTile; + return; + case TileEdge.SOUTHWEST: + // if (destinationFill.southwestTile !== sourceTile) { + // const from = destinationFill.southwestTile || {}; + // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because southwest tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); + // } + destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.southwestMesh !== sourceMesh; + destinationFill.southwestMesh = sourceMesh; + destinationFill.southwestTile = sourceTile; + return; + case TileEdge.SOUTHEAST: + // if (destinationFill.southeastTile !== sourceTile) { + // const from = destinationFill.southeastTile || {}; + // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because southeast tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); + // } + destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.southeastMesh !== sourceMesh; + destinationFill.southeastMesh = sourceMesh; + destinationFill.southeastTile = sourceTile; + return; + } + + if (sourceTile.level <= destinationTile.level) { + // Source edge completely spans the destination edge. + // if (edgeTiles[0] !== sourceTile) { + // const from = edgeTiles[0] || {}; + // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because edge tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); + // } + destinationFill.changedThisFrame = destinationFill.changedThisFrame || edgeMeshes[0] !== sourceMesh || edgeMeshes.length !== 1; + edgeMeshes[0] = sourceMesh; + edgeTiles[0] = sourceTile; + edgeMeshes.length = 1; + edgeTiles.length = 1; + return; + } + + // Source edge is a subset of the destination edge. + // Figure out the range of meshes we're replacing. + var startIndex, endIndex, existingTile, existingRectangle; + var sourceRectangle = sourceTile.rectangle; + + switch (tileEdge) { + case TileEdge.WEST: + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { + existingTile = edgeTiles[startIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.north <= sourceRectangle.north) { + break; + } + } + for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { + existingTile = edgeTiles[endIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.south < sourceRectangle.south) { + break; + } + } + break; + case TileEdge.SOUTH: + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { + existingTile = edgeTiles[startIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.west >= sourceRectangle.west) { + break; + } + } + for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { + existingTile = edgeTiles[endIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.east > sourceRectangle.east) { + break; + } + } + break; + case TileEdge.EAST: + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { + existingTile = edgeTiles[startIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.south >= sourceRectangle.south) { + break; + } + } + for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { + existingTile = edgeTiles[endIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.north > sourceRectangle.north) { + break; + } + } + break; + case TileEdge.NORTH: + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { + existingTile = edgeTiles[startIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.east <= sourceRectangle.east) { + break; + } + } + for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { + existingTile = edgeTiles[endIndex]; + existingRectangle = existingTile.rectangle; + if (existingRectangle.west < sourceRectangle.west) { + break; + } + } + break; + } + + if (endIndex - startIndex === 1) { + // if (edgeTiles[startIndex] !== sourceTile) { + // const from = edgeTiles[startIndex] || {}; + // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because edge tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); + // } + destinationFill.changedThisFrame = destinationFill.changedThisFrame || edgeMeshes[startIndex] !== sourceMesh; + edgeMeshes[startIndex] = sourceMesh; + edgeTiles[startIndex] = sourceTile; + } else { + // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because number of tiles on edge changed`); + destinationFill.changedThisFrame = true; + edgeMeshes.splice(startIndex, endIndex - startIndex, sourceMesh); + edgeTiles.splice(startIndex, endIndex - startIndex, sourceTile); + } + } + + var westScratch = new TerrainTileEdgeDetails(); + var southScratch = new TerrainTileEdgeDetails(); + var eastScratch = new TerrainTileEdgeDetails(); + var northScratch = new TerrainTileEdgeDetails(); + var tileVerticesScratch = []; + var cartographicScratch = new Cartographic(); + var cartesianScratch = new Cartesian3(); + var normalScratch = new Cartesian3(); + var octEncodedNormalScratch = new Cartesian2(); + + function createFillMesh(tileProvider, frameState, tile) { + var surfaceTile = tile.data; + var fill = surfaceTile.fill; + + var quadtree = tileProvider._quadtree; + var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber; + + var west = getEdgeVertices(tile, fill.westTiles, fill.westMeshes, lastSelectionFrameNumber, TileEdge.EAST, westScratch); + var south = getEdgeVertices(tile, fill.southTiles, fill.southMeshes, lastSelectionFrameNumber, TileEdge.NORTH, southScratch); + var east = getEdgeVertices(tile, fill.eastTiles, fill.eastMeshes, lastSelectionFrameNumber, TileEdge.WEST, eastScratch); + var north = getEdgeVertices(tile, fill.northTiles, fill.northMeshes, lastSelectionFrameNumber, TileEdge.SOUTH, northScratch); + + var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; + var hasWebMercatorT = true; // TODO + var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); + + var minimumHeight = Number.MAX_VALUE; + var maximumHeight = -Number.MAX_VALUE; + var hasAnyVertices = false; + + if (west.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, west.minimumHeight); + maximumHeight = Math.max(maximumHeight, west.maximumHeight); + hasAnyVertices = true; + } + + if (south.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, south.minimumHeight); + maximumHeight = Math.max(maximumHeight, south.maximumHeight); + hasAnyVertices = true; + } + + if (east.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, east.minimumHeight); + maximumHeight = Math.max(maximumHeight, east.maximumHeight); + hasAnyVertices = true; + } + + if (north.vertices.length > 0) { + minimumHeight = Math.min(minimumHeight, north.minimumHeight); + maximumHeight = Math.max(maximumHeight, north.maximumHeight); + hasAnyVertices = true; + } + + if (!hasAnyVertices) { + var tileBoundingRegion = surfaceTile.tileBoundingRegion; + minimumHeight = tileBoundingRegion.minimumHeight; + maximumHeight = tileBoundingRegion.maximumHeight; + } + + var middleHeight = (minimumHeight + maximumHeight) * 0.5; + + var tileVertices = tileVerticesScratch; + tileVertices.length = 0; + + var ellipsoid = tile.tilingScheme.ellipsoid; + var rectangle = tile.rectangle; + + var northwestIndex = 0; + addCornerVertexIfNecessary(ellipsoid, 0.0, 1.0, rectangle.west, rectangle.north, middleHeight, west, north, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(west, stride, tileVertices); + var southwestIndex = tileVertices.length / stride; + addCornerVertexIfNecessary(ellipsoid, 0.0, 0.0, rectangle.west, rectangle.south, middleHeight, south, west, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(south, stride, tileVertices); + var southeastIndex = tileVertices.length / stride; + addCornerVertexIfNecessary(ellipsoid, 1.0, 0.0, rectangle.east, rectangle.south, middleHeight, east, south, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(east, stride, tileVertices); + var northeastIndex = tileVertices.length / stride; + addCornerVertexIfNecessary(ellipsoid, 1.0, 1.0, rectangle.east, rectangle.north, middleHeight, north, east, hasVertexNormals, hasWebMercatorT, tileVertices); + addVerticesToFillTile(north, stride, tileVertices); + + // Add a single vertex at the center of the tile. + var obb = OrientedBoundingBox.fromRectangle(tile.rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); + var center = obb.center; + + ellipsoid.cartesianToCartographic(center, cartographicScratch); + cartographicScratch.height = middleHeight; + var centerVertexPosition = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + + tileVertices.push(centerVertexPosition.x, centerVertexPosition.y, centerVertexPosition.z, middleHeight); + tileVertices.push((cartographicScratch.longitude - rectangle.west) / (rectangle.east - rectangle.west)); + tileVertices.push((cartographicScratch.latitude - rectangle.south) / (rectangle.north - rectangle.south)); + + if (hasWebMercatorT) { + var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); + var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); + tileVertices.push((WebMercatorProjection.geodeticLatitudeToMercatorAngle(cartographicScratch.latitude) - southMercatorY) * oneOverMercatorHeight); + } + + if (hasVertexNormals) { + ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); + AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); + tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); + } + + var vertexCount = tileVertices.length / stride; + var indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex + var centerIndex = vertexCount - 1; + + var indexOut = 0; + var i; + for (i = 0; i < vertexCount - 2; ++i) { + indices[indexOut++] = centerIndex; + indices[indexOut++] = i; + indices[indexOut++] = i + 1; + } + + indices[indexOut++] = centerIndex; + indices[indexOut++] = i; + indices[indexOut++] = 0; + + var westIndicesSouthToNorth = []; + for (i = southwestIndex; i >= northwestIndex; --i) { + westIndicesSouthToNorth.push(i); + } + + var southIndicesEastToWest = []; + for (i = southeastIndex; i >= southwestIndex; --i) { + southIndicesEastToWest.push(i); + } + + var eastIndicesNorthToSouth = []; + for (i = northeastIndex; i >= southeastIndex; --i) { + eastIndicesNorthToSouth.push(i); + } + + var northIndicesWestToEast = []; + northIndicesWestToEast.push(0); + for (i = centerIndex - 1; i >= northeastIndex; --i) { + northIndicesWestToEast.push(i); + } + + var packedStride = hasVertexNormals ? stride - 1 : stride; // normal is packed into 1 float + var typedArray = new Float32Array(vertexCount * packedStride); + + for (i = 0; i < vertexCount; ++i) { + var read = i * stride; + var write = i * packedStride; + typedArray[write++] = tileVertices[read++] - center.x; + typedArray[write++] = tileVertices[read++] - center.y; + typedArray[write++] = tileVertices[read++] - center.z; + typedArray[write++] = tileVertices[read++]; + typedArray[write++] = tileVertices[read++]; + typedArray[write++] = tileVertices[read++]; + + if (hasWebMercatorT) { + typedArray[write++] = tileVertices[read++]; + } + + if (hasVertexNormals) { + typedArray[write++] = AttributeCompression.octPackFloat(Cartesian2.fromElements(tileVertices[read++], tileVertices[read++], octEncodedNormalScratch)); + } + } + + var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, hasVertexNormals, hasWebMercatorT); + encoding.center = center; + + var mesh = new TerrainMesh( + obb.center, + typedArray, + indices, + minimumHeight, + maximumHeight, + BoundingSphere.fromOrientedBoundingBox(obb), + computeOccludeePoint(tileProvider, center, rectangle, maximumHeight), + encoding.getStride(), + obb, + encoding, + frameState.terrainExaggeration, + westIndicesSouthToNorth, + southIndicesEastToWest, + eastIndicesNorthToSouth, + northIndicesWestToEast + ); + + fill.mesh = mesh; + + var context = frameState.context; + + if (fill.vertexArray !== undefined) { + fill.vertexArray.destroy(); + fill.vertexArray = undefined; + } + + var buffer = Buffer.createVertexBuffer({ + context : context, + typedArray : typedArray, + usage : BufferUsage.STATIC_DRAW + }); + var attributes = mesh.encoding.getAttributes(buffer); + + var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; + var indexBuffer = Buffer.createIndexBuffer({ + context : context, + typedArray : mesh.indices, + usage : BufferUsage.STATIC_DRAW, + indexDatatype : indexDatatype + }); + + fill.vertexArray = new VertexArray({ + context : context, + attributes : attributes, + indexBuffer : indexBuffer + }); + + var tileImageryCollection = surfaceTile.imagery; + + var len; + if (tileImageryCollection.length === 0) { + var imageryLayerCollection = tileProvider._imageryLayers; + var terrainProvider = tileProvider.terrainProvider; + for (i = 0, len = imageryLayerCollection.length; i < len; ++i) { + var layer = imageryLayerCollection.get(i); + if (layer.show) { + layer._createTileImagerySkeletons(tile, terrainProvider); + } + } + } + + for (i = 0, len = tileImageryCollection.length; i < len; ++i) { + var tileImagery = tileImageryCollection[i]; + if (!defined(tileImagery.loadingImagery)) { + continue; + } + + if (tileImagery.loadingImagery.state === ImageryState.PLACEHOLDER) { + var imageryLayer = tileImagery.loadingImagery.imageryLayer; + if (imageryLayer.imageryProvider.ready) { + // Remove the placeholder and add the actual skeletons (if any) + // at the same position. Then continue the loop at the same index. + tileImagery.freeResources(); + tileImageryCollection.splice(i, 1); + imageryLayer._createTileImagerySkeletons(tile, tileProvider.terrainProvider, i); + --i; + len = tileImageryCollection.length; + continue; + } + } + + tileImagery.processStateMachine(tile, frameState, true); + } + } + + function getEdgeVertices(tile, edgeTiles, edgeMeshes, currentFrameNumber, tileEdge, result) { + var ellipsoid = tile.tilingScheme.ellipsoid; + + result.clear(); + + for (var i = 0; i < edgeMeshes.length; ++i) { + var edgeTile = edgeTiles[i]; + var surfaceTile = edgeTile.data; + if (surfaceTile === undefined) { + continue; + } + + var edgeMesh = edgeMeshes[i]; + if (edgeMesh !== undefined) { + var beforeLength = result.vertices.length; + edgeMesh.getEdgeVertices(tileEdge, edgeTile.rectangle, tile.rectangle, ellipsoid, result); + var afterLength = result.vertices.length; + var numberOfVertices = afterLength - beforeLength; + if (surfaceTile.mesh === undefined && numberOfVertices > 27) { + //console.log(`${numberOfVertices} from L${edgeTile.level}X${edgeTile.x}Y${edgeTile.y}`); + } + } + } + + return result; + } + + function addCornerVertexIfNecessary(ellipsoid, u, v, longitude, latitude, height, edgeDetails, previousEdgeDetails, hasVertexNormals, hasWebMercatorT, tileVertices) { + var vertices = edgeDetails.vertices; + + if (u === vertices[4] && v === vertices[5]) { + // First vertex is a corner vertex, as expected. + return; + } + + // Can we use the last vertex of the previous edge as the corner vertex? + var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); + var previousVertices = previousEdgeDetails.vertices; + var lastVertexStart = previousVertices.length - stride; + var lastU = previousVertices[lastVertexStart + 4]; + var lastV = previousVertices[lastVertexStart + 5]; + + if (lastU === u && lastV === v) { + for (var i = 0; i < stride; ++i) { + tileVertices.push(previousVertices[lastVertexStart + i]); + } + return; + } + + // Previous edge doesn't contain a suitable vertex either, so fabricate one. + cartographicScratch.longitude = longitude; + cartographicScratch.latitude = latitude; + cartographicScratch.height = height; + ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + tileVertices.push(cartesianScratch.x, cartesianScratch.y, cartesianScratch.z, height, u, v); + + if (hasWebMercatorT) { + // Identical to v at 0.0 and 1.0. + tileVertices.push(v); + } + + if (hasVertexNormals) { + ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); + AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); + tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); + //tileVertices.push(AttributeCompression.octPackFloat(octEncodedNormalScratch)); + } + } + + function addVerticesToFillTile(edgeDetails, stride, tileVertices) { + var vertices = edgeDetails.vertices; + + // Copy all but the last vertex. + var i; + var u; + var v; + var lastU; + var lastV; + for (i = 0; i < vertices.length - stride; i += stride) { + u = vertices[i + 4]; + v = vertices[i + 5]; + if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { + // Vertex is very close to the previous one, so skip it. + continue; + } + + var end = i + stride; + for (var j = i; j < end; ++j) { + tileVertices.push(vertices[j]); + } + + lastU = u; + lastV = v; + } + + // Copy the last vertex too if it's _not_ a corner vertex. + var lastVertexStart = i; + u = vertices[lastVertexStart + 4]; + v = vertices[lastVertexStart + 5]; + + if (lastVertexStart < vertices.length && ((u !== 0.0 && u !== 1.0) || (v !== 0.0 && v !== 1.0))) { + if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { + // Overwrite the previous vertex because it's very close to the last one. + tileVertices.length -= stride; + } + + for (; i < vertices.length; ++i) { + tileVertices.push(vertices[i]); + } + } + } + + var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()]; + + function computeOccludeePoint(tileProvider, center, rectangle, height, result) { + var ellipsoidalOccluder = tileProvider.quadtree._occluders.ellipsoid; + var ellipsoid = ellipsoidalOccluder.ellipsoid; + + var cornerPositions = cornerPositionsScratch; + Cartesian3.fromRadians(rectangle.west, rectangle.south, height, ellipsoid, cornerPositions[0]); + Cartesian3.fromRadians(rectangle.east, rectangle.south, height, ellipsoid, cornerPositions[1]); + Cartesian3.fromRadians(rectangle.west, rectangle.north, height, ellipsoid, cornerPositions[2]); + Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]); + + return ellipsoidalOccluder.computeHorizonCullingPoint(center, cornerPositions, result); + } + + return TerrainFillMesh; +}); diff --git a/Source/Scene/TileImagery.js b/Source/Scene/TileImagery.js index 8da112b12ac..e1682f2952f 100644 --- a/Source/Scene/TileImagery.js +++ b/Source/Scene/TileImagery.js @@ -43,15 +43,14 @@ define([ * * @param {Tile} tile The tile to which this instance belongs. * @param {FrameState} frameState The frameState. + * @param {Boolean} skipLoading True to skip loading, but synchronously process imagery that's already mostly ready to go. * @returns {Boolean} True if this instance is done loading; otherwise, false. */ TileImagery.prototype.processStateMachine = function(tile, frameState, skipLoading) { var loadingImagery = this.loadingImagery; var imageryLayer = loadingImagery.imageryLayer; - if (!skipLoading) { - loadingImagery.processStateMachine(frameState, !this.useWebMercatorT); - } + loadingImagery.processStateMachine(frameState, !this.useWebMercatorT, skipLoading); if (loadingImagery.state === ImageryState.READY) { if (defined(this.readyImagery)) { @@ -93,9 +92,7 @@ define([ // Push the ancestor's load process along a bit. This is necessary because some ancestor imagery // tiles may not be attached directly to a terrain tile. Such tiles will never load if // we don't do it here. - if (!skipLoading) { - closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT); - } + closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT, skipLoading); return false; // not done loading } // This imagery tile is failed or invalid, and we have the "best available" substitute. diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index 6b9190d103c..da8364a993d 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -252,7 +252,7 @@ void main() if (u_isFill) { - finalColor = vec4(mix(vec3(1.0, 0.0, 0.0), finalColor.rgb, 0.25), finalColor.a); + finalColor = vec4(mix(vec3(0.0), finalColor.rgb, 0.25), finalColor.a); } #ifdef FOG From 955c996334167fd299ef1115d0068d641f1ce0f2 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 11 Sep 2018 18:00:18 +1000 Subject: [PATCH 035/131] New fill tile code kinda working. --- .../gallery/TerrainPerformance.html | 3 +- Source/Scene/GlobeSurfaceTile.js | 2 +- Source/Scene/GlobeSurfaceTileProvider.js | 2 +- Source/Scene/TerrainFillMesh.js | 553 ++++++++++++++++-- .../CesiumInspectorViewModel.js | 2 +- 5 files changed, 501 insertions(+), 61 deletions(-) diff --git a/Apps/Sandcastle/gallery/TerrainPerformance.html b/Apps/Sandcastle/gallery/TerrainPerformance.html index 1b5b6fbf8a2..5fb457e2c01 100644 --- a/Apps/Sandcastle/gallery/TerrainPerformance.html +++ b/Apps/Sandcastle/gallery/TerrainPerformance.html @@ -36,10 +36,11 @@ var statistics = Cesium.RequestScheduler.statistics; // var cesiumTerrainProviderMeshes = new Cesium.CesiumTerrainProvider({ -// url : 'http://localhost:8081/2018-07-11b', +// url : 'http://localhost:8001/2018-07-11b', // requestWaterMask : true, // requestVertexNormals : true // }); +// viewer.terrainProvider = cesiumTerrainProviderMeshes; viewer.terrainProvider = Cesium.createWorldTerrain(); var startTime; diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index f4059ec25f3..92673085fc2 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -181,7 +181,7 @@ define([ var scratchResult = new Cartesian3(); GlobeSurfaceTile.prototype.pick = function(ray, mode, projection, cullBackFaces, result) { - var mesh = this.mesh || this.fill.mesh; + var mesh = this.mesh || (this.fill && this.fill.mesh); if (!defined(mesh)) { return undefined; } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 343acae5924..65f835f8490 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -510,7 +510,7 @@ define([ // If this frame has a mix of loaded and fill tiles, we need to propagate // loaded heights to the fill tiles. - if (this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) { + if (this.missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE && this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) { TerrainFillMesh.updateFillTiles(this, this._quadtree._tilesToRender, frameState); } diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index d0b9870e79b..d57d8422eab 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -1,5 +1,6 @@ define([ '../Core/AttributeCompression', + '../Core/binarySearch', '../Core/BoundingSphere', '../Core/Cartesian2', '../Core/Cartesian3', @@ -22,6 +23,7 @@ define([ './TileSelectionResult' ], function( AttributeCompression, + binarySearch, BoundingSphere, Cartesian2, Cartesian3, @@ -138,8 +140,9 @@ define([ } var tile = startTile; - while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || tile._lastSelectionResult === TileSelectionResult.KICKED)) { + while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || tile._lastSelectionResult === TileSelectionResult.KICKED || tile._lastSelectionResult === TileSelectionResult.CULLED)) { // This wasn't visited or it was visited and then kicked, so walk up to find the closest ancestor that was rendered. + // We also walk up if the tile was culled, because if siblings were kicked an ancestor may have been rendered. if (downOnly) { return; } @@ -248,8 +251,8 @@ define([ if (sourceFill.changedThisFrame) { createFillMesh(tileProvider, frameState, sourceTile); sourceTile.data.fill.changedThisFrame = false; - sourceMesh = sourceTile.data.fill.mesh; } + sourceMesh = sourceTile.data.fill.mesh; } var edgeMeshes; @@ -430,69 +433,95 @@ define([ var quadtree = tileProvider._quadtree; var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber; - var west = getEdgeVertices(tile, fill.westTiles, fill.westMeshes, lastSelectionFrameNumber, TileEdge.EAST, westScratch); - var south = getEdgeVertices(tile, fill.southTiles, fill.southMeshes, lastSelectionFrameNumber, TileEdge.NORTH, southScratch); - var east = getEdgeVertices(tile, fill.eastTiles, fill.eastMeshes, lastSelectionFrameNumber, TileEdge.WEST, eastScratch); - var north = getEdgeVertices(tile, fill.northTiles, fill.northMeshes, lastSelectionFrameNumber, TileEdge.SOUTH, northScratch); + var tileVertices = tileVerticesScratch; + tileVertices.length = 0; + var ellipsoid = tile.tilingScheme.ellipsoid; var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; var hasWebMercatorT = true; // TODO var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); - var minimumHeight = Number.MAX_VALUE; - var maximumHeight = -Number.MAX_VALUE; - var hasAnyVertices = false; - - if (west.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, west.minimumHeight); - maximumHeight = Math.max(maximumHeight, west.maximumHeight); - hasAnyVertices = true; - } - - if (south.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, south.minimumHeight); - maximumHeight = Math.max(maximumHeight, south.maximumHeight); - hasAnyVertices = true; - } - - if (east.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, east.minimumHeight); - maximumHeight = Math.max(maximumHeight, east.maximumHeight); - hasAnyVertices = true; - } - - if (north.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, north.minimumHeight); - maximumHeight = Math.max(maximumHeight, north.maximumHeight); - hasAnyVertices = true; - } - - if (!hasAnyVertices) { - var tileBoundingRegion = surfaceTile.tileBoundingRegion; - minimumHeight = tileBoundingRegion.minimumHeight; - maximumHeight = tileBoundingRegion.maximumHeight; - } + var northwestIndex = 0; + addCorner(fill, ellipsoid, 0.0, 1.0, fill.northwestTile, fill.northwestMesh, fill.westTiles, fill.westMeshes, fill.northTiles, fill.northMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + var southwestIndex = 1; + addCorner(fill, ellipsoid, 0.0, 0.0, fill.southwestTile, fill.southwestMesh, fill.southTiles, fill.southMeshes, fill.westTiles, fill.westMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + var southeastIndex = 2; + addCorner(fill, ellipsoid, 1.0, 0.0, fill.southeastTile, fill.southeastMesh, fill.eastTiles, fill.eastMeshes, fill.southTiles, fill.southMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + var northeastIndex = 3; + addCorner(fill, ellipsoid, 1.0, 1.0, fill.northeastTile, fill.northeastMesh, fill.northTiles, fill.northMeshes, fill.eastTiles, fill.eastMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + + // TODO: slight optimization: track min/max as we're adding vertices. + var southwestHeight = tileVertices[southwestIndex * stride + 3]; + var southeastHeight = tileVertices[southeastIndex * stride + 3]; + var northwestHeight = tileVertices[northwestIndex * stride + 3]; + var northeastHeight = tileVertices[northeastIndex * stride + 3]; + + var minimumHeight = Math.min(southwestHeight, southeastHeight, northwestHeight, northeastHeight); + var maximumHeight = Math.max(southwestHeight, southeastHeight, northwestHeight, northeastHeight); + + // var west = getEdgeVertices(tile, fill.westTiles, fill.westMeshes, lastSelectionFrameNumber, TileEdge.EAST, westScratch); + // var south = getEdgeVertices(tile, fill.southTiles, fill.southMeshes, lastSelectionFrameNumber, TileEdge.NORTH, southScratch); + // var east = getEdgeVertices(tile, fill.eastTiles, fill.eastMeshes, lastSelectionFrameNumber, TileEdge.WEST, eastScratch); + // var north = getEdgeVertices(tile, fill.northTiles, fill.northMeshes, lastSelectionFrameNumber, TileEdge.SOUTH, northScratch); + + // var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; + // var hasWebMercatorT = true; // TODO + // var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); + + // var minimumHeight = Number.MAX_VALUE; + // var maximumHeight = -Number.MAX_VALUE; + // var hasAnyVertices = false; + + // if (west.vertices.length > 0) { + // minimumHeight = Math.min(minimumHeight, west.minimumHeight); + // maximumHeight = Math.max(maximumHeight, west.maximumHeight); + // hasAnyVertices = true; + // } + + // if (south.vertices.length > 0) { + // minimumHeight = Math.min(minimumHeight, south.minimumHeight); + // maximumHeight = Math.max(maximumHeight, south.maximumHeight); + // hasAnyVertices = true; + // } + + // if (east.vertices.length > 0) { + // minimumHeight = Math.min(minimumHeight, east.minimumHeight); + // maximumHeight = Math.max(maximumHeight, east.maximumHeight); + // hasAnyVertices = true; + // } + + // if (north.vertices.length > 0) { + // minimumHeight = Math.min(minimumHeight, north.minimumHeight); + // maximumHeight = Math.max(maximumHeight, north.maximumHeight); + // hasAnyVertices = true; + // } + + // if (!hasAnyVertices) { + // var tileBoundingRegion = surfaceTile.tileBoundingRegion; + // minimumHeight = tileBoundingRegion.minimumHeight; + // maximumHeight = tileBoundingRegion.maximumHeight; + // } var middleHeight = (minimumHeight + maximumHeight) * 0.5; - var tileVertices = tileVerticesScratch; - tileVertices.length = 0; - - var ellipsoid = tile.tilingScheme.ellipsoid; - var rectangle = tile.rectangle; - - var northwestIndex = 0; - addCornerVertexIfNecessary(ellipsoid, 0.0, 1.0, rectangle.west, rectangle.north, middleHeight, west, north, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(west, stride, tileVertices); - var southwestIndex = tileVertices.length / stride; - addCornerVertexIfNecessary(ellipsoid, 0.0, 0.0, rectangle.west, rectangle.south, middleHeight, south, west, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(south, stride, tileVertices); - var southeastIndex = tileVertices.length / stride; - addCornerVertexIfNecessary(ellipsoid, 1.0, 0.0, rectangle.east, rectangle.south, middleHeight, east, south, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(east, stride, tileVertices); - var northeastIndex = tileVertices.length / stride; - addCornerVertexIfNecessary(ellipsoid, 1.0, 1.0, rectangle.east, rectangle.north, middleHeight, north, east, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(north, stride, tileVertices); + // var tileVertices = tileVerticesScratch; + // tileVertices.length = 0; + + // var ellipsoid = tile.tilingScheme.ellipsoid; + // var rectangle = tile.rectangle; + + // var northwestIndex = 0; + // addCornerVertexIfNecessary(ellipsoid, 0.0, 1.0, rectangle.west, rectangle.north, middleHeight, west, north, hasVertexNormals, hasWebMercatorT, tileVertices); + // addVerticesToFillTile(west, stride, tileVertices); + // var southwestIndex = tileVertices.length / stride; + // addCornerVertexIfNecessary(ellipsoid, 0.0, 0.0, rectangle.west, rectangle.south, middleHeight, south, west, hasVertexNormals, hasWebMercatorT, tileVertices); + // addVerticesToFillTile(south, stride, tileVertices); + // var southeastIndex = tileVertices.length / stride; + // addCornerVertexIfNecessary(ellipsoid, 1.0, 0.0, rectangle.east, rectangle.south, middleHeight, east, south, hasVertexNormals, hasWebMercatorT, tileVertices); + // addVerticesToFillTile(east, stride, tileVertices); + // var northeastIndex = tileVertices.length / stride; + // addCornerVertexIfNecessary(ellipsoid, 1.0, 1.0, rectangle.east, rectangle.north, middleHeight, north, east, hasVertexNormals, hasWebMercatorT, tileVertices); + // addVerticesToFillTile(north, stride, tileVertices); // Add a single vertex at the center of the tile. var obb = OrientedBoundingBox.fromRectangle(tile.rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); @@ -502,6 +531,7 @@ define([ cartographicScratch.height = middleHeight; var centerVertexPosition = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + var rectangle = tile.rectangle; tileVertices.push(centerVertexPosition.x, centerVertexPosition.y, centerVertexPosition.z, middleHeight); tileVertices.push((cartographicScratch.longitude - rectangle.west) / (rectangle.east - rectangle.west)); tileVertices.push((cartographicScratch.latitude - rectangle.south) / (rectangle.north - rectangle.south)); @@ -671,6 +701,15 @@ define([ result.clear(); + // TODO: add first corner vertex if it exists. + // TODO: if a corner is missing, fill it in. When we add a corner, we create an edge + // (or two) that didn't exist before. We need to propagate that edge to adjacent tile(s) + // but be careful not to trigger regeneration of those adjacent tiles. We can do + // that by updating the adjacent tile's list of edge meshes without setting the + // changed flag. + // When creating a new fill mesh, immediately adjust all adjacent fill tiles to know they're in sync + // with that new mesh (because the new mesh was purposely created to be in sync!). + for (var i = 0; i < edgeMeshes.length; ++i) { var edgeTile = edgeTiles[i]; var surfaceTile = edgeTile.data; @@ -690,9 +729,409 @@ define([ } } + // TODO: add last corner vertex if it exists + return result; } + function transformTextureCoordinate(toMin, toMax, fromValue) { + return (fromValue - toMin) / (toMax - toMin); + } + + function transformTextureCoordinates(sourceRectangle, targetRectangle, coordinates, result) { + var sourceWidth = sourceRectangle.east - sourceRectangle.west; + var umin = (targetRectangle.west - sourceRectangle.west) / sourceWidth; + var umax = (targetRectangle.east - sourceRectangle.west) / sourceWidth; + + var sourceHeight = sourceRectangle.north - sourceRectangle.south; + var vmin = (targetRectangle.south - sourceRectangle.south) / sourceHeight; + var vmax = (targetRectangle.north - sourceRectangle.south) / sourceHeight; + + var u = (coordinates.x - umin) / (umax - umin); + var v = (coordinates.y - vmin) / (vmax - vmin); + + // Ensure that coordinates very near the corners are at the corners. + if (Math.abs(u) < Math.EPSILON8) { + u = 0.0; + } else if (Math.abs(u - 1.0) < Math.EPSILON8) { + u = 1.0; + } + + if (Math.abs(v) < Math.EPSILON8) { + v = 0.0; + } else if (Math.abs(v - 1.0) < Math.EPSILON8) { + v = 1.0; + } + + if (!defined(result)) { + return new Cartesian2(u, v); + } + + result.x = u; + result.y = v; + return result; + } + + var positionScratch = new Cartesian3(); + var encodedNormalScratch = new Cartesian2(); + var uvScratch = new Cartesian2(); + + function addVertexFromTileAtCorner(sourceMesh, sourceIndex, u, v, tileVertices) { + var sourceEncoding = sourceMesh.encoding; + var sourceVertices = sourceMesh.vertices; + + sourceEncoding.decodePosition(sourceVertices, sourceIndex, positionScratch); + tileVertices.push(positionScratch.x, positionScratch.y, positionScratch.z); + + tileVertices.push(sourceEncoding.decodeHeight(sourceVertices, sourceIndex), u, v); + + if (sourceEncoding.hasWebMercatorT) { + // At the corners, the geographic and web mercator vertical texture coordinate + // is the same: either 0.0 or 1.0; + tileVertices.push(v); + } + + if (sourceEncoding.hasVertexNormals) { + sourceEncoding.getOctEncodedNormal(sourceVertices, sourceIndex, encodedNormalScratch); + tileVertices.push(encodedNormalScratch.x, encodedNormalScratch.y); + } + } + + var uvScratch2 = new Cartesian2(); + var encodedNormalScratch2 = new Cartesian2(); + var cartesianScratch2 = new Cartesian3(); + + function addInterpolatedVertexAtCorner(ellipsoid, sourceRectangle, targetRectangle, sourceMesh, previousIndex, nextIndex, u, v, interpolateU, tileVertices) { + var sourceEncoding = sourceMesh.encoding; + var sourceVertices = sourceMesh.vertices; + + var previousUv = transformTextureCoordinates(sourceRectangle, targetRectangle, sourceEncoding.decodeTextureCoordinates(sourceVertices, previousIndex, uvScratch), uvScratch); + var nextUv = transformTextureCoordinates(sourceRectangle, targetRectangle, sourceEncoding.decodeTextureCoordinates(sourceVertices, previousIndex, uvScratch2), uvScratch2); + + var ratio; + if (interpolateU) { + ratio = (u - previousUv.x) / (nextUv.x - previousUv.x); + } else { + ratio = (v - previousUv.y) / (nextUv.y - previousUv.y); + } + + var height1 = sourceEncoding.decodeHeight(sourceVertices, previousIndex); + var height2 = sourceEncoding.decodeHeight(sourceVertices, nextIndex); + + cartographicScratch.longitude = CesiumMath.lerp(targetRectangle.west, targetRectangle.east, u); + cartographicScratch.latitude = CesiumMath.lerp(targetRectangle.south, targetRectangle.north, v); + cartographicScratch.height = CesiumMath.lerp(height1, height2, ratio); + var position = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + + tileVertices.push(position.x, position.y, position.z); + tileVertices.push(cartographicScratch.height, u, v); + + if (sourceEncoding.hasWebMercatorT) { + // At the corners, the geographic and web mercator vertical texture coordinate + // is the same: either 0.0 or 1.0; + tileVertices.push(v); + } + + if (sourceEncoding.hasVertexNormals) { + var encodedNormal1 = sourceEncoding.getOctEncodedNormal(sourceVertices, previousIndex, encodedNormalScratch); + var encodedNormal2 = sourceEncoding.getOctEncodedNormal(sourceVertices, nextIndex, encodedNormalScratch2); + var normal1 = AttributeCompression.octDecode(encodedNormal1.x, encodedNormal1.y, cartesianScratch); + var normal2 = AttributeCompression.octDecode(encodedNormal2.x, encodedNormal2.y, cartesianScratch2); + var normal = Cartesian3.lerp(normal1, normal2, ratio, cartesianScratch); + Cartesian3.normalize(normal, normal); + var encodedNormal = AttributeCompression.octEncode(normal, encodedNormalScratch); + tileVertices.push(encodedNormal.x, encodedNormal.y); + } + } + + function addVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, hasVertexNormals, hasWebMercatorT, tileVertices) { + var rectangle = terrainFillMesh.tile.rectangle; + + cartographicScratch.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); + cartographicScratch.latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); + cartographicScratch.height = height; + var position = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + + tileVertices.push(position.x, position.y, position.z); + tileVertices.push(height, u, v); + + if (hasWebMercatorT) { + // At the corners, the geographic and web mercator vertical texture coordinate + // is the same: either 0.0 or 1.0; + tileVertices.push(v); + } + + if (hasVertexNormals) { + var normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); + var encodedNormal = AttributeCompression.octEncode(normal, encodedNormalScratch); + tileVertices.push(encodedNormal.x, encodedNormal.y); + } + } + + function addCorner( + terrainFillMesh, + ellipsoid, + u, v, + cornerTile, cornerMesh, + previousEdgeTiles, previousEdgeMeshes, + nextEdgeTiles, nextEdgeMeshes, + hasVertexNormals, hasWebMercatorT, + tileVertices + ) { + var vertexIndex; + + if (cornerMesh !== undefined && !cornerMesh.changedThisFrame) { + // Corner mesh is valid, copy its corner vertex to this mesh. + var cornerTerrainMesh = cornerMesh.mesh === undefined ? cornerMesh : cornerMesh.mesh; + if (u === 0.0) { + if (v === 0.0) { + // southwest destination, northeast source + vertexIndex = cornerTerrainMesh.eastIndicesNorthToSouth[0]; + } else { + // northwest destination, southeast source + vertexIndex = cornerTerrainMesh.southIndicesEastToWest[0]; + } + } else if (v === 0.0) { + // southeast destination, northwest source + vertexIndex = cornerTerrainMesh.northIndicesWestToEast[0]; + } else { + // northeast destination, southwest source + vertexIndex = cornerTerrainMesh.westIndicesSouthToNorth[0]; + } + addVertexFromTileAtCorner(cornerTerrainMesh, vertexIndex, u, v, tileVertices); + return; + } + + var gotCorner = + addCornerFromEdge(terrainFillMesh, ellipsoid, previousEdgeMeshes, previousEdgeTiles, false, u, v, tileVertices) || + addCornerFromEdge(terrainFillMesh, ellipsoid, nextEdgeMeshes, nextEdgeTiles, true, u, v, tileVertices); + if (gotCorner) { + return; + } + + // There is no precise vertex available from the corner or from either adjacent edge. + // So use the height from the closest vertex anywhere on the perimeter of this tile. + // TODO: would be better to find the closest height rather than favoring a side. + // TODO: We'll do the wrong thing if the height is exactly 0.0 because that's falsy. + var height; + if (u === 0.0) { + if (v === 0.0) { + // southwest + height = + getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, false, TileEdge.NORTH, u, v) || + getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, true, TileEdge.EAST, u, v) || + getHeightAtCorner(terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, u, v) || + getHeightAtCorner(terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, false, TileEdge.WEST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, true, TileEdge.SOUTH, u, v) || + getHeightAtCorner(terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, u, v); + } else { + // northwest + height = + getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, false, TileEdge.SOUTH, u, v) || + getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, true, TileEdge.EAST, u, v) || + getHeightAtCorner(terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, u, v) || + getHeightAtCorner(terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, false, TileEdge.WEST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, true, TileEdge.NORTH, u, v) || + getHeightAtCorner(terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, u, v); + } + } else if (v === 0.0) { + // southeast + height = + getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, false, TileEdge.NORTH, u, v) || + getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, true, TileEdge.WEST, u, v) || + getHeightAtCorner(terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, u, v) || + getHeightAtCorner(terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, false, TileEdge.EAST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, true, TileEdge.SOUTH, u, v) || + getHeightAtCorner(terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, u, v); + } else { + // northeast + height = + getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, false, TileEdge.SOUTH, u, v) || + getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, true, TileEdge.WEST, u, v) || + getHeightAtCorner(terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, u, v) || + getHeightAtCorner(terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, false, TileEdge.EAST, u, v) || + getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, true, TileEdge.NORTH, u, v) || + getHeightAtCorner(terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, u, v); + } + + if (!defined(height)) { + // No heights available whatsoever, so use the average of this tile's minimum and maximum height. + var surfaceTile = terrainFillMesh.tile.data; + var tileBoundingRegion = surfaceTile.tileBoundingRegion; + var minimumHeight = tileBoundingRegion.minimumHeight; + var maximumHeight = tileBoundingRegion.maximumHeight; + height = (minimumHeight + maximumHeight) * 0.5; + } + + addVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, hasVertexNormals, hasWebMercatorT, tileVertices); + } + + function getNearestHeightOnEdge(meshes, tiles, isNext, edge, u, v) { + var meshStart; + var meshEnd; + var meshStep; + + if (isNext) { + meshStart = 0; + meshEnd = meshes.length; + meshStep = 1; + } else { + meshStart = meshes.length - 1; + meshEnd = -1; + meshStep = -1; + } + + for (var meshIndex = meshStart; meshIndex !== meshEnd; meshIndex += meshStep) { + var mesh = meshes[meshIndex]; + if (!defined(mesh) || mesh.changedThisFrame) { + continue; + } + + var terrainMesh = mesh.mesh ? mesh.mesh : mesh; + + var indices; + switch (edge) { + case TileEdge.WEST: + indices = terrainMesh.westIndicesSouthToNorth; + break; + case TileEdge.SOUTH: + indices = terrainMesh.southIndicesEastToWest; + break; + case TileEdge.EAST: + indices = terrainMesh.eastIndicesNorthToSouth; + break; + case TileEdge.NORTH: + indices = terrainMesh.northIndicesWestToEast; + break; + } + + var index = indices[isNext ? 0 : indices.length - 1]; + if (defined(index)) { + return mesh.encoding.decodeHeight(terrainMesh.vertices, index); + } + } + + return undefined; + } + + function getHeightAtCorner(mesh, tile, edge, u, v) { + if (!defined(mesh) || mesh.changedThisFrame) { + return undefined; + } + + var terrainMesh = mesh.mesh ? mesh.mesh : mesh; + + var indices; + switch (edge) { + case TileEdge.SOUTHWEST: + indices = terrainMesh.westIndicesSouthToNorth; + break; + case TileEdge.SOUTHEAST: + indices = terrainMesh.southIndicesEastToWest; + break; + case TileEdge.NORTHEAST: + indices = terrainMesh.eastIndicesNorthToSouth; + break; + case TileEdge.NORTHWEST: + indices = terrainMesh.northIndicesWestToEast; + break; + } + + var index = indices[0]; + if (defined(index)) { + return mesh.encoding.decodeHeight(terrainMesh.vertices, index); + } + + return undefined; + } + + function addCornerFromEdge(terrainFillMesh, ellipsoid, edgeMeshes, edgeTiles, isNext, u, v, tileVertices) { + var edgeVertices; + var compareU; + var increasing; + var vertexIndexIndex; + var vertexIndex; + var sourceMesh = edgeMeshes[isNext ? 0 : edgeMeshes.length - 1]; + + if (sourceMesh !== undefined && !sourceMesh.changedThisFrame) { + // Previous mesh is valid, but we don't know yet if it covers this corner. + var sourceTerrainMesh = sourceMesh.mesh === undefined ? sourceMesh : sourceMesh.mesh; + + if (u === 0.0) { + if (v === 0.0) { + // southwest + edgeVertices = isNext ? sourceTerrainMesh.eastIndicesNorthToSouth : sourceTerrainMesh.northIndicesWestToEast; + compareU = !isNext; + increasing = !isNext; + } else { + // northwest + edgeVertices = isNext ? sourceTerrainMesh.southIndicesEastToWest : sourceTerrainMesh.eastIndicesNorthToSouth; + compareU = isNext; + increasing = false; + } + } else if (v === 0.0) { + // southeast + edgeVertices = isNext ? sourceTerrainMesh.northIndicesWestToEast : sourceTerrainMesh.westIndicesSouthToNorth; + compareU = isNext; + increasing = true; + } else { + // northeast + edgeVertices = isNext ? sourceTerrainMesh.westIndicesSouthToNorth : sourceTerrainMesh.southIndicesEastToWest; + compareU = !isNext; + increasing = isNext; + } + + if (edgeVertices.length > 0) { + // The vertex we want will very often be the first/last vertex so check that first. + var sourceTile = edgeTiles[isNext ? 0 : edgeTiles.length - 1]; + vertexIndexIndex = isNext ? edgeVertices.length - 1 : 0; + vertexIndex = edgeVertices[vertexIndexIndex]; + sourceTerrainMesh.encoding.decodeTextureCoordinates(sourceTerrainMesh.vertices, vertexIndex, uvScratch); + var targetUv = transformTextureCoordinates(sourceTile.rectangle, terrainFillMesh.tile.rectangle, uvScratch, uvScratch); + if (targetUv.x === u && targetUv.y === v) { + // Vertex is good! + addVertexFromTileAtCorner(sourceTerrainMesh, vertexIndex, u, v, tileVertices); + return true; + } + + // The last vertex is not the one we need, try binary searching for the right one. + vertexIndexIndex = binarySearch(edgeVertices, compareU ? u : v, function(vertexIndex, textureCoordinate) { + sourceTerrainMesh.encoding.decodeTextureCoordinates(sourceTerrainMesh.vertices, vertexIndex, uvScratch); + var targetUv = transformTextureCoordinates(sourceTile.rectangle, terrainFillMesh.tile.rectangle, uvScratch, uvScratch); + if (increasing) { + if (compareU) { + return u - targetUv.x; + } + return v - targetUv.y; + } else if (compareU) { + return targetUv.x - u; + } + return targetUv.y - v; + }); + + if (vertexIndexIndex < 0) { + vertexIndexIndex = ~vertexIndexIndex; + + if (vertexIndexIndex > 0 && vertexIndexIndex < edgeVertices.length) { + // The corner falls between two vertices, so interpolate between them. + addInterpolatedVertexAtCorner(ellipsoid, sourceTile.rectangle, terrainFillMesh.tile.rectangle, sourceTerrainMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, tileVertices); + return true; + } + } else { + // Found a vertex that fits in the corner exactly. + addVertexFromTileAtCorner(sourceTerrainMesh, edgeVertices[vertexIndexIndex], u, v, tileVertices); + return true; + } + } + } + + return false; + } + function addCornerVertexIfNecessary(ellipsoid, u, v, longitude, latitude, height, edgeDetails, previousEdgeDetails, hasVertexNormals, hasWebMercatorT, tileVertices) { var vertices = edgeDetails.vertices; diff --git a/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js b/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js index 20c1d23a79a..d18b2f225cf 100644 --- a/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js +++ b/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js @@ -498,7 +498,7 @@ define([ this._doFilterTile = createCommand(function() { if (!that.filterTile) { - that.suspendUpdates = false; + //that.suspendUpdates = false; } else { that.suspendUpdates = true; From 3a93a1026054bf7b4aafbca281ca7ac345136672 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 12 Sep 2018 21:50:14 +1000 Subject: [PATCH 036/131] Add edge vertices, fix bugs. --- Source/Scene/TerrainFillMesh.js | 106 +++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index d57d8422eab..2361b3b2782 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -49,13 +49,13 @@ define([ function TerrainFillMesh() { this.tile = undefined; this.frameLastUpdated = undefined; - this.westMeshes = []; + this.westMeshes = []; // north to south (CCW) this.westTiles = []; - this.southMeshes = []; + this.southMeshes = []; // west to east (CCW) this.southTiles = []; - this.eastMeshes = []; + this.eastMeshes = []; // south to north (CCW) this.eastTiles = []; - this.northMeshes = []; + this.northMeshes = []; // east to west (CCW) this.northTiles = []; this.southwestMesh = undefined; this.southwestTile = undefined; @@ -442,13 +442,17 @@ define([ var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); var northwestIndex = 0; - addCorner(fill, ellipsoid, 0.0, 1.0, fill.northwestTile, fill.northwestMesh, fill.westTiles, fill.westMeshes, fill.northTiles, fill.northMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); - var southwestIndex = 1; - addCorner(fill, ellipsoid, 0.0, 0.0, fill.southwestTile, fill.southwestMesh, fill.southTiles, fill.southMeshes, fill.westTiles, fill.westMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); - var southeastIndex = 2; - addCorner(fill, ellipsoid, 1.0, 0.0, fill.southeastTile, fill.southeastMesh, fill.eastTiles, fill.eastMeshes, fill.southTiles, fill.southMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); - var northeastIndex = 3; - addCorner(fill, ellipsoid, 1.0, 1.0, fill.northeastTile, fill.northeastMesh, fill.northTiles, fill.northMeshes, fill.eastTiles, fill.eastMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + addCorner(fill, ellipsoid, 0.0, 1.0, fill.northwestTile, fill.northwestMesh, fill.northTiles, fill.northMeshes, fill.westTiles, fill.westMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + addEdge(fill, ellipsoid, fill.westTiles, fill.westMeshes, TileEdge.EAST, stride, tileVertices); + var southwestIndex = tileVertices.length / stride; + addCorner(fill, ellipsoid, 0.0, 0.0, fill.southwestTile, fill.southwestMesh, fill.westTiles, fill.westMeshes, fill.southTiles, fill.southMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + addEdge(fill, ellipsoid, fill.southTiles, fill.southMeshes, TileEdge.NORTH, stride, tileVertices); + var southeastIndex = tileVertices.length / stride; + addCorner(fill, ellipsoid, 1.0, 0.0, fill.southeastTile, fill.southeastMesh, fill.southTiles, fill.southMeshes, fill.eastTiles, fill.eastMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + addEdge(fill, ellipsoid, fill.eastTiles, fill.eastMeshes, TileEdge.WEST, stride, tileVertices); + var northeastIndex = tileVertices.length / stride; + addCorner(fill, ellipsoid, 1.0, 1.0, fill.northeastTile, fill.northeastMesh, fill.eastTiles, fill.eastMeshes, fill.northTiles, fill.northMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); + addEdge(fill, ellipsoid, fill.northTiles, fill.northMeshes, TileEdge.SOUTH, stride, tileVertices); // TODO: slight optimization: track min/max as we're adding vertices. var southwestHeight = tileVertices[southwestIndex * stride + 3]; @@ -970,6 +974,64 @@ define([ addVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, hasVertexNormals, hasWebMercatorT, tileVertices); } + function addEdge(terrainFillMesh, ellipsoid, edgeTiles, edgeMeshes, tileEdge, stride, tileVertices) { + for (var i = 0; i < edgeTiles.length; ++i) { + addEdgeMesh(terrainFillMesh, ellipsoid, edgeTiles[i], edgeMeshes[i], tileEdge, stride, tileVertices); + } + } + + var edgeDetailsScratch = new TerrainTileEdgeDetails(); + + function addEdgeMesh(terrainFillMesh, ellipsoid, edgeTile, edgeMesh, tileEdge, stride, tileVertices) { + var terrainMesh = edgeMesh.mesh ? edgeMesh.mesh : edgeMesh; + + edgeDetailsScratch.clear(); + var edgeDetails = terrainMesh.getEdgeVertices(tileEdge, edgeTile.rectangle, terrainFillMesh.tile.rectangle, ellipsoid, edgeDetailsScratch); + + var vertices = edgeDetails.vertices; + + var previousVertexIndex = tileVertices.length - stride; + + // Copy all but the last vertex. + var i; + var u; + var v; + var lastU = tileVertices[previousVertexIndex + 4]; + var lastV = tileVertices[previousVertexIndex + 5]; + for (i = 0; i < vertices.length - stride; i += stride) { + u = vertices[i + 4]; + v = vertices[i + 5]; + if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { + // Vertex is very close to the previous one, so skip it. + continue; + } + + var end = i + stride; + for (var j = i; j < end; ++j) { + tileVertices.push(vertices[j]); + } + + lastU = u; + lastV = v; + } + + // Copy the last vertex too if it's _not_ a corner vertex. + var lastVertexStart = i; + u = vertices[lastVertexStart + 4]; + v = vertices[lastVertexStart + 5]; + + if (lastVertexStart < vertices.length && ((u !== 0.0 && u !== 1.0) || (v !== 0.0 && v !== 1.0))) { + if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { + // Overwrite the previous vertex because it's very close to the last one. + tileVertices.length -= stride; + } + + for (; i < vertices.length; ++i) { + tileVertices.push(vertices[i]); + } + } + } + function getNearestHeightOnEdge(meshes, tiles, isNext, edge, u, v) { var meshStart; var meshEnd; @@ -1064,31 +1126,31 @@ define([ if (u === 0.0) { if (v === 0.0) { // southwest - edgeVertices = isNext ? sourceTerrainMesh.eastIndicesNorthToSouth : sourceTerrainMesh.northIndicesWestToEast; - compareU = !isNext; - increasing = !isNext; + edgeVertices = isNext ? sourceTerrainMesh.northIndicesWestToEast : sourceTerrainMesh.eastIndicesNorthToSouth; + compareU = isNext; + increasing = isNext; } else { // northwest - edgeVertices = isNext ? sourceTerrainMesh.southIndicesEastToWest : sourceTerrainMesh.eastIndicesNorthToSouth; - compareU = isNext; + edgeVertices = isNext ? sourceTerrainMesh.eastIndicesNorthToSouth : sourceTerrainMesh.southIndicesEastToWest; + compareU = !isNext; increasing = false; } } else if (v === 0.0) { // southeast - edgeVertices = isNext ? sourceTerrainMesh.northIndicesWestToEast : sourceTerrainMesh.westIndicesSouthToNorth; - compareU = isNext; + edgeVertices = isNext ? sourceTerrainMesh.westIndicesSouthToNorth : sourceTerrainMesh.northIndicesWestToEast; + compareU = !isNext; increasing = true; } else { // northeast - edgeVertices = isNext ? sourceTerrainMesh.westIndicesSouthToNorth : sourceTerrainMesh.southIndicesEastToWest; - compareU = !isNext; - increasing = isNext; + edgeVertices = isNext ? sourceTerrainMesh.southIndicesEastToWest : sourceTerrainMesh.westIndicesSouthToNorth; + compareU = isNext; + increasing = !isNext; } if (edgeVertices.length > 0) { // The vertex we want will very often be the first/last vertex so check that first. var sourceTile = edgeTiles[isNext ? 0 : edgeTiles.length - 1]; - vertexIndexIndex = isNext ? edgeVertices.length - 1 : 0; + vertexIndexIndex = isNext ? 0 : edgeVertices.length - 1; vertexIndex = edgeVertices[vertexIndexIndex]; sourceTerrainMesh.encoding.decodeTextureCoordinates(sourceTerrainMesh.vertices, vertexIndex, uvScratch); var targetUv = transformTextureCoordinates(sourceTile.rectangle, terrainFillMesh.tile.rectangle, uvScratch, uvScratch); From 34160b7923f23b7a9ca2d151acbe76be75064339 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 12 Sep 2018 22:37:12 +1000 Subject: [PATCH 037/131] Use correct index for interpolation. --- Source/Scene/TerrainFillMesh.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 2361b3b2782..3d86944db86 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -103,7 +103,7 @@ define([ for (var i = 0; i < renderedTiles.length; ++i) { var renderedTile = renderedTiles[i]; - if (renderedTile.renderable) { + if (defined(renderedTile.data.vertexArray)) { traversalQueue.enqueue(renderedTiles[i]); } } @@ -220,7 +220,7 @@ define([ } function visitTile(tileProvider, frameState, sourceTile, destinationTile, tileEdge, frameNumber, traversalQueue) { - if (destinationTile.renderable) { + if (defined(destinationTile.data.vertexArray)) { // No further processing necessary for renderable tiles. return; } @@ -810,7 +810,7 @@ define([ var sourceVertices = sourceMesh.vertices; var previousUv = transformTextureCoordinates(sourceRectangle, targetRectangle, sourceEncoding.decodeTextureCoordinates(sourceVertices, previousIndex, uvScratch), uvScratch); - var nextUv = transformTextureCoordinates(sourceRectangle, targetRectangle, sourceEncoding.decodeTextureCoordinates(sourceVertices, previousIndex, uvScratch2), uvScratch2); + var nextUv = transformTextureCoordinates(sourceRectangle, targetRectangle, sourceEncoding.decodeTextureCoordinates(sourceVertices, nextIndex, uvScratch2), uvScratch2); var ratio; if (interpolateU) { From 98d414a731419e90e4152c5880c01e091fde8021 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 12 Sep 2018 22:42:01 +1000 Subject: [PATCH 038/131] Handle totally disconnected fill tiles. --- Source/Scene/GlobeSurfaceTileProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 65f835f8490..18ca86aeac3 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1884,7 +1884,7 @@ define([ // height range. surfaceTile.fill = new TerrainFillMesh(); surfaceTile.fill.tile = tile; - return; // TODO + surfaceTile.fill.changedThisFrame = true; } surfaceTile.fill.update(tileProvider, frameState); //return; From f58b7f87ab527a5d51be6938de744ef39d465815 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 13 Sep 2018 10:45:26 +1000 Subject: [PATCH 039/131] Prefer a corner derived from an adjacent edge over a corner from the diagonal corner. --- Source/Scene/TerrainFillMesh.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 3d86944db86..fd0c01c20b5 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -882,6 +882,13 @@ define([ hasVertexNormals, hasWebMercatorT, tileVertices ) { + var gotCorner = + addCornerFromEdge(terrainFillMesh, ellipsoid, previousEdgeMeshes, previousEdgeTiles, false, u, v, tileVertices) || + addCornerFromEdge(terrainFillMesh, ellipsoid, nextEdgeMeshes, nextEdgeTiles, true, u, v, tileVertices); + if (gotCorner) { + return; + } + var vertexIndex; if (cornerMesh !== undefined && !cornerMesh.changedThisFrame) { @@ -906,13 +913,6 @@ define([ return; } - var gotCorner = - addCornerFromEdge(terrainFillMesh, ellipsoid, previousEdgeMeshes, previousEdgeTiles, false, u, v, tileVertices) || - addCornerFromEdge(terrainFillMesh, ellipsoid, nextEdgeMeshes, nextEdgeTiles, true, u, v, tileVertices); - if (gotCorner) { - return; - } - // There is no precise vertex available from the corner or from either adjacent edge. // So use the height from the closest vertex anywhere on the perimeter of this tile. // TODO: would be better to find the closest height rather than favoring a side. From 8eacbf5f917ff60c073c55374eb1d40040604c31 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 13 Sep 2018 21:05:38 +1000 Subject: [PATCH 040/131] Less crashy. --- Source/Scene/GlobeSurfaceTile.js | 2 + Source/Scene/TerrainFillMesh.js | 214 ++++++++++++++++++++----------- 2 files changed, 142 insertions(+), 74 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 92673085fc2..c474b5d8862 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -636,6 +636,8 @@ define([ }); surfaceTile.terrainState = TerrainState.READY; + + surfaceTile.fill = surfaceTile.fill && surfaceTile.fill.destroy(); } function getContextWaterMaskData(context) { diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index fd0c01c20b5..10d68d11d3f 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -80,6 +80,7 @@ define([ TerrainFillMesh.prototype.destroy = function() { this.vertexArray = this.vertexArray && this.vertexArray.destroy(); + return undefined; }; TerrainFillMesh.updateFillTiles = function(tileProvider, renderedTiles, frameState) { @@ -333,67 +334,106 @@ define([ var startIndex, endIndex, existingTile, existingRectangle; var sourceRectangle = sourceTile.rectangle; + function firstIsLessThanSecond(a, b, epsilon) { + if (Math.abs(a - b) < epsilon) { + return false; + } + return a < b; + } + + function firstIsLessThanOrEqualToSecond(a, b, epsilon) { + if (Math.abs(a - b) < epsilon) { + return true; + } + return a < b; + } + + function firstIsGreaterThanSecond(a, b, epsilon) { + if (Math.abs(a - b) < epsilon) { + return false; + } + return a > b; + } + + function firstIsGreaterThanOrEqualToSecond(a, b, epsilon) { + if (Math.abs(a - b) < epsilon) { + return true; + } + return a > b; + } + + var epsilon; + var destinationRectangle = destinationTile.rectangle; + switch (tileEdge) { case TileEdge.WEST: + epsilon = (destinationRectangle.north - destinationRectangle.south) * CesiumMath.EPSILON5; + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.north <= sourceRectangle.north) { + if (firstIsGreaterThanSecond(sourceRectangle.north, existingRectangle.south, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.south < sourceRectangle.south) { + if (firstIsGreaterThanOrEqualToSecond(sourceRectangle.south, existingRectangle.north, epsilon)) { break; } } break; case TileEdge.SOUTH: + epsilon = (destinationRectangle.east - destinationRectangle.west) * CesiumMath.EPSILON5; + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.west >= sourceRectangle.west) { + if (firstIsLessThanSecond(sourceRectangle.west, existingRectangle.east, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.east > sourceRectangle.east) { + if (firstIsLessThanOrEqualToSecond(sourceRectangle.east, existingRectangle.west, epsilon)) { break; } } break; case TileEdge.EAST: + epsilon = (destinationRectangle.north - destinationRectangle.south) * CesiumMath.EPSILON5; + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.south >= sourceRectangle.south) { + if (firstIsLessThanSecond(sourceRectangle.south, existingRectangle.north, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.north > sourceRectangle.north) { + if (firstIsLessThanOrEqualToSecond(sourceRectangle.north, existingRectangle.south, epsilon)) { break; } } break; case TileEdge.NORTH: + epsilon = (destinationRectangle.east - destinationRectangle.west) * CesiumMath.EPSILON5; + for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.east <= sourceRectangle.east) { + if (firstIsGreaterThanSecond(sourceRectangle.east, existingRectangle.west, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (existingRectangle.west < sourceRectangle.west) { + if (firstIsGreaterThanOrEqualToSecond(sourceRectangle.west, existingRectangle.east, epsilon)) { break; } } @@ -425,6 +465,7 @@ define([ var cartesianScratch = new Cartesian3(); var normalScratch = new Cartesian3(); var octEncodedNormalScratch = new Cartesian2(); + var highestFillTileVertexCount = 0; function createFillMesh(tileProvider, frameState, tile) { var surfaceTile = tile.data; @@ -614,6 +655,82 @@ define([ var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, hasVertexNormals, hasWebMercatorT); encoding.center = center; + if (vertexCount > highestFillTileVertexCount) { + highestFillTileVertexCount = vertexCount; + console.log('New highest vertex count: ', highestFillTileVertexCount, ' from L', tile.level, 'X', tile.x, 'Y', tile.y); + } + + var q1, q2, s; + var previousU, previousV, currentU, currentV; + + for (s = 1; s < eastIndicesNorthToSouth.length; ++s) { + q1 = eastIndicesNorthToSouth[s - 1]; + q2 = eastIndicesNorthToSouth[s]; + previousU = tileVertices[q1 * stride + 4]; + previousV = tileVertices[q1 * stride + 5]; + currentU = tileVertices[q2 * stride + 4]; + currentV = tileVertices[q2 * stride + 5]; + + if (previousU !== 1.0 || currentU !== 1.0) { + console.log('not east?!'); + } + + if (currentV >= previousV) { + console.log('not descending?!'); + } + } + + for (s = 1; s < westIndicesSouthToNorth.length; ++s) { + q1 = westIndicesSouthToNorth[s - 1]; + q2 = westIndicesSouthToNorth[s]; + previousU = tileVertices[q1 * stride + 4]; + previousV = tileVertices[q1 * stride + 5]; + currentU = tileVertices[q2 * stride + 4]; + currentV = tileVertices[q2 * stride + 5]; + + if (previousU !== 0.0 || currentU !== 0.0) { + console.log('not west?!'); + } + + if (currentV <= previousV) { + console.log('not ascending?!'); + } + } + + for (s = 1; s < southIndicesEastToWest.length; ++s) { + q1 = southIndicesEastToWest[s - 1]; + q2 = southIndicesEastToWest[s]; + previousU = tileVertices[q1 * stride + 4]; + previousV = tileVertices[q1 * stride + 5]; + currentU = tileVertices[q2 * stride + 4]; + currentV = tileVertices[q2 * stride + 5]; + + if (previousV !== 0.0 || currentV !== 0.0) { + console.log('not south?!'); + } + + if (currentU >= previousU) { + console.log('not descending?!'); + } + } + + for (s = 1; s < northIndicesWestToEast.length; ++s) { + q1 = northIndicesWestToEast[s - 1]; + q2 = northIndicesWestToEast[s]; + previousU = tileVertices[q1 * stride + 4]; + previousV = tileVertices[q1 * stride + 5]; + currentU = tileVertices[q2 * stride + 4]; + currentV = tileVertices[q2 * stride + 5]; + + if (previousV !== 1.0 || currentV !== 1.0) { + console.log('not north?!'); + } + + if (currentU <= previousU) { + console.log('not ascending?!'); + } + } + var mesh = new TerrainMesh( obb.center, typedArray, @@ -755,15 +872,15 @@ define([ var v = (coordinates.y - vmin) / (vmax - vmin); // Ensure that coordinates very near the corners are at the corners. - if (Math.abs(u) < Math.EPSILON8) { + if (Math.abs(u) < Math.EPSILON5) { u = 0.0; - } else if (Math.abs(u - 1.0) < Math.EPSILON8) { + } else if (Math.abs(u - 1.0) < Math.EPSILON5) { u = 1.0; } - if (Math.abs(v) < Math.EPSILON8) { + if (Math.abs(v) < Math.EPSILON5) { v = 0.0; - } else if (Math.abs(v - 1.0) < Math.EPSILON8) { + } else if (Math.abs(v - 1.0) < Math.EPSILON5) { v = 1.0; } @@ -992,20 +1109,28 @@ define([ var previousVertexIndex = tileVertices.length - stride; - // Copy all but the last vertex. + // Copy all except the corner vertices. var i; var u; var v; var lastU = tileVertices[previousVertexIndex + 4]; var lastV = tileVertices[previousVertexIndex + 5]; - for (i = 0; i < vertices.length - stride; i += stride) { + for (i = 0; i < vertices.length; i += stride) { u = vertices[i + 4]; v = vertices[i + 5]; - if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { + if (Math.abs(u - lastU) < CesiumMath.EPSILON5 && Math.abs(v - lastV) < CesiumMath.EPSILON5) { // Vertex is very close to the previous one, so skip it. continue; } + var nearlyEdgeU = Math.abs(u) < CesiumMath.EPSILON5 || Math.abs(u - 1.0) < CesiumMath.EPSILON5; + var nearlyEdgeV = Math.abs(v) < CesiumMath.EPSILON5 || Math.abs(v - 1.0) < CesiumMath.EPSILON5; + + if (nearlyEdgeU && nearlyEdgeV) { + // Corner vertex - skip it. + continue; + } + var end = i + stride; for (var j = i; j < end; ++j) { tileVertices.push(vertices[j]); @@ -1014,22 +1139,6 @@ define([ lastU = u; lastV = v; } - - // Copy the last vertex too if it's _not_ a corner vertex. - var lastVertexStart = i; - u = vertices[lastVertexStart + 4]; - v = vertices[lastVertexStart + 5]; - - if (lastVertexStart < vertices.length && ((u !== 0.0 && u !== 1.0) || (v !== 0.0 && v !== 1.0))) { - if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { - // Overwrite the previous vertex because it's very close to the last one. - tileVertices.length -= stride; - } - - for (; i < vertices.length; ++i) { - tileVertices.push(vertices[i]); - } - } } function getNearestHeightOnEdge(meshes, tiles, isNext, edge, u, v) { @@ -1236,49 +1345,6 @@ define([ } } - function addVerticesToFillTile(edgeDetails, stride, tileVertices) { - var vertices = edgeDetails.vertices; - - // Copy all but the last vertex. - var i; - var u; - var v; - var lastU; - var lastV; - for (i = 0; i < vertices.length - stride; i += stride) { - u = vertices[i + 4]; - v = vertices[i + 5]; - if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { - // Vertex is very close to the previous one, so skip it. - continue; - } - - var end = i + stride; - for (var j = i; j < end; ++j) { - tileVertices.push(vertices[j]); - } - - lastU = u; - lastV = v; - } - - // Copy the last vertex too if it's _not_ a corner vertex. - var lastVertexStart = i; - u = vertices[lastVertexStart + 4]; - v = vertices[lastVertexStart + 5]; - - if (lastVertexStart < vertices.length && ((u !== 0.0 && u !== 1.0) || (v !== 0.0 && v !== 1.0))) { - if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { - // Overwrite the previous vertex because it's very close to the last one. - tileVertices.length -= stride; - } - - for (; i < vertices.length; ++i) { - tileVertices.push(vertices[i]); - } - } - } - var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()]; function computeOccludeePoint(tileProvider, center, rectangle, height, result) { From 33faa199afa91f5be5d4e2155c229cad26ce773e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 13 Sep 2018 22:16:57 +1000 Subject: [PATCH 041/131] Remove unnecessary code and fill tile shading. --- Source/Scene/GlobeSurfaceTileProvider.js | 2 +- Source/Scene/TerrainFillMesh.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 18ca86aeac3..2af7f21002f 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -2048,7 +2048,7 @@ define([ var uniformMapProperties = uniformMap.properties; Cartesian4.clone(initialColor, uniformMapProperties.initialColor); - uniformMapProperties.isFill = surfaceTile.vertexArray === undefined; + //uniformMapProperties.isFill = surfaceTile.vertexArray === undefined; uniformMapProperties.oceanNormalMap = oceanNormalMap; uniformMapProperties.lightingFadeDistance.x = tileProvider.lightingFadeOutDistance; uniformMapProperties.lightingFadeDistance.y = tileProvider.lightingFadeInDistance; diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 10d68d11d3f..6a644db0211 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -1100,7 +1100,7 @@ define([ var edgeDetailsScratch = new TerrainTileEdgeDetails(); function addEdgeMesh(terrainFillMesh, ellipsoid, edgeTile, edgeMesh, tileEdge, stride, tileVertices) { - var terrainMesh = edgeMesh.mesh ? edgeMesh.mesh : edgeMesh; + var terrainMesh = edgeMesh; edgeDetailsScratch.clear(); var edgeDetails = terrainMesh.getEdgeVertices(tileEdge, edgeTile.rectangle, terrainFillMesh.tile.rectangle, ellipsoid, edgeDetailsScratch); @@ -1162,7 +1162,7 @@ define([ continue; } - var terrainMesh = mesh.mesh ? mesh.mesh : mesh; + var terrainMesh = mesh; var indices; switch (edge) { @@ -1194,7 +1194,7 @@ define([ return undefined; } - var terrainMesh = mesh.mesh ? mesh.mesh : mesh; + var terrainMesh = mesh; var indices; switch (edge) { From db79a4fccd2d4a6ad69e44133b809392d3d459c5 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 21 Sep 2018 14:17:44 +1000 Subject: [PATCH 042/131] Fix eslint warnings and heightmap terrain. --- Source/Core/HeightmapTerrainData.js | 8 +- Source/Core/HeightmapTessellator.js | 35 +- Source/Scene/GlobeSurfaceTileProvider.js | 440 +----------------- Source/Scene/TerrainFillMesh.js | 93 +--- Source/Workers/createVerticesFromHeightmap.js | 6 +- .../upsampleQuantizedTerrainMeshSpec.js | 4 +- 6 files changed, 46 insertions(+), 540 deletions(-) diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index 5c92fe6bca5..e73ab99c2f1 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -169,7 +169,7 @@ define([ return this._childTileMask; } } -}); + }); var taskProcessor = new TaskProcessor('createVerticesFromHeightmap'); @@ -250,7 +250,11 @@ define([ result.numberOfAttributes, result.orientedBoundingBox, TerrainEncoding.clone(result.encoding), - exaggeration); + exaggeration, + result.westIndicesSouthToNorth, + result.southIndicesEastToWest, + result.eastIndicesNorthToSouth, + result.northIndicesWestToEast); // Free memory received from server after mesh is created. that._buffer = undefined; diff --git a/Source/Core/HeightmapTessellator.js b/Source/Core/HeightmapTessellator.js index b7604ce29a8..ddbcb11a05a 100644 --- a/Source/Core/HeightmapTessellator.js +++ b/Source/Core/HeightmapTessellator.js @@ -254,8 +254,8 @@ define([ var hMin = Number.POSITIVE_INFINITY; - var arrayWidth = width + (skirtHeight > 0.0 ? 2.0 : 0.0); - var arrayHeight = height + (skirtHeight > 0.0 ? 2.0 : 0.0); + var arrayWidth = width + (skirtHeight > 0.0 ? 2 : 0); + var arrayHeight = height + (skirtHeight > 0.0 ? 2 : 0); var size = arrayWidth * arrayHeight; var positions = new Array(size); var heights = new Array(size); @@ -267,7 +267,7 @@ define([ var startCol = 0; var endCol = width; - if (skirtHeight > 0) { + if (skirtHeight > 0.0) { --startRow; ++endRow; --startCol; @@ -428,6 +428,29 @@ define([ bufferIndex = encoding.encode(vertices, bufferIndex, positions[j], uvs[j], heights[j], undefined, webMercatorTs[j]); } + var westIndicesSouthToNorth; + var southIndicesEastToWest; + var eastIndicesNorthToSouth; + var northIndicesWestToEast; + + if (skirtHeight > 0.0) { + northIndicesWestToEast = []; + southIndicesEastToWest = []; + for (var i1 = 0; i1 < width; ++i1) { + northIndicesWestToEast.push(arrayWidth + 1 + i1); + southIndicesEastToWest.push(arrayWidth * (arrayHeight - 1) - 2 - i1); + } + + westIndicesSouthToNorth = []; + eastIndicesNorthToSouth = []; + for (var i2 = 0; i2 < height; ++i2) { + eastIndicesNorthToSouth.push((i2 + 1) * arrayWidth + width); + westIndicesSouthToNorth.push((height - i2) * arrayWidth + 1); + } + } else { + westIndicesSouthToNorth = southIndicesEastToWest = eastIndicesNorthToSouth = northIndicesWestToEast = []; + } + return { vertices : vertices, maximumHeight : maximumHeight, @@ -435,7 +458,11 @@ define([ encoding : encoding, boundingSphere3D : boundingSphere3D, orientedBoundingBox : orientedBoundingBox, - occludeePointInScaledSpace : occludeePointInScaledSpace + occludeePointInScaledSpace : occludeePointInScaledSpace, + westIndicesSouthToNorth : westIndicesSouthToNorth, + southIndicesEastToWest : southIndicesEastToWest, + eastIndicesNorthToSouth : eastIndicesNorthToSouth, + northIndicesWestToEast : northIndicesWestToEast }; }; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 2af7f21002f..d4e2d8f2296 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1410,444 +1410,6 @@ define([ }; })(); - function findRenderedTiles(startTile, currentFrameNumber, edge, downOnly) { - if (startTile === undefined) { - // There are no tiles North or South of the poles. - return []; - } - - if (startTile._lastSelectionResultFrame !== currentFrameNumber || startTile._lastSelectionResult === TileSelectionResult.KICKED) { - if (downOnly) { - return []; - } - - // This wasn't visited or was visited and then kicked, so walk up to find the closest ancestor that was rendered. - var tile = startTile.parent; - while (tile && tile._lastSelectionResultFrame !== currentFrameNumber) { - tile = tile.parent; - } - - if (tile !== undefined && tile._lastSelectionResult === TileSelectionResult.RENDERED) { - return [tile]; - } - - // No ancestor was rendered. - return []; - } - - if (startTile._lastSelectionResult === TileSelectionResult.RENDERED) { - return [startTile]; - } - - if (startTile._lastSelectionResult === TileSelectionResult.CULLED) { - return []; - } - - // This tile was refined, so find rendered children, if any. - // Return the tiles in clockwise order. - switch (edge) { - case TileEdge.WEST: - return findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge, true)); - case TileEdge.EAST: - return findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge, true)); - case TileEdge.SOUTH: - return findRenderedTiles(startTile.southeastChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.southwestChild, currentFrameNumber, edge, true)); - case TileEdge.NORTH: - return findRenderedTiles(startTile.northwestChild, currentFrameNumber, edge, true).concat(findRenderedTiles(startTile.northeastChild, currentFrameNumber, edge, true)); - default: - throw new DeveloperError('Invalid edge'); - } - } - - function getEdgeVertices(tile, startingTile, currentFrameNumber, tileEdge, result) { - var ellipsoid = tile.tilingScheme.ellipsoid; - var edgeTiles = findRenderedTiles(startingTile, currentFrameNumber, tileEdge); - - tile.edgeTiles = tile.edgeTiles || []; - tile.edgeTiles[tileEdge] = edgeTiles; - - result.clear(); - - for (var i = 0; i < edgeTiles.length; ++i) { - var edgeTile = edgeTiles[i]; - var surfaceTile = edgeTile.data; - if (surfaceTile === undefined) { - continue; - } - - var mesh = surfaceTile.fillMesh; - if (surfaceTile.mesh !== undefined && surfaceTile.vertexArray !== undefined) { - mesh = surfaceTile.mesh; - } - - if (mesh !== undefined) { - var beforeLength = result.vertices.length; - mesh.getEdgeVertices(tileEdge, edgeTile.rectangle, tile.rectangle, ellipsoid, result); - var afterLength = result.vertices.length; - var numberOfVertices = afterLength - beforeLength; - if (surfaceTile.mesh === undefined && numberOfVertices > 27) { - console.log(`${numberOfVertices} from L${edgeTile.level}X${edgeTile.x}Y${edgeTile.y}`); - } - } - } - - return result; - } - - var cartographicScratch = new Cartographic(); - var cartesianScratch = new Cartesian3(); - var normalScratch = new Cartesian3(); - var octEncodedNormalScratch = new Cartesian2(); - - function addCornerVertexIfNecessary(ellipsoid, u, v, longitude, latitude, height, edgeDetails, previousEdgeDetails, hasVertexNormals, hasWebMercatorT, tileVertices) { - var vertices = edgeDetails.vertices; - - if (u === vertices[4] && v === vertices[5]) { - // First vertex is a corner vertex, as expected. - return; - } - - // Can we use the last vertex of the previous edge as the corner vertex? - var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); - var previousVertices = previousEdgeDetails.vertices; - var lastVertexStart = previousVertices.length - stride; - var lastU = previousVertices[lastVertexStart + 4]; - var lastV = previousVertices[lastVertexStart + 5]; - - if (lastU === u && lastV === v) { - for (var i = 0; i < stride; ++i) { - tileVertices.push(previousVertices[lastVertexStart + i]); - } - return; - } - - // Previous edge doesn't contain a suitable vertex either, so fabricate one. - cartographicScratch.longitude = longitude; - cartographicScratch.latitude = latitude; - cartographicScratch.height = height; - ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); - tileVertices.push(cartesianScratch.x, cartesianScratch.y, cartesianScratch.z, height, u, v); - - if (hasWebMercatorT) { - // Identical to v at 0.0 and 1.0. - tileVertices.push(v); - } - - if (hasVertexNormals) { - ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); - AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); - tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); - //tileVertices.push(AttributeCompression.octPackFloat(octEncodedNormalScratch)); - } - } - - function addVerticesToFillTile(edgeDetails, stride, tileVertices) { - var vertices = edgeDetails.vertices; - - // Copy all but the last vertex. - var i; - var u; - var v; - var lastU; - var lastV; - for (i = 0; i < vertices.length - stride; i += stride) { - u = vertices[i + 4]; - v = vertices[i + 5]; - if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { - // Vertex is very close to the previous one, so skip it. - continue; - } - - var end = i + stride; - for (var j = i; j < end; ++j) { - tileVertices.push(vertices[j]); - } - - lastU = u; - lastV = v; - } - - // Copy the last vertex too if it's _not_ a corner vertex. - var lastVertexStart = i; - u = vertices[lastVertexStart + 4]; - v = vertices[lastVertexStart + 5]; - - if (lastVertexStart < vertices.length && ((u !== 0.0 && u !== 1.0) || (v !== 0.0 && v !== 1.0))) { - if (Math.abs(u - lastU) < CesiumMath.EPSILON4 && Math.abs(v - lastV) < CesiumMath.EPSILON4) { - // Overwrite the previous vertex because it's very close to the last one. - tileVertices.length -= stride; - } - - for (; i < vertices.length; ++i) { - tileVertices.push(vertices[i]); - } - } - } - - var westScratch = new TerrainTileEdgeDetails(); - var southScratch = new TerrainTileEdgeDetails(); - var eastScratch = new TerrainTileEdgeDetails(); - var northScratch = new TerrainTileEdgeDetails(); - var tileVerticesScratch = []; - - function createFillTile(tileProvider, tile, frameState) { - //console.log('L' + tile.level + 'X' + tile.x + 'Y' + tile.y); - var start = performance.now(); - - var mesh; - var typedArray; - var indices; - var surfaceTile = tile.data; - // if (surfaceTile.mesh === undefined) { - var quadtree = tileProvider._quadtree; - var levelZeroTiles = quadtree._levelZeroTiles; - var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber; - - var west = getEdgeVertices(tile, tile.findTileToWest(levelZeroTiles), lastSelectionFrameNumber, TileEdge.EAST, westScratch); - var south = getEdgeVertices(tile, tile.findTileToSouth(levelZeroTiles), lastSelectionFrameNumber, TileEdge.NORTH, southScratch); - var east = getEdgeVertices(tile, tile.findTileToEast(levelZeroTiles), lastSelectionFrameNumber, TileEdge.WEST, eastScratch); - var north = getEdgeVertices(tile, tile.findTileToNorth(levelZeroTiles), lastSelectionFrameNumber, TileEdge.SOUTH, northScratch); - - var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; - var hasWebMercatorT = true; // TODO - var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); - - var minimumHeight = Number.MAX_VALUE; - var maximumHeight = -Number.MAX_VALUE; - var hasAnyVertices = false; - - if (west.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, west.minimumHeight); - maximumHeight = Math.max(maximumHeight, west.maximumHeight); - hasAnyVertices = true; - } - - if (south.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, south.minimumHeight); - maximumHeight = Math.max(maximumHeight, south.maximumHeight); - hasAnyVertices = true; - } - - if (east.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, east.minimumHeight); - maximumHeight = Math.max(maximumHeight, east.maximumHeight); - hasAnyVertices = true; - } - - if (north.vertices.length > 0) { - minimumHeight = Math.min(minimumHeight, north.minimumHeight); - maximumHeight = Math.max(maximumHeight, north.maximumHeight); - hasAnyVertices = true; - } - - if (!hasAnyVertices) { - var tileBoundingRegion = surfaceTile.tileBoundingRegion; - minimumHeight = tileBoundingRegion.minimumHeight; - maximumHeight = tileBoundingRegion.maximumHeight; - } - - var middleHeight = (minimumHeight + maximumHeight) * 0.5; - - var tileVertices = tileVerticesScratch; - tileVertices.length = 0; - - var ellipsoid = tile.tilingScheme.ellipsoid; - var rectangle = tile.rectangle; - - var northwestIndex = 0; - addCornerVertexIfNecessary(ellipsoid, 0.0, 1.0, rectangle.west, rectangle.north, middleHeight, west, north, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(west, stride, tileVertices); - var southwestIndex = tileVertices.length / stride; - addCornerVertexIfNecessary(ellipsoid, 0.0, 0.0, rectangle.west, rectangle.south, middleHeight, south, west, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(south, stride, tileVertices); - var southeastIndex = tileVertices.length / stride; - addCornerVertexIfNecessary(ellipsoid, 1.0, 0.0, rectangle.east, rectangle.south, middleHeight, east, south, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(east, stride, tileVertices); - var northeastIndex = tileVertices.length / stride; - addCornerVertexIfNecessary(ellipsoid, 1.0, 1.0, rectangle.east, rectangle.north, middleHeight, north, east, hasVertexNormals, hasWebMercatorT, tileVertices); - addVerticesToFillTile(north, stride, tileVertices); - - // Add a single vertex at the center of the tile. - var obb = OrientedBoundingBox.fromRectangle(tile.rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); - var center = obb.center; - - ellipsoid.cartesianToCartographic(center, cartographicScratch); - cartographicScratch.height = middleHeight; - var centerVertexPosition = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); - - tileVertices.push(centerVertexPosition.x, centerVertexPosition.y, centerVertexPosition.z, middleHeight); - tileVertices.push((cartographicScratch.longitude - rectangle.west) / (rectangle.east - rectangle.west)); - tileVertices.push((cartographicScratch.latitude - rectangle.south) / (rectangle.north - rectangle.south)); - - if (hasWebMercatorT) { - var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); - var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); - tileVertices.push((WebMercatorProjection.geodeticLatitudeToMercatorAngle(cartographicScratch.latitude) - southMercatorY) * oneOverMercatorHeight); - } - - if (hasVertexNormals) { - ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); - AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); - tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); - } - - var vertexCount = tileVertices.length / stride; - indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex - var centerIndex = vertexCount - 1; - - var indexOut = 0; - var i; - for (i = 0; i < vertexCount - 2; ++i) { - indices[indexOut++] = centerIndex; - indices[indexOut++] = i; - indices[indexOut++] = i + 1; - } - - indices[indexOut++] = centerIndex; - indices[indexOut++] = i; - indices[indexOut++] = 0; - - var westIndicesSouthToNorth = []; - for (i = southwestIndex; i >= northwestIndex; --i) { - westIndicesSouthToNorth.push(i); - } - - var southIndicesEastToWest = []; - for (i = southeastIndex; i >= southwestIndex; --i) { - southIndicesEastToWest.push(i); - } - - var eastIndicesNorthToSouth = []; - for (i = northeastIndex; i >= southeastIndex; --i) { - eastIndicesNorthToSouth.push(i); - } - - var northIndicesWestToEast = []; - northIndicesWestToEast.push(0); - for (i = centerIndex - 1; i >= northeastIndex; --i) { - northIndicesWestToEast.push(i); - } - - var packedStride = hasVertexNormals ? stride - 1 : stride; // normal is packed into 1 float - typedArray = new Float32Array(vertexCount * packedStride); - - for (i = 0; i < vertexCount; ++i) { - var read = i * stride; - var write = i * packedStride; - typedArray[write++] = tileVertices[read++] - center.x; - typedArray[write++] = tileVertices[read++] - center.y; - typedArray[write++] = tileVertices[read++] - center.z; - typedArray[write++] = tileVertices[read++]; - typedArray[write++] = tileVertices[read++]; - typedArray[write++] = tileVertices[read++]; - - if (hasWebMercatorT) { - typedArray[write++] = tileVertices[read++]; - } - - if (hasVertexNormals) { - typedArray[write++] = AttributeCompression.octPackFloat(Cartesian2.fromElements(tileVertices[read++], tileVertices[read++], octEncodedNormalScratch)); - } - } - - var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, hasVertexNormals, hasWebMercatorT); - encoding.center = center; - - mesh = new TerrainMesh( - obb.center, - typedArray, - indices, - minimumHeight, - maximumHeight, - BoundingSphere.fromOrientedBoundingBox(obb), - computeOccludeePoint(tileProvider, center, rectangle, maximumHeight), - encoding.getStride(), - obb, - encoding, - frameState.terrainExaggeration, - westIndicesSouthToNorth, - southIndicesEastToWest, - eastIndicesNorthToSouth, - northIndicesWestToEast - ); - - surfaceTile.fillMesh = mesh; - // } else { - // mesh = surfaceTile.mesh; - // typedArray = mesh.vertices; - // indices = mesh.indices; - // } - - var context = frameState.context; - - if (surfaceTile.fillVertexArray !== undefined) { - surfaceTile.fillVertexArray.destroy(); - surfaceTile.fillVertexArray = undefined; - } - - var buffer = Buffer.createVertexBuffer({ - context : context, - typedArray : typedArray, - usage : BufferUsage.STATIC_DRAW - }); - var attributes = mesh.encoding.getAttributes(buffer); - - var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; - var indexBuffer = Buffer.createIndexBuffer({ - context : context, - typedArray : mesh.indices, - usage : BufferUsage.STATIC_DRAW, - indexDatatype : indexDatatype - }); - - surfaceTile.fillVertexArray = new VertexArray({ - context : context, - attributes : attributes, - indexBuffer : indexBuffer - }); - - var tileImageryCollection = surfaceTile.imagery; - - var len; - if (tileImageryCollection.length === 0) { - var imageryLayerCollection = tileProvider._imageryLayers; - var terrainProvider = tileProvider.terrainProvider; - for (i = 0, len = imageryLayerCollection.length; i < len; ++i) { - var layer = imageryLayerCollection.get(i); - if (layer.show) { - layer._createTileImagerySkeletons(tile, terrainProvider); - } - } - } - - for (i = 0, len = tileImageryCollection.length; i < len; ++i) { - var tileImagery = tileImageryCollection[i]; - if (!defined(tileImagery.loadingImagery)) { - continue; - } - - if (tileImagery.loadingImagery.state === ImageryState.PLACEHOLDER) { - var imageryLayer = tileImagery.loadingImagery.imageryLayer; - if (imageryLayer.imageryProvider.ready) { - // Remove the placeholder and add the actual skeletons (if any) - // at the same position. Then continue the loop at the same index. - tileImagery.freeResources(); - tileImageryCollection.splice(i, 1); - imageryLayer._createTileImagerySkeletons(tile, tileProvider.terrainProvider, i); - --i; - len = tileImageryCollection.length; - continue; - } - } - - tileImagery.processStateMachine(tile, frameState, true); - } - - var stop = performance.now(); - - //console.log('fill: ' + (stop - start)); - } - var otherPassesInitialColor = new Cartesian4(0.0, 0.0, 0.0, 0.0); function addDrawCommandsForTile(tileProvider, tile, frameState, subset) { @@ -1878,7 +1440,7 @@ define([ // } else if (surfaceTile.fillMesh.changedThisFrame) { // console.log(`Frame ${frameState.frameNumber} L${tile.level}X${tile.x}Y${tile.y} changed`); // } - if (surfaceTile.fill == undefined) { + if (surfaceTile.fill === undefined) { // No fill was created for this tile, probably because this tile is not connected to // any renderable tiles. So create a simple tile in the middle of the tile's possible // height range. diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 6a644db0211..2ac9d4fd4ef 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -113,7 +113,7 @@ define([ while (tile !== undefined) { var tileToWest = tile.findTileToWest(levelZeroTiles); - var tileToSouth = tile.findTileToSouth(levelZeroTiles) + var tileToSouth = tile.findTileToSouth(levelZeroTiles); var tileToEast = tile.findTileToEast(levelZeroTiles); var tileToNorth = tile.findTileToNorth(levelZeroTiles); visitRenderedTiles(tileProvider, frameState, tile, tileToWest, lastSelectionFrameNumber, TileEdge.EAST, false, traversalQueue); @@ -456,10 +456,6 @@ define([ } } - var westScratch = new TerrainTileEdgeDetails(); - var southScratch = new TerrainTileEdgeDetails(); - var eastScratch = new TerrainTileEdgeDetails(); - var northScratch = new TerrainTileEdgeDetails(); var tileVerticesScratch = []; var cartographicScratch = new Cartographic(); var cartesianScratch = new Cartesian3(); @@ -471,9 +467,6 @@ define([ var surfaceTile = tile.data; var fill = surfaceTile.fill; - var quadtree = tileProvider._quadtree; - var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber; - var tileVertices = tileVerticesScratch; tileVertices.length = 0; @@ -817,48 +810,6 @@ define([ } } - function getEdgeVertices(tile, edgeTiles, edgeMeshes, currentFrameNumber, tileEdge, result) { - var ellipsoid = tile.tilingScheme.ellipsoid; - - result.clear(); - - // TODO: add first corner vertex if it exists. - // TODO: if a corner is missing, fill it in. When we add a corner, we create an edge - // (or two) that didn't exist before. We need to propagate that edge to adjacent tile(s) - // but be careful not to trigger regeneration of those adjacent tiles. We can do - // that by updating the adjacent tile's list of edge meshes without setting the - // changed flag. - // When creating a new fill mesh, immediately adjust all adjacent fill tiles to know they're in sync - // with that new mesh (because the new mesh was purposely created to be in sync!). - - for (var i = 0; i < edgeMeshes.length; ++i) { - var edgeTile = edgeTiles[i]; - var surfaceTile = edgeTile.data; - if (surfaceTile === undefined) { - continue; - } - - var edgeMesh = edgeMeshes[i]; - if (edgeMesh !== undefined) { - var beforeLength = result.vertices.length; - edgeMesh.getEdgeVertices(tileEdge, edgeTile.rectangle, tile.rectangle, ellipsoid, result); - var afterLength = result.vertices.length; - var numberOfVertices = afterLength - beforeLength; - if (surfaceTile.mesh === undefined && numberOfVertices > 27) { - //console.log(`${numberOfVertices} from L${edgeTile.level}X${edgeTile.x}Y${edgeTile.y}`); - } - } - } - - // TODO: add last corner vertex if it exists - - return result; - } - - function transformTextureCoordinate(toMin, toMax, fromValue) { - return (fromValue - toMin) / (toMax - toMin); - } - function transformTextureCoordinates(sourceRectangle, targetRectangle, coordinates, result) { var sourceWidth = sourceRectangle.east - sourceRectangle.west; var umin = (targetRectangle.west - sourceRectangle.west) / sourceWidth; @@ -1303,48 +1254,6 @@ define([ return false; } - function addCornerVertexIfNecessary(ellipsoid, u, v, longitude, latitude, height, edgeDetails, previousEdgeDetails, hasVertexNormals, hasWebMercatorT, tileVertices) { - var vertices = edgeDetails.vertices; - - if (u === vertices[4] && v === vertices[5]) { - // First vertex is a corner vertex, as expected. - return; - } - - // Can we use the last vertex of the previous edge as the corner vertex? - var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); - var previousVertices = previousEdgeDetails.vertices; - var lastVertexStart = previousVertices.length - stride; - var lastU = previousVertices[lastVertexStart + 4]; - var lastV = previousVertices[lastVertexStart + 5]; - - if (lastU === u && lastV === v) { - for (var i = 0; i < stride; ++i) { - tileVertices.push(previousVertices[lastVertexStart + i]); - } - return; - } - - // Previous edge doesn't contain a suitable vertex either, so fabricate one. - cartographicScratch.longitude = longitude; - cartographicScratch.latitude = latitude; - cartographicScratch.height = height; - ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); - tileVertices.push(cartesianScratch.x, cartesianScratch.y, cartesianScratch.z, height, u, v); - - if (hasWebMercatorT) { - // Identical to v at 0.0 and 1.0. - tileVertices.push(v); - } - - if (hasVertexNormals) { - ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); - AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); - tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); - //tileVertices.push(AttributeCompression.octPackFloat(octEncodedNormalScratch)); - } - } - var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()]; function computeOccludeePoint(tileProvider, center, rectangle, height, result) { diff --git a/Source/Workers/createVerticesFromHeightmap.js b/Source/Workers/createVerticesFromHeightmap.js index 21408411117..26f55c5be16 100644 --- a/Source/Workers/createVerticesFromHeightmap.js +++ b/Source/Workers/createVerticesFromHeightmap.js @@ -36,7 +36,11 @@ define([ boundingSphere3D : statistics.boundingSphere3D, orientedBoundingBox : statistics.orientedBoundingBox, occludeePointInScaledSpace : statistics.occludeePointInScaledSpace, - encoding : statistics.encoding + encoding : statistics.encoding, + westIndicesSouthToNorth : statistics.westIndicesSouthToNorth, + southIndicesEastToWest : statistics.southIndicesEastToWest, + eastIndicesNorthToSouth : statistics.eastIndicesNorthToSouth, + northIndicesWestToEast : statistics.northIndicesWestToEast }; } diff --git a/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js b/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js index cc71808ce67..0592bb407f6 100644 --- a/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js +++ b/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js @@ -6,7 +6,6 @@ defineSuite([ createWorldTerrain) { 'use strict'; - it('time', function() { var worldTerrain = createWorldTerrain({ requestWaterMask: true, @@ -59,7 +58,8 @@ defineSuite([ }, []); } var stop = performance.now(); - alert(stop - start); + console.log(stop - start); + //alert(stop - start); }); }); }); From 240299710bae0687cf5158f1e846dc353ff48c6a Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 23 Sep 2018 21:11:57 +1000 Subject: [PATCH 043/131] Remmove ability to render partial tiles. The TILE_LIMIT_RECTANGLE functionality is equivalent. --- Source/Scene/GlobeSurfaceShaderSet.js | 9 +---- Source/Scene/GlobeSurfaceTileProvider.js | 51 ++++-------------------- Source/Shaders/GlobeFS.glsl | 15 ------- 3 files changed, 9 insertions(+), 66 deletions(-) diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 0e2f99344f5..34765974e21 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -66,7 +66,7 @@ define([ return useWebMercatorProjection ? get2DYPositionFractionMercatorProjection : get2DYPositionFractionGeographicProjection; } - GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, clippingPlanes, clippedByBoundaries, renderPartialTile) { + GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, clippingPlanes, clippedByBoundaries) { var quantization = 0; var quantizationDefine = ''; @@ -110,8 +110,7 @@ define([ (applySplit << 15) | (enableClippingPlanes << 16) | (vertexLogDepth << 17) | - (cartographicLimitRectangleFlag << 18) | - (renderPartialTile << 19); + (cartographicLimitRectangleFlag << 18); var currentClippingShaderState = 0; if (defined(clippingPlanes)) { @@ -198,10 +197,6 @@ define([ fs.defines.push('ENABLE_CLIPPING_PLANES'); } - if (renderPartialTile) { - fs.defines.push('RENDER_PARTIAL_TILE'); - } - var computeDayColor = '\ vec4 computeDayColor(vec4 initialColor, vec3 textureCoordinates)\n\ {\n\ diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 7502ee150ec..2f627d35eb9 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -125,12 +125,6 @@ define([ */ RENDER_NOTHING: 0, - /** - * Render a subset of the closest renderable ancestor. This is cheap on the CPU, but can be very expensive in terms - * of GPU fill rate. It also leads to cracking due to missing skirts. - */ - RENDER_ANCESTOR_SUBSET: 1, - /** * Create a very simple tile to fill the space by matching the heights of adjacent tiles on the edges. This is * cheaper on the CPU than a full upsample and avoids cracks. But it's less representative of the real @@ -1269,9 +1263,6 @@ define([ u_minimumBrightness : function() { return frameState.fog.minimumBrightness; }, - u_textureCoordinateSubset : function() { - return this.properties.textureCoordinateSubset; - }, // make a separate object so that changes to the properties are seen on // derived commands that combine another uniform map with this one. @@ -1310,7 +1301,6 @@ define([ scaleAndBias : new Matrix4(), clippingPlanesEdgeColor : Color.clone(Color.WHITE), clippingPlanesEdgeWidth : 0.0, - textureCoordinateSubset : new Cartesian4(), localizedCartographicLimitRectangle : new Cartesian4() } @@ -1453,34 +1443,13 @@ define([ var otherPassesInitialColor = new Cartesian4(0.0, 0.0, 0.0, 0.0); - function addDrawCommandsForTile(tileProvider, tile, frameState, subset) { + function addDrawCommandsForTile(tileProvider, tile, frameState) { var surfaceTile = tile.data; if (surfaceTile.renderableTile !== undefined) { - // We can't render this tile yet, so instead render a subset of our closest renderable ancestor. + // We can't render this tile yet, so do something else, depending on our missing tile strategy. var missingTileStrategy = tileProvider.missingTileStrategy; - if (missingTileStrategy === MissingTileStrategy.RENDER_ANCESTOR_SUBSET) { - addDrawCommandsForTile(tileProvider, surfaceTile.renderableTile, frameState, surfaceTile.renderableTileSubset); - return; - } else if (missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE) { - // if (surfaceTile.fillMesh === undefined) { - // if (tile.renderable) { - // console.log(`Frame ${frameState.frameNumber} L${tile.level}X${tile.x}Y${tile.y} renderable`); - // } else { - // console.log(`Frame ${frameState.frameNumber} L${tile.level}X${tile.x}Y${tile.y} no mesh`); - // } - - // for (var i = 0; i < tileProvider._quadtree._tilesToRender.length; ++i) { - // var foo = tileProvider._quadtree._tilesToRender[i]; - // if (foo.data && foo.data.fillMesh) { - // foo.data.fillMesh.visitedFrame = undefined; - // } - // } - - // TerrainFillMesh.updateFillTiles(tileProvider, tileProvider._quadtree._tilesToRender, frameState); - // } else if (surfaceTile.fillMesh.changedThisFrame) { - // console.log(`Frame ${frameState.frameNumber} L${tile.level}X${tile.x}Y${tile.y} changed`); - // } + if (missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE) { if (surfaceTile.fill === undefined) { // No fill was created for this tile, probably because this tile is not connected to // any renderable tiles. So create a simple tile in the middle of the tile's possible @@ -1490,17 +1459,15 @@ define([ surfaceTile.fill.changedThisFrame = true; } surfaceTile.fill.update(tileProvider, frameState); - //return; - //createFillTile(tileProvider, tile, frameState); } else { return; } } //>>includeStart('debug', pragmas.debug); - // if (!tile.renderable) { - // throw new DeveloperError('A rendered tile is not renderable, this should not be possible.'); - // } + if (!tile.renderable) { + throw new DeveloperError('A rendered tile is not renderable, this should not be possible.'); + } //>>includeEnd('debug'); var creditDisplay = frameState.creditDisplay; @@ -1666,10 +1633,6 @@ define([ uniformMapProperties.southMercatorYAndOneOverHeight.x = southMercatorY; uniformMapProperties.southMercatorYAndOneOverHeight.y = oneOverMercatorHeight; - if (subset !== undefined) { - Cartesian4.clone(subset, uniformMapProperties.textureCoordinateSubset); - } - // Convert tile limiter rectangle from cartographic to texture space using the tileRectangle. var localizedCartographicLimitRectangle = localizedCartographicLimitRectangleScratch; var cartographicLimitRectangle = clipRectangleAntimeridian(tile.rectangle, tileProvider.cartographicLimitRectangle); @@ -1786,7 +1749,7 @@ define([ uniformMap = combine(uniformMap, tileProvider.uniformMap); } - command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, clippingPlanes, surfaceTile.clippedByBoundaries, subset !== undefined); + command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, clippingPlanes, surfaceTile.clippedByBoundaries); command.castShadows = castShadows; command.receiveShadows = receiveShadows; command.renderState = renderState; diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index ac1e6bc5dba..fb295540b40 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -66,10 +66,6 @@ uniform vec4 u_clippingPlanesEdgeStyle; uniform float u_minimumBrightness; #endif -#ifdef RENDER_PARTIAL_TILE -uniform vec4 u_textureCoordinateSubset; -#endif - varying vec3 v_positionMC; varying vec3 v_positionEC; varying vec3 v_textureCoordinates; @@ -164,17 +160,6 @@ vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat void main() { -#ifdef RENDER_PARTIAL_TILE - if (v_textureCoordinates.x < u_textureCoordinateSubset.x || - v_textureCoordinates.y < u_textureCoordinateSubset.y || - v_textureCoordinates.x > u_textureCoordinateSubset.z || - v_textureCoordinates.y > u_textureCoordinateSubset.w) - { - discard; - } -#endif - - #ifdef TILE_LIMIT_RECTANGLE if (v_textureCoordinates.x < u_cartographicLimitRectangle.x || u_cartographicLimitRectangle.z < v_textureCoordinates.x || v_textureCoordinates.y < u_cartographicLimitRectangle.y || u_cartographicLimitRectangle.w < v_textureCoordinates.y) From 7b1ba2215187e0c8f1b994bd3afd17d5e4fcd15b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 23 Sep 2018 21:27:37 +1000 Subject: [PATCH 044/131] Remove bad assertion. --- Source/Scene/GlobeSurfaceTileProvider.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 2f627d35eb9..c8f9f95e4be 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1464,12 +1464,6 @@ define([ } } - //>>includeStart('debug', pragmas.debug); - if (!tile.renderable) { - throw new DeveloperError('A rendered tile is not renderable, this should not be possible.'); - } - //>>includeEnd('debug'); - var creditDisplay = frameState.creditDisplay; var terrainData = surfaceTile.terrainData; From 94067ff7d527ca3332874ce75bb2e7ac5faecdc1 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 23 Sep 2018 22:07:47 +1000 Subject: [PATCH 045/131] Optional fill tile highlighting. --- Source/Scene/GlobeSurfaceShaderSet.js | 9 +++++++-- Source/Scene/GlobeSurfaceTileProvider.js | 21 ++++++++++++++++----- Source/Shaders/GlobeFS.glsl | 12 +++++++----- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 34765974e21..3243f8b5a74 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -66,7 +66,7 @@ define([ return useWebMercatorProjection ? get2DYPositionFractionMercatorProjection : get2DYPositionFractionGeographicProjection; } - GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, clippingPlanes, clippedByBoundaries) { + GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, clippingPlanes, clippedByBoundaries, highlightFillTile) { var quantization = 0; var quantizationDefine = ''; @@ -110,7 +110,8 @@ define([ (applySplit << 15) | (enableClippingPlanes << 16) | (vertexLogDepth << 17) | - (cartographicLimitRectangleFlag << 18); + (cartographicLimitRectangleFlag << 18) | + (highlightFillTile << 19); var currentClippingShaderState = 0; if (defined(clippingPlanes)) { @@ -197,6 +198,10 @@ define([ fs.defines.push('ENABLE_CLIPPING_PLANES'); } + if (highlightFillTile) { + fs.defines.push('HIGHLIGHT_FILL_TILE'); + } + var computeDayColor = '\ vec4 computeDayColor(vec4 initialColor, vec3 textureCoordinates)\n\ {\n\ diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index c8f9f95e4be..c34d9c46af3 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -179,6 +179,13 @@ define([ */ this.missingTileStrategy = MissingTileStrategy.CREATE_FILL_TILE; + /** + * The color to use to highlight fill tiles. If undefined, fill tiles are not + * highlighted at all. The alpha value is used to alpha blend with the tile's + * actual color. + */ + this.fillHighlightColor = undefined; + this._quadtree = undefined; this._terrainProvider = options.terrainProvider; this._imageryLayers = options.imageryLayers; @@ -1154,8 +1161,8 @@ define([ u_initialColor : function() { return this.properties.initialColor; }, - u_isFill : function() { - return this.properties.isFill; + u_fillHighlightColor : function() { + return this.properties.fillHighlightColor; }, u_zoomedOutOceanSpecularIntensity : function() { return this.properties.zoomedOutOceanSpecularIntensity; @@ -1268,7 +1275,7 @@ define([ // derived commands that combine another uniform map with this one. properties : { initialColor : new Cartesian4(0.0, 0.0, 0.5, 1.0), - isFill : false, + fillHighlightColor : new Color(0.0, 0.0, 0.0, 0.0), zoomedOutOceanSpecularIntensity : 0.5, oceanNormalMap : undefined, lightingFadeDistance : new Cartesian2(6500000.0, 9000000.0), @@ -1612,12 +1619,16 @@ define([ var uniformMapProperties = uniformMap.properties; Cartesian4.clone(initialColor, uniformMapProperties.initialColor); - //uniformMapProperties.isFill = surfaceTile.vertexArray === undefined; uniformMapProperties.oceanNormalMap = oceanNormalMap; uniformMapProperties.lightingFadeDistance.x = tileProvider.lightingFadeOutDistance; uniformMapProperties.lightingFadeDistance.y = tileProvider.lightingFadeInDistance; uniformMapProperties.zoomedOutOceanSpecularIntensity = tileProvider.zoomedOutOceanSpecularIntensity; + var highlightFillTile = !defined(surfaceTile.vertexArray) && defined(tileProvider.fillHighlightColor) && tileProvider.fillHighlightColor.alpha > 0.0; + if (highlightFillTile) { + Color.clone(tileProvider.fillHighlightColor, uniformMapProperties.fillHighlightColor); + } + uniformMapProperties.center3D = mesh.center; Cartesian3.clone(rtc, uniformMapProperties.rtc); @@ -1743,7 +1754,7 @@ define([ uniformMap = combine(uniformMap, tileProvider.uniformMap); } - command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, clippingPlanes, surfaceTile.clippedByBoundaries); + command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, clippingPlanes, surfaceTile.clippedByBoundaries, highlightFillTile); command.castShadows = castShadows; command.receiveShadows = receiveShadows; command.renderState = renderState; diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index fb295540b40..01a5449df82 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -1,6 +1,5 @@ //#define SHOW_TILE_BOUNDARIES uniform vec4 u_initialColor; -uniform bool u_isFill; #if TEXTURE_UNITS > 0 uniform sampler2D u_dayTextures[TEXTURE_UNITS]; @@ -66,6 +65,10 @@ uniform vec4 u_clippingPlanesEdgeStyle; uniform float u_minimumBrightness; #endif +#ifdef HIGHLIGHT_FILL_TILE +uniform vec4 u_fillHighlightColor; +#endif + varying vec3 v_positionMC; varying vec3 v_positionEC; varying vec3 v_textureCoordinates; @@ -266,10 +269,9 @@ void main() } #endif - if (u_isFill) - { - finalColor = vec4(mix(vec3(0.0), finalColor.rgb, 0.25), finalColor.a); - } +#ifdef HIGHLIGHT_FILL_TILE + finalColor = vec4(mix(finalColor.rgb, u_fillHighlightColor.rgb, u_fillHighlightColor.a), finalColor.a); +#endif #ifdef FOG const float fExposure = 2.0; From 2df6dcc1cd4ab56a0eecbfd4a05de62ea5222ad8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 5 Oct 2018 14:46:34 +1000 Subject: [PATCH 046/131] Add Terrain Tweaks Sandcastle example. --- .../gallery/development/Terrain Tweaks.html | 98 +++++++++++++++++++ Source/Scene/QuadtreePrimitive.js | 4 +- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 Apps/Sandcastle/gallery/development/Terrain Tweaks.html diff --git a/Apps/Sandcastle/gallery/development/Terrain Tweaks.html b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html new file mode 100644 index 00000000000..cb35e70295d --- /dev/null +++ b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html @@ -0,0 +1,98 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + + + + + + + + + + + + + +
Loading Descendant Limit + +
Preload Ancestors + +
Preload Siblings + +
+
+ + + diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 60ee8d87b1c..4052c2d0bf1 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -157,7 +157,9 @@ define([ /** * Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded. - * Setting this to true + * Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even + * if they are culled. Setting this to true may provide a better panning experience at the + * cost of loading more tiles. */ this.preloadSiblings = false; From 4eebbbb856ca834c0ccdb06676bf9fbda782e6ec Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 5 Oct 2018 15:21:33 +1000 Subject: [PATCH 047/131] Add fill tile highlight option to Terrain Tweaks.html. --- .../gallery/development/Terrain Tweaks.html | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Apps/Sandcastle/gallery/development/Terrain Tweaks.html b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html index cb35e70295d..843b188a5f7 100644 --- a/Apps/Sandcastle/gallery/development/Terrain Tweaks.html +++ b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html @@ -55,6 +55,13 @@ + + Fill Tile Highlight + + + + + @@ -67,7 +74,9 @@ var viewModel = { loadingDescendantLimit: viewer.scene.globe._surface.loadingDescendantLimit, preloadAncestors: viewer.scene.globe._surface.preloadAncestors, - preloadSiblings: viewer.scene.globe._surface.preloadSiblings + preloadSiblings: viewer.scene.globe._surface.preloadSiblings, + fillHighlightColor: Cesium.defined(viewer.scene.globe._surface._tileProvider.fillHighlightColor) ? viewer.scene.globe._surface._tileProvider.fillHighlightColor.toCssColorString() : 'rgba(255, 255, 0, 0.5)', + fillHighlightEnabled: Cesium.defined(viewer.scene.globe._surface._tileProvider.fillHighlightColor) }; Cesium.knockout.track(viewModel); @@ -85,6 +94,21 @@ viewer.scene.globe._surface.preloadSiblings = newValue; }); +function updateFillHighlight() { + if (viewModel.fillHighlightEnabled) { + viewer.scene.globe._surface._tileProvider.fillHighlightColor = Cesium.Color.fromCssColorString(viewModel.fillHighlightColor); + } else { + viewer.scene.globe._surface._tileProvider.fillHighlightColor = undefined; + } +} + +Cesium.knockout.getObservable(viewModel, 'fillHighlightEnabled').subscribe(function(newValue) { + updateFillHighlight(); +}); +Cesium.knockout.getObservable(viewModel, 'fillHighlightColor').subscribe(function(newValue) { + updateFillHighlight(); +}); + //Sandcastle_End Sandcastle.finishedLoading(); } From 955c6abbebda48cfc7786e7950aa6e516e98800b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 5 Oct 2018 22:01:24 +1000 Subject: [PATCH 048/131] Move comparison funcs to CesiumMath, other cleanup. --- Source/Core/Math.js | 102 +++++++++++++++++++++++++++++++ Source/Scene/TerrainFillMesh.js | 78 +++++------------------- Specs/Core/MathSpec.js | 104 ++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 63 deletions(-) diff --git a/Source/Core/Math.js b/Source/Core/Math.js index 04494d6484e..c3c2174221d 100644 --- a/Source/Core/Math.js +++ b/Source/Core/Math.js @@ -583,6 +583,108 @@ define([ return absDiff <= absoluteEpsilon || absDiff <= relativeEpsilon * Math.max(Math.abs(left), Math.abs(right)); }; + /** + * Determines if the left value is less than the right value. If the two values are within + * absoluteEpsilon of each other, they are considered equal and this function returns false. + * + * @param {Number} left The first number to compare. + * @param {Number} second The second number to compare. + * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison. + * @returns {Boolean} true if left is less than right by more than + * absoluteEpsilon. false if left is greater or if the two + * values are nearly equal. + */ + CesiumMath.leftIsLessThanRight = function(left, right, absoluteEpsilon) { + //>>includeStart('debug', pragmas.debug); + if (!defined(left)) { + throw new DeveloperError('first is required.'); + } + if (!defined(right)) { + throw new DeveloperError('second is required.'); + } + if (!defined(absoluteEpsilon)) { + throw new DeveloperError('relativeEpsilon is required.'); + } + //>>includeEnd('debug'); + return left - right < -absoluteEpsilon; + }; + + /** + * Determines if the left value is less than or equal to the right value. If the two values are within + * absoluteEpsilon of each other, they are considered equal and this function returns true. + * + * @param {Number} left The first number to compare. + * @param {Number} second The second number to compare. + * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison. + * @returns {Boolean} true if left is less than right or if the + * the values are nearly equal. + */ + CesiumMath.leftIsLessThanOrEqualToRight = function(left, right, absoluteEpsilon) { + //>>includeStart('debug', pragmas.debug); + if (!defined(left)) { + throw new DeveloperError('first is required.'); + } + if (!defined(right)) { + throw new DeveloperError('second is required.'); + } + if (!defined(absoluteEpsilon)) { + throw new DeveloperError('relativeEpsilon is required.'); + } + //>>includeEnd('debug'); + return left - right < absoluteEpsilon; + }; + + /** + * Determines if the left value is greater the right value. If the two values are within + * absoluteEpsilon of each other, they are considered equal and this function returns false. + * + * @param {Number} left The first number to compare. + * @param {Number} second The second number to compare. + * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison. + * @returns {Boolean} true if left is greater than right by more than + * absoluteEpsilon. false if left is less or if the two + * values are nearly equal. + */ + CesiumMath.leftIsGreaterThanRight = function(left, right, absoluteEpsilon) { + //>>includeStart('debug', pragmas.debug); + if (!defined(left)) { + throw new DeveloperError('first is required.'); + } + if (!defined(right)) { + throw new DeveloperError('second is required.'); + } + if (!defined(absoluteEpsilon)) { + throw new DeveloperError('relativeEpsilon is required.'); + } + //>>includeEnd('debug'); + return left - right > absoluteEpsilon; + }; + + /** + * Determines if the left value is greater than or equal to the right value. If the two values are within + * absoluteEpsilon of each other, they are considered equal and this function returns true. + * + * @param {Number} left The first number to compare. + * @param {Number} second The second number to compare. + * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison. + * @returns {Boolean} true if left is greater than right or if the + * the values are nearly equal. + */ + CesiumMath.leftIsGreaterThanOrEqualToRight = function(left, right, absoluteEpsilon) { + //>>includeStart('debug', pragmas.debug); + if (!defined(left)) { + throw new DeveloperError('first is required.'); + } + if (!defined(right)) { + throw new DeveloperError('second is required.'); + } + if (!defined(absoluteEpsilon)) { + throw new DeveloperError('relativeEpsilon is required.'); + } + //>>includeEnd('debug'); + return left - right > -absoluteEpsilon; + }; + var factorials = [1]; /** diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 2ac9d4fd4ef..5a5a45e7bb3 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -83,6 +83,8 @@ define([ return undefined; }; + var traversalQueueScratch = new Queue(); + TerrainFillMesh.updateFillTiles = function(tileProvider, renderedTiles, frameState) { // We want our fill tiles to look natural, which means they should align perfectly with // adjacent loaded tiles, and their edges that are not adjacent to loaded tiles should have @@ -100,8 +102,10 @@ define([ var levelZeroTiles = quadtree._levelZeroTiles; var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber; - var traversalQueue = new Queue(); + var traversalQueue = traversalQueueScratch; + traversalQueue.clear(); + // Add the tiles with real geometry to the traversal queue. for (var i = 0; i < renderedTiles.length; ++i) { var renderedTile = renderedTiles[i]; if (defined(renderedTile.data.vertexArray)) { @@ -142,7 +146,7 @@ define([ var tile = startTile; while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || tile._lastSelectionResult === TileSelectionResult.KICKED || tile._lastSelectionResult === TileSelectionResult.CULLED)) { - // This wasn't visited or it was visited and then kicked, so walk up to find the closest ancestor that was rendered. + // This tile wasn't visited or it was visited and then kicked, so walk up to find the closest ancestor that was rendered. // We also walk up if the tile was culled, because if siblings were kicked an ancestor may have been rendered. if (downOnly) { return; @@ -248,6 +252,7 @@ define([ var sourceMesh = sourceTile.data.mesh; if (sourceMesh === undefined) { + // Source is a fill, create/update it if necessary. var sourceFill = sourceTile.data.fill; if (sourceFill.changedThisFrame) { createFillMesh(tileProvider, frameState, sourceTile); @@ -278,37 +283,21 @@ define([ break; // Corners are simpler. case TileEdge.NORTHWEST: - // if (destinationFill.northwestTile !== sourceTile) { - // const from = destinationFill.northwestTile || {}; - // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because northwest tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); - // } destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.northwestMesh !== sourceMesh; destinationFill.northwestMesh = sourceMesh; destinationFill.northwestTile = sourceTile; return; case TileEdge.NORTHEAST: - // if (destinationFill.northeastTile !== sourceTile) { - // const from = destinationFill.northeastTile || {}; - // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because northeast tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); - // } destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.northeastMesh !== sourceMesh; destinationFill.northeastMesh = sourceMesh; destinationFill.northeastTile = sourceTile; return; case TileEdge.SOUTHWEST: - // if (destinationFill.southwestTile !== sourceTile) { - // const from = destinationFill.southwestTile || {}; - // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because southwest tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); - // } destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.southwestMesh !== sourceMesh; destinationFill.southwestMesh = sourceMesh; destinationFill.southwestTile = sourceTile; return; case TileEdge.SOUTHEAST: - // if (destinationFill.southeastTile !== sourceTile) { - // const from = destinationFill.southeastTile || {}; - // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because southeast tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); - // } destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.southeastMesh !== sourceMesh; destinationFill.southeastMesh = sourceMesh; destinationFill.southeastTile = sourceTile; @@ -317,10 +306,6 @@ define([ if (sourceTile.level <= destinationTile.level) { // Source edge completely spans the destination edge. - // if (edgeTiles[0] !== sourceTile) { - // const from = edgeTiles[0] || {}; - // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because edge tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); - // } destinationFill.changedThisFrame = destinationFill.changedThisFrame || edgeMeshes[0] !== sourceMesh || edgeMeshes.length !== 1; edgeMeshes[0] = sourceMesh; edgeTiles[0] = sourceTile; @@ -334,34 +319,6 @@ define([ var startIndex, endIndex, existingTile, existingRectangle; var sourceRectangle = sourceTile.rectangle; - function firstIsLessThanSecond(a, b, epsilon) { - if (Math.abs(a - b) < epsilon) { - return false; - } - return a < b; - } - - function firstIsLessThanOrEqualToSecond(a, b, epsilon) { - if (Math.abs(a - b) < epsilon) { - return true; - } - return a < b; - } - - function firstIsGreaterThanSecond(a, b, epsilon) { - if (Math.abs(a - b) < epsilon) { - return false; - } - return a > b; - } - - function firstIsGreaterThanOrEqualToSecond(a, b, epsilon) { - if (Math.abs(a - b) < epsilon) { - return true; - } - return a > b; - } - var epsilon; var destinationRectangle = destinationTile.rectangle; @@ -372,14 +329,14 @@ define([ for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (firstIsGreaterThanSecond(sourceRectangle.north, existingRectangle.south, epsilon)) { + if (CesiumMath.leftIsGreaterThanRight(sourceRectangle.north, existingRectangle.south, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (firstIsGreaterThanOrEqualToSecond(sourceRectangle.south, existingRectangle.north, epsilon)) { + if (CesiumMath.leftIsGreaterThanOrEqualToRight(sourceRectangle.south, existingRectangle.north, epsilon)) { break; } } @@ -390,14 +347,14 @@ define([ for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (firstIsLessThanSecond(sourceRectangle.west, existingRectangle.east, epsilon)) { + if (CesiumMath.leftIsLessThanRight(sourceRectangle.west, existingRectangle.east, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (firstIsLessThanOrEqualToSecond(sourceRectangle.east, existingRectangle.west, epsilon)) { + if (CesiumMath.leftIsLessThanOrEqualToRight(sourceRectangle.east, existingRectangle.west, epsilon)) { break; } } @@ -408,14 +365,14 @@ define([ for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (firstIsLessThanSecond(sourceRectangle.south, existingRectangle.north, epsilon)) { + if (CesiumMath.leftIsLessThanRight(sourceRectangle.south, existingRectangle.north, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (firstIsLessThanOrEqualToSecond(sourceRectangle.north, existingRectangle.south, epsilon)) { + if (CesiumMath.leftIsLessThanOrEqualToRight(sourceRectangle.north, existingRectangle.south, epsilon)) { break; } } @@ -426,14 +383,14 @@ define([ for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) { existingTile = edgeTiles[startIndex]; existingRectangle = existingTile.rectangle; - if (firstIsGreaterThanSecond(sourceRectangle.east, existingRectangle.west, epsilon)) { + if (CesiumMath.leftIsGreaterThanRight(sourceRectangle.east, existingRectangle.west, epsilon)) { break; } } for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) { existingTile = edgeTiles[endIndex]; existingRectangle = existingTile.rectangle; - if (firstIsGreaterThanOrEqualToSecond(sourceRectangle.west, existingRectangle.east, epsilon)) { + if (CesiumMath.leftIsGreaterThanOrEqualToRight(sourceRectangle.west, existingRectangle.east, epsilon)) { break; } } @@ -441,15 +398,10 @@ define([ } if (endIndex - startIndex === 1) { - // if (edgeTiles[startIndex] !== sourceTile) { - // const from = edgeTiles[startIndex] || {}; - // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because edge tile changed from L${from.level}X${from.x}Y${from.y} to L${sourceTile.level}X${sourceTile.x}Y${sourceTile.y}`); - // } destinationFill.changedThisFrame = destinationFill.changedThisFrame || edgeMeshes[startIndex] !== sourceMesh; edgeMeshes[startIndex] = sourceMesh; edgeTiles[startIndex] = sourceTile; } else { - // console.log(`L${destinationTile.level}X${destinationTile.x}Y${destinationTile.y} changed because number of tiles on edge changed`); destinationFill.changedThisFrame = true; edgeMeshes.splice(startIndex, endIndex - startIndex, sourceMesh); edgeTiles.splice(startIndex, endIndex - startIndex, sourceTile); diff --git a/Specs/Core/MathSpec.js b/Specs/Core/MathSpec.js index 60876d81eef..fba21c6269d 100644 --- a/Specs/Core/MathSpec.js +++ b/Specs/Core/MathSpec.js @@ -254,6 +254,110 @@ defineSuite([ }).toThrowDeveloperError(); }); + it('leftIsLessThanRight works', function() { + expect(CesiumMath.leftIsLessThanRight(1.0, 2.0, 0.2)).toBe(true); + expect(CesiumMath.leftIsLessThanRight(2.0, 1.0, 0.2)).toBe(false); + expect(CesiumMath.leftIsLessThanRight(1.0, 1.0, 0.2)).toBe(false); + expect(CesiumMath.leftIsLessThanRight(1.0, 1.2, 0.2)).toBe(false); + expect(CesiumMath.leftIsLessThanRight(1.2, 1.0, 0.2)).toBe(false); + }); + + it('leftIsLessThanRight throws for undefined left', function() { + expect(function() { + CesiumMath.leftIsLessThanRight(undefined, 5.0, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsLessThanRight throws for undefined right', function() { + expect(function() { + CesiumMath.leftIsLessThanRight(1.0, undefined, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsLessThanRight throws for undefined absoluteEpsilon', function() { + expect(function() { + CesiumMath.leftIsLessThanRight(1.0, 5.0, undefined); + }).toThrowDeveloperError(); + }); + + it('leftIsLessThanOrEqualToRight works', function() { + expect(CesiumMath.leftIsLessThanOrEqualToRight(1.0, 2.0, 0.2)).toBe(true); + expect(CesiumMath.leftIsLessThanOrEqualToRight(2.0, 1.0, 0.2)).toBe(false); + expect(CesiumMath.leftIsLessThanOrEqualToRight(1.0, 1.0, 0.2)).toBe(true); + expect(CesiumMath.leftIsLessThanOrEqualToRight(1.0, 1.2, 0.2)).toBe(true); + expect(CesiumMath.leftIsLessThanOrEqualToRight(1.2, 1.0, 0.2)).toBe(true); + }); + + it('leftIsLessThanOrEqualToRight throws for undefined left', function() { + expect(function() { + CesiumMath.leftIsLessThanOrEqualToRight(undefined, 5.0, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsLessThanOrEqualToRight throws for undefined right', function() { + expect(function() { + CesiumMath.leftIsLessThanOrEqualToRight(1.0, undefined, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsLessThanOrEqualToRight throws for undefined absoluteEpsilon', function() { + expect(function() { + CesiumMath.leftIsLessThanOrEqualToRight(1.0, 5.0, undefined); + }).toThrowDeveloperError(); + }); + + it('leftIsGreaterThanRight works', function() { + expect(CesiumMath.leftIsGreaterThanRight(1.0, 2.0, 0.2)).toBe(false); + expect(CesiumMath.leftIsGreaterThanRight(2.0, 1.0, 0.2)).toBe(true); + expect(CesiumMath.leftIsGreaterThanRight(1.0, 1.0, 0.2)).toBe(false); + expect(CesiumMath.leftIsGreaterThanRight(1.0, 1.2, 0.2)).toBe(false); + expect(CesiumMath.leftIsGreaterThanRight(1.2, 1.0, 0.2)).toBe(false); + }); + + it('leftIsGreaterThanRight throws for undefined left', function() { + expect(function() { + CesiumMath.leftIsGreaterThanRight(undefined, 5.0, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsGreaterThanRight throws for undefined right', function() { + expect(function() { + CesiumMath.leftIsGreaterThanRight(1.0, undefined, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsGreaterThanRight throws for undefined absoluteEpsilon', function() { + expect(function() { + CesiumMath.leftIsGreaterThanRight(1.0, 5.0, undefined); + }).toThrowDeveloperError(); + }); + + it('leftIsGreaterThanOrEqualToRight works', function() { + expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 2.0, 0.2)).toBe(false); + expect(CesiumMath.leftIsGreaterThanOrEqualToRight(2.0, 1.0, 0.2)).toBe(true); + expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 1.0, 0.2)).toBe(true); + expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 1.2, 0.2)).toBe(true); + expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.2, 1.0, 0.2)).toBe(true); + }); + + it('leftIsGreaterThanOrEqualToRight throws for undefined left', function() { + expect(function() { + CesiumMath.leftIsGreaterThanOrEqualToRight(undefined, 5.0, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsGreaterThanOrEqualToRight throws for undefined right', function() { + expect(function() { + CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, undefined, CesiumMath.EPSILON16); + }).toThrowDeveloperError(); + }); + + it('leftIsGreaterThanOrEqualToRight throws for undefined absoluteEpsilon', function() { + expect(function() { + CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 5.0, undefined); + }).toThrowDeveloperError(); + }); + it('factorial produces the correct results', function() { var factorials = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800, 479001600, 6227020800, 87178291200, 1307674368000, 20922789888000, 355687428096000, 6402373705728000, 121645100408832000, 2432902008176640000, 51090942171709440000, 1124000727777607680000, 25852016738884976640000, 620448401733239439360000]; From f21a0b020b3b12ca8722f01a7d2b3cf0e693baa8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 5 Oct 2018 23:32:27 +1000 Subject: [PATCH 049/131] Fixed a bug propating edges across the antimeridian. --- Source/Scene/TerrainFillMesh.js | 121 ++++++++------------------------ 1 file changed, 29 insertions(+), 92 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 5a5a45e7bb3..f55ddc072f8 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -11,6 +11,7 @@ define([ '../Core/IndexDatatype', '../Core/OrientedBoundingBox', '../Core/Queue', + '../Core/Rectangle', '../Core/TileEdge', '../Core/TerrainEncoding', '../Core/TerrainMesh', @@ -34,6 +35,7 @@ define([ IndexDatatype, OrientedBoundingBox, Queue, + Rectangle, TileEdge, TerrainEncoding, TerrainMesh, @@ -413,7 +415,6 @@ define([ var cartesianScratch = new Cartesian3(); var normalScratch = new Cartesian3(); var octEncodedNormalScratch = new Cartesian2(); - var highestFillTileVertexCount = 0; function createFillMesh(tileProvider, frameState, tile) { var surfaceTile = tile.data; @@ -449,70 +450,8 @@ define([ var minimumHeight = Math.min(southwestHeight, southeastHeight, northwestHeight, northeastHeight); var maximumHeight = Math.max(southwestHeight, southeastHeight, northwestHeight, northeastHeight); - // var west = getEdgeVertices(tile, fill.westTiles, fill.westMeshes, lastSelectionFrameNumber, TileEdge.EAST, westScratch); - // var south = getEdgeVertices(tile, fill.southTiles, fill.southMeshes, lastSelectionFrameNumber, TileEdge.NORTH, southScratch); - // var east = getEdgeVertices(tile, fill.eastTiles, fill.eastMeshes, lastSelectionFrameNumber, TileEdge.WEST, eastScratch); - // var north = getEdgeVertices(tile, fill.northTiles, fill.northMeshes, lastSelectionFrameNumber, TileEdge.SOUTH, northScratch); - - // var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; - // var hasWebMercatorT = true; // TODO - // var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); - - // var minimumHeight = Number.MAX_VALUE; - // var maximumHeight = -Number.MAX_VALUE; - // var hasAnyVertices = false; - - // if (west.vertices.length > 0) { - // minimumHeight = Math.min(minimumHeight, west.minimumHeight); - // maximumHeight = Math.max(maximumHeight, west.maximumHeight); - // hasAnyVertices = true; - // } - - // if (south.vertices.length > 0) { - // minimumHeight = Math.min(minimumHeight, south.minimumHeight); - // maximumHeight = Math.max(maximumHeight, south.maximumHeight); - // hasAnyVertices = true; - // } - - // if (east.vertices.length > 0) { - // minimumHeight = Math.min(minimumHeight, east.minimumHeight); - // maximumHeight = Math.max(maximumHeight, east.maximumHeight); - // hasAnyVertices = true; - // } - - // if (north.vertices.length > 0) { - // minimumHeight = Math.min(minimumHeight, north.minimumHeight); - // maximumHeight = Math.max(maximumHeight, north.maximumHeight); - // hasAnyVertices = true; - // } - - // if (!hasAnyVertices) { - // var tileBoundingRegion = surfaceTile.tileBoundingRegion; - // minimumHeight = tileBoundingRegion.minimumHeight; - // maximumHeight = tileBoundingRegion.maximumHeight; - // } - var middleHeight = (minimumHeight + maximumHeight) * 0.5; - // var tileVertices = tileVerticesScratch; - // tileVertices.length = 0; - - // var ellipsoid = tile.tilingScheme.ellipsoid; - // var rectangle = tile.rectangle; - - // var northwestIndex = 0; - // addCornerVertexIfNecessary(ellipsoid, 0.0, 1.0, rectangle.west, rectangle.north, middleHeight, west, north, hasVertexNormals, hasWebMercatorT, tileVertices); - // addVerticesToFillTile(west, stride, tileVertices); - // var southwestIndex = tileVertices.length / stride; - // addCornerVertexIfNecessary(ellipsoid, 0.0, 0.0, rectangle.west, rectangle.south, middleHeight, south, west, hasVertexNormals, hasWebMercatorT, tileVertices); - // addVerticesToFillTile(south, stride, tileVertices); - // var southeastIndex = tileVertices.length / stride; - // addCornerVertexIfNecessary(ellipsoid, 1.0, 0.0, rectangle.east, rectangle.south, middleHeight, east, south, hasVertexNormals, hasWebMercatorT, tileVertices); - // addVerticesToFillTile(east, stride, tileVertices); - // var northeastIndex = tileVertices.length / stride; - // addCornerVertexIfNecessary(ellipsoid, 1.0, 1.0, rectangle.east, rectangle.north, middleHeight, north, east, hasVertexNormals, hasWebMercatorT, tileVertices); - // addVerticesToFillTile(north, stride, tileVertices); - // Add a single vertex at the center of the tile. var obb = OrientedBoundingBox.fromRectangle(tile.rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); var center = obb.center; @@ -526,11 +465,9 @@ define([ tileVertices.push((cartographicScratch.longitude - rectangle.west) / (rectangle.east - rectangle.west)); tileVertices.push((cartographicScratch.latitude - rectangle.south) / (rectangle.north - rectangle.south)); - if (hasWebMercatorT) { - var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); - var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); - tileVertices.push((WebMercatorProjection.geodeticLatitudeToMercatorAngle(cartographicScratch.latitude) - southMercatorY) * oneOverMercatorHeight); - } + var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); + var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); + tileVertices.push((WebMercatorProjection.geodeticLatitudeToMercatorAngle(cartographicScratch.latitude) - southMercatorY) * oneOverMercatorHeight); if (hasVertexNormals) { ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); @@ -588,9 +525,7 @@ define([ typedArray[write++] = tileVertices[read++]; typedArray[write++] = tileVertices[read++]; - if (hasWebMercatorT) { - typedArray[write++] = tileVertices[read++]; - } + typedArray[write++] = tileVertices[read++]; if (hasVertexNormals) { typedArray[write++] = AttributeCompression.octPackFloat(Cartesian2.fromElements(tileVertices[read++], tileVertices[read++], octEncodedNormalScratch)); @@ -600,11 +535,6 @@ define([ var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, hasVertexNormals, hasWebMercatorT); encoding.center = center; - if (vertexCount > highestFillTileVertexCount) { - highestFillTileVertexCount = vertexCount; - console.log('New highest vertex count: ', highestFillTileVertexCount, ' from L', tile.level, 'X', tile.x, 'Y', tile.y); - } - var q1, q2, s; var previousU, previousV, currentU, currentV; @@ -809,11 +739,9 @@ define([ tileVertices.push(sourceEncoding.decodeHeight(sourceVertices, sourceIndex), u, v); - if (sourceEncoding.hasWebMercatorT) { - // At the corners, the geographic and web mercator vertical texture coordinate - // is the same: either 0.0 or 1.0; - tileVertices.push(v); - } + // At the corners, the geographic and web mercator vertical texture coordinate + // is the same: either 0.0 or 1.0; + tileVertices.push(v); if (sourceEncoding.hasVertexNormals) { sourceEncoding.getOctEncodedNormal(sourceVertices, sourceIndex, encodedNormalScratch); @@ -850,11 +778,9 @@ define([ tileVertices.push(position.x, position.y, position.z); tileVertices.push(cartographicScratch.height, u, v); - if (sourceEncoding.hasWebMercatorT) { - // At the corners, the geographic and web mercator vertical texture coordinate - // is the same: either 0.0 or 1.0; - tileVertices.push(v); - } + // At the corners, the geographic and web mercator vertical texture coordinate + // is the same: either 0.0 or 1.0; + tileVertices.push(v); if (sourceEncoding.hasVertexNormals) { var encodedNormal1 = sourceEncoding.getOctEncodedNormal(sourceVertices, previousIndex, encodedNormalScratch); @@ -879,11 +805,9 @@ define([ tileVertices.push(position.x, position.y, position.z); tileVertices.push(height, u, v); - if (hasWebMercatorT) { - // At the corners, the geographic and web mercator vertical texture coordinate - // is the same: either 0.0 or 1.0; - tileVertices.push(v); - } + // At the corners, the geographic and web mercator vertical texture coordinate + // is the same: either 0.0 or 1.0; + tileVertices.push(v); if (hasVertexNormals) { var normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); @@ -1001,12 +925,25 @@ define([ } var edgeDetailsScratch = new TerrainTileEdgeDetails(); + var sourceRectangleScratch = new Rectangle(); function addEdgeMesh(terrainFillMesh, ellipsoid, edgeTile, edgeMesh, tileEdge, stride, tileVertices) { var terrainMesh = edgeMesh; + // Handle copying edges across the anti-meridian. + var sourceRectangle = edgeTile.rectangle; + if (tileEdge === TileEdge.EAST && terrainFillMesh.tile.x === 0) { + sourceRectangle = Rectangle.clone(edgeTile.rectangle, sourceRectangleScratch); + sourceRectangle.west -= CesiumMath.TWO_PI; + sourceRectangle.east -= CesiumMath.TWO_PI; + } else if (tileEdge === TileEdge.WEST && edgeTile.x === 0) { + sourceRectangle = Rectangle.clone(edgeTile.rectangle, sourceRectangleScratch); + sourceRectangle.west += CesiumMath.TWO_PI; + sourceRectangle.east += CesiumMath.TWO_PI; + } + edgeDetailsScratch.clear(); - var edgeDetails = terrainMesh.getEdgeVertices(tileEdge, edgeTile.rectangle, terrainFillMesh.tile.rectangle, ellipsoid, edgeDetailsScratch); + var edgeDetails = terrainMesh.getEdgeVertices(tileEdge, sourceRectangle, terrainFillMesh.tile.rectangle, ellipsoid, edgeDetailsScratch); var vertices = edgeDetails.vertices; From b68efbd6a1b830ecd7475f05fec7f8c9234bd253 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 6 Oct 2018 21:52:09 +1000 Subject: [PATCH 050/131] Fix another antimeridian problem. --- Source/Scene/TerrainFillMesh.js | 103 ++++++++------------------------ 1 file changed, 24 insertions(+), 79 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index f55ddc072f8..de0623c41b3 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -535,77 +535,6 @@ define([ var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, hasVertexNormals, hasWebMercatorT); encoding.center = center; - var q1, q2, s; - var previousU, previousV, currentU, currentV; - - for (s = 1; s < eastIndicesNorthToSouth.length; ++s) { - q1 = eastIndicesNorthToSouth[s - 1]; - q2 = eastIndicesNorthToSouth[s]; - previousU = tileVertices[q1 * stride + 4]; - previousV = tileVertices[q1 * stride + 5]; - currentU = tileVertices[q2 * stride + 4]; - currentV = tileVertices[q2 * stride + 5]; - - if (previousU !== 1.0 || currentU !== 1.0) { - console.log('not east?!'); - } - - if (currentV >= previousV) { - console.log('not descending?!'); - } - } - - for (s = 1; s < westIndicesSouthToNorth.length; ++s) { - q1 = westIndicesSouthToNorth[s - 1]; - q2 = westIndicesSouthToNorth[s]; - previousU = tileVertices[q1 * stride + 4]; - previousV = tileVertices[q1 * stride + 5]; - currentU = tileVertices[q2 * stride + 4]; - currentV = tileVertices[q2 * stride + 5]; - - if (previousU !== 0.0 || currentU !== 0.0) { - console.log('not west?!'); - } - - if (currentV <= previousV) { - console.log('not ascending?!'); - } - } - - for (s = 1; s < southIndicesEastToWest.length; ++s) { - q1 = southIndicesEastToWest[s - 1]; - q2 = southIndicesEastToWest[s]; - previousU = tileVertices[q1 * stride + 4]; - previousV = tileVertices[q1 * stride + 5]; - currentU = tileVertices[q2 * stride + 4]; - currentV = tileVertices[q2 * stride + 5]; - - if (previousV !== 0.0 || currentV !== 0.0) { - console.log('not south?!'); - } - - if (currentU >= previousU) { - console.log('not descending?!'); - } - } - - for (s = 1; s < northIndicesWestToEast.length; ++s) { - q1 = northIndicesWestToEast[s - 1]; - q2 = northIndicesWestToEast[s]; - previousU = tileVertices[q1 * stride + 4]; - previousV = tileVertices[q1 * stride + 5]; - currentU = tileVertices[q2 * stride + 4]; - currentV = tileVertices[q2 * stride + 5]; - - if (previousV !== 1.0 || currentV !== 1.0) { - console.log('not north?!'); - } - - if (currentU <= previousU) { - console.log('not ascending?!'); - } - } - var mesh = new TerrainMesh( obb.center, typedArray, @@ -692,7 +621,23 @@ define([ } } - function transformTextureCoordinates(sourceRectangle, targetRectangle, coordinates, result) { + var sourceRectangleScratch = new Rectangle(); + + function transformTextureCoordinates(sourceTile, targetTile, coordinates, result) { + var sourceRectangle = sourceTile.rectangle; + var targetRectangle = targetTile.rectangle; + + // Handle transforming across the anti-meridian. + if (targetTile.x === 0 && coordinates.x === 1.0 && sourceTile.x === sourceTile.tilingScheme.getNumberOfXTilesAtLevel(sourceTile.level) - 1) { + sourceRectangle = Rectangle.clone(sourceTile.rectangle, sourceRectangleScratch); + sourceRectangle.west -= CesiumMath.TWO_PI; + sourceRectangle.east -= CesiumMath.TWO_PI; + } else if (sourceTile.x === 0 && coordinates.x === 0.0 && targetTile.x === targetTile.tilingScheme.getNumberOfXTilesAtLevel(targetTile.level) - 1) { + sourceRectangle = Rectangle.clone(sourceTile.rectangle, sourceRectangleScratch); + sourceRectangle.west += CesiumMath.TWO_PI; + sourceRectangle.east += CesiumMath.TWO_PI; + } + var sourceWidth = sourceRectangle.east - sourceRectangle.west; var umin = (targetRectangle.west - sourceRectangle.west) / sourceWidth; var umax = (targetRectangle.east - sourceRectangle.west) / sourceWidth; @@ -753,12 +698,12 @@ define([ var encodedNormalScratch2 = new Cartesian2(); var cartesianScratch2 = new Cartesian3(); - function addInterpolatedVertexAtCorner(ellipsoid, sourceRectangle, targetRectangle, sourceMesh, previousIndex, nextIndex, u, v, interpolateU, tileVertices) { + function addInterpolatedVertexAtCorner(ellipsoid, sourceTile, targetTile, sourceMesh, previousIndex, nextIndex, u, v, interpolateU, tileVertices) { var sourceEncoding = sourceMesh.encoding; var sourceVertices = sourceMesh.vertices; - var previousUv = transformTextureCoordinates(sourceRectangle, targetRectangle, sourceEncoding.decodeTextureCoordinates(sourceVertices, previousIndex, uvScratch), uvScratch); - var nextUv = transformTextureCoordinates(sourceRectangle, targetRectangle, sourceEncoding.decodeTextureCoordinates(sourceVertices, nextIndex, uvScratch2), uvScratch2); + var previousUv = transformTextureCoordinates(sourceTile, targetTile, sourceEncoding.decodeTextureCoordinates(sourceVertices, previousIndex, uvScratch), uvScratch); + var nextUv = transformTextureCoordinates(sourceTile, targetTile, sourceEncoding.decodeTextureCoordinates(sourceVertices, nextIndex, uvScratch2), uvScratch2); var ratio; if (interpolateU) { @@ -770,6 +715,7 @@ define([ var height1 = sourceEncoding.decodeHeight(sourceVertices, previousIndex); var height2 = sourceEncoding.decodeHeight(sourceVertices, nextIndex); + var targetRectangle = targetTile.rectangle; cartographicScratch.longitude = CesiumMath.lerp(targetRectangle.west, targetRectangle.east, u); cartographicScratch.latitude = CesiumMath.lerp(targetRectangle.south, targetRectangle.north, v); cartographicScratch.height = CesiumMath.lerp(height1, height2, ratio); @@ -925,7 +871,6 @@ define([ } var edgeDetailsScratch = new TerrainTileEdgeDetails(); - var sourceRectangleScratch = new Rectangle(); function addEdgeMesh(terrainFillMesh, ellipsoid, edgeTile, edgeMesh, tileEdge, stride, tileVertices) { var terrainMesh = edgeMesh; @@ -1102,7 +1047,7 @@ define([ vertexIndexIndex = isNext ? 0 : edgeVertices.length - 1; vertexIndex = edgeVertices[vertexIndexIndex]; sourceTerrainMesh.encoding.decodeTextureCoordinates(sourceTerrainMesh.vertices, vertexIndex, uvScratch); - var targetUv = transformTextureCoordinates(sourceTile.rectangle, terrainFillMesh.tile.rectangle, uvScratch, uvScratch); + var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch); if (targetUv.x === u && targetUv.y === v) { // Vertex is good! addVertexFromTileAtCorner(sourceTerrainMesh, vertexIndex, u, v, tileVertices); @@ -1112,7 +1057,7 @@ define([ // The last vertex is not the one we need, try binary searching for the right one. vertexIndexIndex = binarySearch(edgeVertices, compareU ? u : v, function(vertexIndex, textureCoordinate) { sourceTerrainMesh.encoding.decodeTextureCoordinates(sourceTerrainMesh.vertices, vertexIndex, uvScratch); - var targetUv = transformTextureCoordinates(sourceTile.rectangle, terrainFillMesh.tile.rectangle, uvScratch, uvScratch); + var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch); if (increasing) { if (compareU) { return u - targetUv.x; @@ -1129,7 +1074,7 @@ define([ if (vertexIndexIndex > 0 && vertexIndexIndex < edgeVertices.length) { // The corner falls between two vertices, so interpolate between them. - addInterpolatedVertexAtCorner(ellipsoid, sourceTile.rectangle, terrainFillMesh.tile.rectangle, sourceTerrainMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, tileVertices); + addInterpolatedVertexAtCorner(ellipsoid, sourceTile, terrainFillMesh.tile, sourceTerrainMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, tileVertices); return true; } } else { From 16268a16ebdefcc41d46724a4d37c124ccb76a9d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 6 Oct 2018 22:54:48 +1000 Subject: [PATCH 051/131] Address some TODOs. --- Source/Scene/TerrainFillMesh.js | 126 ++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 38 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index de0623c41b3..e4b78e94585 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -684,8 +684,8 @@ define([ tileVertices.push(sourceEncoding.decodeHeight(sourceVertices, sourceIndex), u, v); - // At the corners, the geographic and web mercator vertical texture coordinate - // is the same: either 0.0 or 1.0; + // At the corners, the geographic and web mercator vertical texture coordinates + // are the same: either 0.0 or 1.0; tileVertices.push(v); if (sourceEncoding.hasVertexNormals) { @@ -724,8 +724,8 @@ define([ tileVertices.push(position.x, position.y, position.z); tileVertices.push(cartographicScratch.height, u, v); - // At the corners, the geographic and web mercator vertical texture coordinate - // is the same: either 0.0 or 1.0; + // At the corners, the geographic and web mercator vertical texture coordinates + // are the same: either 0.0 or 1.0; tileVertices.push(v); if (sourceEncoding.hasVertexNormals) { @@ -805,51 +805,53 @@ define([ // There is no precise vertex available from the corner or from either adjacent edge. // So use the height from the closest vertex anywhere on the perimeter of this tile. - // TODO: would be better to find the closest height rather than favoring a side. - // TODO: We'll do the wrong thing if the height is exactly 0.0 because that's falsy. var height; if (u === 0.0) { if (v === 0.0) { // southwest - height = - getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, false, TileEdge.NORTH, u, v) || - getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, true, TileEdge.EAST, u, v) || - getHeightAtCorner(terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, u, v) || - getHeightAtCorner(terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, false, TileEdge.WEST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, true, TileEdge.SOUTH, u, v) || - getHeightAtCorner(terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, u, v); + height = getClosestHeightToCorner( + terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, + terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, + terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, + terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, + terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, + terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, + terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, + u, v); } else { // northwest - height = - getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, false, TileEdge.SOUTH, u, v) || - getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, true, TileEdge.EAST, u, v) || - getHeightAtCorner(terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, u, v) || - getHeightAtCorner(terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, false, TileEdge.WEST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, true, TileEdge.NORTH, u, v) || - getHeightAtCorner(terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, u, v); + height = getClosestHeightToCorner( + terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, + terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, + terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, + terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, + terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, + terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, + terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, + u, v); } } else if (v === 0.0) { // southeast - height = - getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, false, TileEdge.NORTH, u, v) || - getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, true, TileEdge.WEST, u, v) || - getHeightAtCorner(terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, u, v) || - getHeightAtCorner(terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, false, TileEdge.EAST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, true, TileEdge.SOUTH, u, v) || - getHeightAtCorner(terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, u, v); + height = getClosestHeightToCorner( + terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, + terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, + terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, + terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, + terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, + terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, + terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, + u, v); } else { // northeast - height = - getNearestHeightOnEdge(terrainFillMesh.northMeshes, terrainFillMesh.northTiles, false, TileEdge.SOUTH, u, v) || - getNearestHeightOnEdge(terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, true, TileEdge.WEST, u, v) || - getHeightAtCorner(terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, u, v) || - getHeightAtCorner(terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.westMeshes, terrainFillMesh.westTiles, false, TileEdge.EAST, u, v) || - getNearestHeightOnEdge(terrainFillMesh.southMeshes, terrainFillMesh.southTiles, true, TileEdge.NORTH, u, v) || - getHeightAtCorner(terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, u, v); + height = getClosestHeightToCorner( + terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, + terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, + terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, + terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, + terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, + terrainFillMesh.southMeshes, terrainFillMesh.southTiles,TileEdge.NORTH, + terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, + u, v); } if (!defined(height)) { @@ -864,6 +866,54 @@ define([ addVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, hasVertexNormals, hasWebMercatorT, tileVertices); } + function getClosestHeightToCorner( + adjacentEdge1Meshes, adjacentEdge1Tiles, adjacentEdge1, + adjacentEdge2Meshes, adjacentEdge2Tiles, adjacentEdge2, + adjacentCorner1Mesh, adjacentCorner1Tile, adjacentCorner1, + adjacentCorner2Mesh, adjacentCorner2Tile, adjacentCorner2, + oppositeEdge1Meshes, oppositeEdge1Tiles, oppositeEdge1, + oppositeEdge2Meshes, oppositeEdge2Tiles, oppositeEdge2, + oppositeCornerMesh, oppositeCornerTile, oppositeCorner, + u, v + ) { + // To find a height to use for this corner, we'll first look at the two adjacent edges, + // then the two adjacent corners, then the two opposite edges, then the two opposite + // corners. When e.g. both adjacent edges have a height, it would be better to choose + // the closest height rather than always choosing adjacentEdge1's height, as we're + // doing here, but it probably doesn't matter too much. + var height = getNearestHeightOnEdge(adjacentEdge1Meshes, adjacentEdge1Tiles, false, adjacentEdge1, u, v); + if (defined(height)) { + return height; + } + + height = getNearestHeightOnEdge(adjacentEdge2Meshes, adjacentEdge2Tiles, true, adjacentEdge2, u, v); + if (defined(height)) { + return height; + } + + height = getHeightAtCorner(adjacentCorner1Mesh, adjacentCorner1Tile, adjacentCorner1, u, v); + if (defined(height)) { + return height; + } + + height = getHeightAtCorner(adjacentCorner2Mesh, adjacentCorner2Tile, adjacentCorner2, u, v); + if (defined(height)) { + return height; + } + + height = getNearestHeightOnEdge(oppositeEdge1Meshes, oppositeEdge1Tiles, false, oppositeEdge1, u, v); + if (defined(height)) { + return height; + } + + height = getNearestHeightOnEdge(oppositeEdge2Meshes, oppositeEdge2Tiles, true, oppositeEdge2, u, v); + if (defined(height)) { + return height; + } + + return getHeightAtCorner(oppositeCornerMesh, oppositeCornerTile, oppositeCorner, u, v); + } + function addEdge(terrainFillMesh, ellipsoid, edgeTiles, edgeMeshes, tileEdge, stride, tileVertices) { for (var i = 0; i < edgeTiles.length; ++i) { addEdgeMesh(terrainFillMesh, ellipsoid, edgeTiles[i], edgeMeshes[i], tileEdge, stride, tileVertices); From 6a53390f172fed1d1f5fc8d0b06bf16c61a025b6 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 7 Oct 2018 00:02:31 +1000 Subject: [PATCH 052/131] Cleanup. --- Source/Scene/QuadtreePrimitive.js | 116 ++++++------------------------ 1 file changed, 23 insertions(+), 93 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 4052c2d0bf1..e9e23b87330 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -538,28 +538,6 @@ define([ customDataRemoved.length = 0; } - // Our goal with load ordering is to first load all of the tiles we need to - // render the current scene at full detail. Loading any other tiles is just - // a form of prefetching, and we need not do it at all (other concerns aside). This - // simple and obvious statement gets more complicated when we realize that, because - // we don't have bounding volumes for the entire terrain tile pyramid, we don't - // precisely know which tiles we need to render the scene at full detail, until we do - // some loading. - // - // So our load priority is (from high to low): - // 1. Tiles that we _would_ render, except that they're not sufficiently loaded yet. - // Ideally this would only include tiles that we've already determined to be visible, - // but since we don't have reliable visibility information until a tile is loaded, - // and because we (currently) must have all children in a quad renderable before we - // can refine, this pretty much means tiles we'd like to refine to, regardless of - // visibility. (high) - // 2. Tiles that we're rendering. (medium) - // 3. All other tiles. (low) - // - // Within each priority group, tiles should be loaded in approximate near-to-far order, - // but currently they're just loaded in our traversal order which makes no guarantees - // about depth ordering. - // Traverse in depth-first, near-to-far order. for (i = 0, len = levelZeroTiles.length; i < len; ++i) { tile = levelZeroTiles[i]; @@ -586,46 +564,12 @@ define([ queue.push(tile); } - // function getState(action, heightSource, renderable) { - // return `A:${action} HS:${heightSource !== undefined ? heightSource.level : 'undefined'} R:${renderable}`; - // } - - // var lastFrame; - - function reportTileAction(frameState, tile, action) { - return; - // var heightSource = tile.data.boundingVolumeSourceTile; - // var renderable = tile.renderable; - // if (tile._lastAction !== action || tile._lastHeightSource !== heightSource || tile._lastRenderable !== renderable) { - // if (lastFrame !== frameState.frameNumber) { - // console.log('****** FRAME ' + frameState.frameNumber); - // lastFrame = frameState.frameNumber; - // } - - // var tileID = `L${tile.level}X${tile.x}Y${tile.y}`; - // var emphasize = tile._lastAction !== undefined && tile._lastAction !== action ? '*' : ''; - // console.log(`${emphasize}${tileID} NOW ${getState(action, heightSource, renderable)} WAS ${getState(tile._lastAction, tile._lastHeightSource, tile._lastRenderable)}`); - - // tile._lastAction = action; - // tile._lastHeightSource = heightSource; - // tile._lastRenderable = renderable; - // } - } - function TraversalDetails() { this.allAreRenderable = true; this.anyWereRenderedLastFrame = false; - this.anyAreRenderable = false; this.notYetRenderableCount = 0; } - // TraversalDetails.prototype.clear = function() { - // this.allAreRenderable = true; - // this.anyWereRenderedLastFrame = false; - // this.anyAreRenderable = false; - // this.notYetRenderableCount = 0; - // }; - function TraversalQuadDetails() { this.southwest = new TraversalDetails(); this.southeast = new TraversalDetails(); @@ -641,7 +585,6 @@ define([ result.allAreRenderable = southwest.allAreRenderable && southeast.allAreRenderable && northwest.allAreRenderable && northeast.allAreRenderable; result.anyWereRenderedLastFrame = southwest.anyWereRenderedLastFrame || southeast.anyWereRenderedLastFrame || northwest.anyWereRenderedLastFrame || northeast.anyWereRenderedLastFrame; - result.anyAreRenderable = southwest.anyAreRenderable || southeast.anyAreRenderable || northwest.anyAreRenderable || northeast.anyAreRenderable; result.notYetRenderableCount = southwest.notYetRenderableCount + southeast.notYetRenderableCount + northwest.notYetRenderableCount + northeast.notYetRenderableCount; }; @@ -654,7 +597,7 @@ define([ * Visits a tile for possible rendering. When we call this function with a tile: * * * the tile has been determined to be visible (possibly based on a bounding volume that is not very tight-fitting) - * * its parent tile does _not_ meet the SSE. + * * its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, see comments below) * * the tile may or may not be renderable * * @private @@ -665,8 +608,7 @@ define([ * @param {QuadtreeTile} nearestRenderableTile The nearest ancestor tile for which the `renderable` property is true. * @param {Boolean} ancestorMeetsSse True if a tile higher in the tile tree already met the SSE and we're refining further only * to maintain detail while that higher tile loads. - * @returns A bit mask where bit 1 is set if this tile or _any_ of its descendants are renderable, and bit 2 is - * set if _all_ selected tiles starting with this tile are renderable. + * @param {TraversalDetails} traveralDetails On return, populated with details of how the traversal of this tile went. */ function visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { var debug = primitive._debug; @@ -694,33 +636,24 @@ define([ var lastFrame = primitive._lastSelectionFrameNumber; if (meetsSse || ancestorMeetsSse) { - if (meetsSse) { - reportTileAction(frameState, tile, 'meets SSE'); - } else { - reportTileAction(frameState, tile, 'ancestor meets SSE'); - } - - // This is the tile we want to render this frame, but we'll do different things depending + // This tile (or an ancestor) is the one we want to render this frame, but we'll do different things depending // on the state of this tile and on what we did _last_ frame. - // 1. If it's completely loaded (terrain _and_ imagery), render it. + // 1. If this tile is completely loaded (terrain _and_ imagery), render it. // TODO: technically we only need to ensure that the geometry and any imagery layers previously // loaded on descendants are loaded on this tile. It doesn't need to be really, totally // done loading. Hard to determine this though. - // 2. If it's renderable at all (even if not all imagery is loaded) and we were previously rendering it - // (even if we were rendering a fill), render it. - // 3. If this tile's children were not visited last frame because this tile was culled - // or because an ancestor tile met the SSE, then render this tile, or a fill if this tile isn't - // renderable yet. + // 2. If this tile's children were not visited last frame because this tile was culled + // or because this tile or an ancestor tile met the SSE, then render this tile, or a fill if + // this tile isn't renderable yet. var oneCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE; - var twoRenderableAndPreviouslyRendered = tile.renderable && tile._frameRendered === lastFrame; - var threeNoChildrenVisitedLastFrame = + var twoNoChildrenVisitedLastFrame = southwestChild._frameVisited !== lastFrame && southeastChild._frameVisited !== lastFrame && northwestChild._frameVisited !== lastFrame && northeastChild._frameVisited !== lastFrame; - if (oneCompletelyLoaded || twoRenderableAndPreviouslyRendered || threeNoChildrenVisitedLastFrame) { + if (oneCompletelyLoaded || twoNoChildrenVisitedLastFrame) { // Only load this tile if it (not just an ancestor) meets the SSE. if (meetsSse) { queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); @@ -730,14 +663,13 @@ define([ tile._lastSelectionResultFrame = frameState.frameNumber; tile._lastSelectionResult = TileSelectionResult.RENDERED; - traversalDetails.anyAreRenderable = tile.renderable; traversalDetails.allAreRenderable = tile.renderable; traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; return; } - // 4. Otherwise, we can't render this tile (or its fill) because doing so would cause detail to disappear + // 3. Otherwise, we can't render this tile (or its fill) because doing so would cause detail to disappear // that was visible last frame. Instead, keep rendering any still-visible descendants that were rendered // last frame and render fills for newly-visible descendants. E.g. if we were rendering level 15 last // frame but this frame we want level 14 and the closest renderable level <= 14 is 0, rendering level @@ -758,8 +690,6 @@ define([ northwestChild.upsampledFromParent && northeastChild.upsampledFromParent; if (allAreUpsampled) { - reportTileAction(frameState, tile, 'all upsampled'); - tile._lastSelectionResultFrame = frameState.frameNumber; tile._lastSelectionResult = TileSelectionResult.RENDERED; @@ -775,7 +705,6 @@ define([ primitive._tileReplacementQueue.markTileRendered(northwestChild); primitive._tileReplacementQueue.markTileRendered(northeastChild); - traversalDetails.anyAreRenderable = tile.renderable; traversalDetails.allAreRenderable = tile.renderable; traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; @@ -783,8 +712,6 @@ define([ } // SSE is not good enough, so refine. - reportTileAction(frameState, tile, 'refine'); - tile._lastSelectionResultFrame = frameState.frameNumber; tile._lastSelectionResult = TileSelectionResult.REFINED; @@ -796,11 +723,13 @@ define([ // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile, ancestorMeetsSse, traversalDetails); + // If no descendant tiles were added to the render list by the function above, it means they were all + // culled even though this tile was deemed visible. That's pretty common. + if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) { - // If no descendant tiles were added to the render list, it means they were all - // culled even though this tile was deemed visible. That's pretty common. + // At least one descendant tile was added to the render list. + // The traversalDetails tell us what happened while visiting the children. - // Since we're in this `if` block though, at least one descendant tile _was_ added to the render list! var allAreRenderable = traversalDetails.allAreRenderable; var anyWereRenderedLastFrame = traversalDetails.anyWereRenderedLastFrame; var notYetRenderableCount = traversalDetails.notYetRenderableCount; @@ -809,6 +738,7 @@ define([ // Some of our descendants aren't ready to render yet, and none were rendered last frame, // so kick them all out of the render list and render this tile instead. Continue to load them though! + // Mark the rendered descendants and their ancestors - up to this tile - as kicked. var renderList = primitive._tilesToRender; for (var i = firstRenderedDescendantIndex; i < renderList.length; ++i) { var workTile = renderList[i]; @@ -818,17 +748,19 @@ define([ } } + // Remove all descendants from the render list. primitive._tilesToRender.length = firstRenderedDescendantIndex; primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; addTileToRenderList(primitive, tile, nearestRenderableTile); tile._lastSelectionResult = TileSelectionResult.RENDERED; - // EXCEPT if we're waiting on heaps of descendants, the above will take too long. So in that case, + // If we're waiting on heaps of descendants, the above will take too long. So in that case, // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting // on this tile. Keep doing this until we actually manage to render this tile. var wasRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; if (!wasRenderedLastFrame && notYetRenderableCount > primitive.loadingDescendantLimit) { + // Remove all descendants from the load queues. primitive._tileLoadQueueLow.length = loadIndexLow; primitive._tileLoadQueueMedium.length = loadIndexMedium; primitive._tileLoadQueueHigh.length = loadIndexHigh; @@ -836,7 +768,10 @@ define([ traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; } - traversalDetails.anyAreRenderable = tile.renderable; + // TODO: consolidate _frameRendered and _lastSelectionResultFrame. + // Will probably need to set anyWereRenderedLastFrame earlier, before we overwrite + // with the new selection result. + traversalDetails.allAreRenderable = tile.renderable; traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; @@ -851,8 +786,6 @@ define([ return; } - reportTileAction(frameState, tile, 'can\'t refine'); - tile._lastSelectionResultFrame = frameState.frameNumber; tile._lastSelectionResult = TileSelectionResult.RENDERED; @@ -863,7 +796,6 @@ define([ addTileToRenderList(primitive, tile, nearestRenderableTile); queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); - traversalDetails.anyAreRenderable = tile.renderable; traversalDetails.allAreRenderable = tile.renderable; traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; @@ -918,13 +850,11 @@ define([ return visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails); } - reportTileAction(frameState, tile, 'culled'); tile._lastSelectionResultFrame = frameState.frameNumber; tile._lastSelectionResult = TileSelectionResult.CULLED; ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); - traversalDetails.anyAreRenderable = false; traversalDetails.allAreRenderable = true; traversalDetails.anyWereRenderedLastFrame = false; traversalDetails.notYetRenderableCount = 0; From a0cd91052284e7a9c1dfc1611e4dad1674799175 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 7 Oct 2018 00:07:15 +1000 Subject: [PATCH 053/131] Removing missing tile strategy. --- Source/Scene/GlobeSurfaceTileProvider.js | 54 ++++-------------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 74dc1c154fc..aba908ca2ac 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -114,31 +114,6 @@ define([ TerrainFillMesh) { 'use strict'; - /** - * The strategy to use to fill the space of tiles that are selected for rendering but that - * are not yet loaded / renderable. - * @private - */ - var MissingTileStrategy = { - /** - * Render nothing, the globe will have holes during load. - */ - RENDER_NOTHING: 0, - - /** - * Create a very simple tile to fill the space by matching the heights of adjacent tiles on the edges. This is - * cheaper on the CPU than a full upsample and avoids cracks. But it's less representative of the real - * terrain surface than an upsample from a nearby ancestor. - */ - CREATE_FILL_TILE: 2 - - /** - * Synchronously upsample the tile. This is expensive on the CPU and, when skipping several levels, it sometimes - * results in big cracks anyway. (currently not implemented) - */ - // SYNCHRONOUS_UPSAMPLE: 3 - }; - /** * Provides quadtree tiles representing the surface of the globe. This type is intended to be used * with {@link QuadtreePrimitive}. @@ -175,11 +150,6 @@ define([ this.showGroundAtmosphere = false; this.shadows = ShadowMode.RECEIVE_ONLY; - /** - * The strategy to use to fill holes in the globe when terrain tiles are not yet loaded. - */ - this.missingTileStrategy = MissingTileStrategy.CREATE_FILL_TILE; - /** * The color to use to highlight fill tiles. If undefined, fill tiles are not * highlighted at all. The alpha value is used to alpha blend with the tile's @@ -518,7 +488,7 @@ define([ // If this frame has a mix of loaded and fill tiles, we need to propagate // loaded heights to the fill tiles. - if (this.missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE && this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) { + if (this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) { TerrainFillMesh.updateFillTiles(this, this._quadtree._tilesToRender, frameState); } @@ -1482,21 +1452,15 @@ define([ var surfaceTile = tile.data; if (surfaceTile.renderableTile !== undefined) { - // We can't render this tile yet, so do something else, depending on our missing tile strategy. - var missingTileStrategy = tileProvider.missingTileStrategy; - if (missingTileStrategy === MissingTileStrategy.CREATE_FILL_TILE) { - if (surfaceTile.fill === undefined) { - // No fill was created for this tile, probably because this tile is not connected to - // any renderable tiles. So create a simple tile in the middle of the tile's possible - // height range. - surfaceTile.fill = new TerrainFillMesh(); - surfaceTile.fill.tile = tile; - surfaceTile.fill.changedThisFrame = true; - } - surfaceTile.fill.update(tileProvider, frameState); - } else { - return; + if (surfaceTile.fill === undefined) { + // No fill was created for this tile, probably because this tile is not connected to + // any renderable tiles. So create a simple tile in the middle of the tile's possible + // height range. + surfaceTile.fill = new TerrainFillMesh(); + surfaceTile.fill.tile = tile; + surfaceTile.fill.changedThisFrame = true; } + surfaceTile.fill.update(tileProvider, frameState); } var creditDisplay = frameState.creditDisplay; From 395529db5c5c52031ed7dac9384fd571cb083e71 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 6 Nov 2018 12:07:04 +1100 Subject: [PATCH 054/131] Eliminate a copy while creating fill meshes. --- Source/Scene/QuadtreePrimitive.js | 2 +- Source/Scene/TerrainFillMesh.js | 325 +++++++++++++++++------------- 2 files changed, 181 insertions(+), 146 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index e9e23b87330..60238002357 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -153,7 +153,7 @@ define([ * @type {Boolean} * @default false */ - this.preloadAncestors = false; + this.preloadAncestors = true; /** * Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded. diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index e4b78e94585..26333ef59a3 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -182,6 +182,10 @@ define([ } if (tile._lastSelectionResult === TileSelectionResult.RENDERED) { + if (defined(tile.data.vertexArray)) { + // No further processing necessary for renderable tiles. + return; + } visitTile(tileProvider, frameState, sourceTile, tile, tileEdge, currentFrameNumber, traversalQueue); return; } @@ -227,11 +231,6 @@ define([ } function visitTile(tileProvider, frameState, sourceTile, destinationTile, tileEdge, frameNumber, traversalQueue) { - if (defined(destinationTile.data.vertexArray)) { - // No further processing necessary for renderable tiles. - return; - } - var destinationSurfaceTile = destinationTile.data; if (destinationSurfaceTile.fill === undefined) { @@ -410,77 +409,138 @@ define([ } } - var tileVerticesScratch = []; var cartographicScratch = new Cartographic(); + var centerCartographicScratch = new Cartographic(); var cartesianScratch = new Cartesian3(); var normalScratch = new Cartesian3(); var octEncodedNormalScratch = new Cartesian2(); + var uvScratch2 = new Cartesian2(); + var uvScratch = new Cartesian2(); + + function HeightAndNormal() { + this.height = 0.0; + this.encodedNormal = new Cartesian2(); + } + + var swVertexScratch = new HeightAndNormal(); + var seVertexScratch = new HeightAndNormal(); + var nwVertexScratch = new HeightAndNormal(); + var neVertexScratch = new HeightAndNormal(); function createFillMesh(tileProvider, frameState, tile) { var surfaceTile = tile.data; var fill = surfaceTile.fill; - - var tileVertices = tileVerticesScratch; - tileVertices.length = 0; + var rectangle = tile.rectangle; var ellipsoid = tile.tilingScheme.ellipsoid; - var hasVertexNormals = tileProvider.terrainProvider.hasVertexNormals; - var hasWebMercatorT = true; // TODO - var stride = 6 + (hasWebMercatorT ? 1 : 0) + (hasVertexNormals ? 2 : 0); - - var northwestIndex = 0; - addCorner(fill, ellipsoid, 0.0, 1.0, fill.northwestTile, fill.northwestMesh, fill.northTiles, fill.northMeshes, fill.westTiles, fill.westMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); - addEdge(fill, ellipsoid, fill.westTiles, fill.westMeshes, TileEdge.EAST, stride, tileVertices); - var southwestIndex = tileVertices.length / stride; - addCorner(fill, ellipsoid, 0.0, 0.0, fill.southwestTile, fill.southwestMesh, fill.westTiles, fill.westMeshes, fill.southTiles, fill.southMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); - addEdge(fill, ellipsoid, fill.southTiles, fill.southMeshes, TileEdge.NORTH, stride, tileVertices); - var southeastIndex = tileVertices.length / stride; - addCorner(fill, ellipsoid, 1.0, 0.0, fill.southeastTile, fill.southeastMesh, fill.southTiles, fill.southMeshes, fill.eastTiles, fill.eastMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); - addEdge(fill, ellipsoid, fill.eastTiles, fill.eastMeshes, TileEdge.WEST, stride, tileVertices); - var northeastIndex = tileVertices.length / stride; - addCorner(fill, ellipsoid, 1.0, 1.0, fill.northeastTile, fill.northeastMesh, fill.eastTiles, fill.eastMeshes, fill.northTiles, fill.northMeshes, hasVertexNormals, hasWebMercatorT, tileVertices); - addEdge(fill, ellipsoid, fill.northTiles, fill.northMeshes, TileEdge.SOUTH, stride, tileVertices); - - // TODO: slight optimization: track min/max as we're adding vertices. - var southwestHeight = tileVertices[southwestIndex * stride + 3]; - var southeastHeight = tileVertices[southeastIndex * stride + 3]; - var northwestHeight = tileVertices[northwestIndex * stride + 3]; - var northeastHeight = tileVertices[northeastIndex * stride + 3]; + + var nwCorner = getCorner(fill, ellipsoid, 0.0, 1.0, fill.northwestTile, fill.northwestMesh, fill.northTiles, fill.northMeshes, fill.westTiles, fill.westMeshes, nwVertexScratch); + var swCorner = getCorner(fill, ellipsoid, 0.0, 0.0, fill.southwestTile, fill.southwestMesh, fill.westTiles, fill.westMeshes, fill.southTiles, fill.southMeshes, swVertexScratch); + var seCorner = getCorner(fill, ellipsoid, 1.0, 0.0, fill.southeastTile, fill.southeastMesh, fill.southTiles, fill.southMeshes, fill.eastTiles, fill.eastMeshes, seVertexScratch); + var neCorner = getCorner(fill, ellipsoid, 1.0, 1.0, fill.northeastTile, fill.northeastMesh, fill.eastTiles, fill.eastMeshes, fill.northTiles, fill.northMeshes, neVertexScratch); + + var southwestHeight = swCorner.height; + var southeastHeight = seCorner.height; + var northwestHeight = nwCorner.height; + var northeastHeight = neCorner.height; var minimumHeight = Math.min(southwestHeight, southeastHeight, northwestHeight, northeastHeight); var maximumHeight = Math.max(southwestHeight, southeastHeight, northwestHeight, northeastHeight); var middleHeight = (minimumHeight + maximumHeight) * 0.5; - // Add a single vertex at the center of the tile. - var obb = OrientedBoundingBox.fromRectangle(tile.rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); - var center = obb.center; + var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, true, true); - ellipsoid.cartesianToCartographic(center, cartographicScratch); - cartographicScratch.height = middleHeight; - var centerVertexPosition = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); + var centerCartographic = centerCartographicScratch; + centerCartographic.longitude = (rectangle.east + rectangle.west) * 0.5; + centerCartographic.latitude = (rectangle.north + rectangle.south) * 0.5; + centerCartographic.height = middleHeight; + encoding.center = ellipsoid.cartographicToCartesian(centerCartographic, encoding.center); - var rectangle = tile.rectangle; - tileVertices.push(centerVertexPosition.x, centerVertexPosition.y, centerVertexPosition.z, middleHeight); - tileVertices.push((cartographicScratch.longitude - rectangle.west) / (rectangle.east - rectangle.west)); - tileVertices.push((cartographicScratch.latitude - rectangle.south) / (rectangle.north - rectangle.south)); + // At _most_, we have vertices for the 4 corners, plus 1 center, plus every adjacent edge vertex. + // In reality there will be less most of the time, but close enough; better + // to overestimate than to re-allocate/copy/traverse the vertices twice. + var maxVertexCount = 5; + var i; + var len; + var meshes; + + meshes = fill.westMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].eastIndicesNorthToSouth.length; + } + + meshes = fill.southMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].northIndicesWestToEast.length; + } + + meshes = fill.eastMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].westIndicesSouthToNorth.length; + } + + meshes = fill.northMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].southIndicesEastToWest.length; + } + + var typedArray = new Float32Array(maxVertexCount * encoding.getStride()); + + function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT) { + var cartographic = cartographicScratch; + cartographic.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); + cartographic.latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); + cartographic.height = height; + var position = ellipsoid.cartographicToCartesian(cartographic, cartesianScratch); + + var uv = uvScratch2; + uv.x = u; + uv.y = v; + + encoding.encode(buffer, index * encoding.getStride(), position, uv, height, encodedNormal, webMercatorT); + + return index + 1; + } + + var nextIndex = 0; + var northwestIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 1.0, nwCorner.height, nwCorner.encodedNormal, 1.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.westTiles, fill.westMeshes, TileEdge.EAST); + var southwestIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 0.0, swCorner.height, swCorner.encodedNormal, 0.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.southTiles, fill.southMeshes, TileEdge.NORTH); + var southeastIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 0.0, seCorner.height, seCorner.encodedNormal, 0.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.eastTiles, fill.eastMeshes, TileEdge.WEST); + var northeastIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 1.0, neCorner.height, neCorner.encodedNormal, 1.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.northTiles, fill.northMeshes, TileEdge.SOUTH); + + // Add a single vertex at the center of the tile. + // TODO: minimumHeight and maximumHeight only reflect the corners + var obb = OrientedBoundingBox.fromRectangle(rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); + var center = obb.center; var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); - tileVertices.push((WebMercatorProjection.geodeticLatitudeToMercatorAngle(cartographicScratch.latitude) - southMercatorY) * oneOverMercatorHeight); + var centerWebMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(centerCartographic.latitude) - southMercatorY) * oneOverMercatorHeight; + + ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); + var centerEncodedNormal = AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); - if (hasVertexNormals) { - ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); - AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); - tileVertices.push(octEncodedNormalScratch.x, octEncodedNormalScratch.y); + var centerIndex = nextIndex; + encoding.encode(typedArray, nextIndex * encoding.getStride(), center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); + ++nextIndex; + + if (nextIndex * encoding.getStride() > typedArray.length) { + debugger; } - var vertexCount = tileVertices.length / stride; + var vertexCount = nextIndex; var indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex - var centerIndex = vertexCount - 1; var indexOut = 0; - var i; for (i = 0; i < vertexCount - 2; ++i) { indices[indexOut++] = centerIndex; indices[indexOut++] = i; @@ -512,29 +572,6 @@ define([ northIndicesWestToEast.push(i); } - var packedStride = hasVertexNormals ? stride - 1 : stride; // normal is packed into 1 float - var typedArray = new Float32Array(vertexCount * packedStride); - - for (i = 0; i < vertexCount; ++i) { - var read = i * stride; - var write = i * packedStride; - typedArray[write++] = tileVertices[read++] - center.x; - typedArray[write++] = tileVertices[read++] - center.y; - typedArray[write++] = tileVertices[read++] - center.z; - typedArray[write++] = tileVertices[read++]; - typedArray[write++] = tileVertices[read++]; - typedArray[write++] = tileVertices[read++]; - - typedArray[write++] = tileVertices[read++]; - - if (hasVertexNormals) { - typedArray[write++] = AttributeCompression.octPackFloat(Cartesian2.fromElements(tileVertices[read++], tileVertices[read++], octEncodedNormalScratch)); - } - } - - var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, hasVertexNormals, hasWebMercatorT); - encoding.center = center; - var mesh = new TerrainMesh( obb.center, typedArray, @@ -585,7 +622,6 @@ define([ var tileImageryCollection = surfaceTile.imagery; - var len; if (tileImageryCollection.length === 0) { var imageryLayerCollection = tileProvider._imageryLayers; var terrainProvider = tileProvider.terrainProvider; @@ -671,34 +707,27 @@ define([ return result; } - var positionScratch = new Cartesian3(); var encodedNormalScratch = new Cartesian2(); - var uvScratch = new Cartesian2(); - function addVertexFromTileAtCorner(sourceMesh, sourceIndex, u, v, tileVertices) { + function getVertexFromTileAtCorner(sourceMesh, sourceIndex, u, v, vertex) { var sourceEncoding = sourceMesh.encoding; var sourceVertices = sourceMesh.vertices; - sourceEncoding.decodePosition(sourceVertices, sourceIndex, positionScratch); - tileVertices.push(positionScratch.x, positionScratch.y, positionScratch.z); - - tileVertices.push(sourceEncoding.decodeHeight(sourceVertices, sourceIndex), u, v); - - // At the corners, the geographic and web mercator vertical texture coordinates - // are the same: either 0.0 or 1.0; - tileVertices.push(v); + vertex.height = sourceEncoding.decodeHeight(sourceVertices, sourceIndex); if (sourceEncoding.hasVertexNormals) { - sourceEncoding.getOctEncodedNormal(sourceVertices, sourceIndex, encodedNormalScratch); - tileVertices.push(encodedNormalScratch.x, encodedNormalScratch.y); + sourceEncoding.getOctEncodedNormal(sourceVertices, sourceIndex, vertex.encodedNormal); + } else { + var normal = vertex.encodedNormal; + normal.x = 0.0; + normal.y = 0.0; } } - var uvScratch2 = new Cartesian2(); var encodedNormalScratch2 = new Cartesian2(); var cartesianScratch2 = new Cartesian3(); - function addInterpolatedVertexAtCorner(ellipsoid, sourceTile, targetTile, sourceMesh, previousIndex, nextIndex, u, v, interpolateU, tileVertices) { + function getInterpolatedVertexAtCorner(ellipsoid, sourceTile, targetTile, sourceMesh, previousIndex, nextIndex, u, v, interpolateU, vertex) { var sourceEncoding = sourceMesh.encoding; var sourceVertices = sourceMesh.vertices; @@ -718,65 +747,43 @@ define([ var targetRectangle = targetTile.rectangle; cartographicScratch.longitude = CesiumMath.lerp(targetRectangle.west, targetRectangle.east, u); cartographicScratch.latitude = CesiumMath.lerp(targetRectangle.south, targetRectangle.north, v); - cartographicScratch.height = CesiumMath.lerp(height1, height2, ratio); - var position = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); - - tileVertices.push(position.x, position.y, position.z); - tileVertices.push(cartographicScratch.height, u, v); - - // At the corners, the geographic and web mercator vertical texture coordinates - // are the same: either 0.0 or 1.0; - tileVertices.push(v); + vertex.height = cartographicScratch.height = CesiumMath.lerp(height1, height2, ratio); + var normal; if (sourceEncoding.hasVertexNormals) { var encodedNormal1 = sourceEncoding.getOctEncodedNormal(sourceVertices, previousIndex, encodedNormalScratch); var encodedNormal2 = sourceEncoding.getOctEncodedNormal(sourceVertices, nextIndex, encodedNormalScratch2); var normal1 = AttributeCompression.octDecode(encodedNormal1.x, encodedNormal1.y, cartesianScratch); var normal2 = AttributeCompression.octDecode(encodedNormal2.x, encodedNormal2.y, cartesianScratch2); - var normal = Cartesian3.lerp(normal1, normal2, ratio, cartesianScratch); + normal = Cartesian3.lerp(normal1, normal2, ratio, cartesianScratch); Cartesian3.normalize(normal, normal); - var encodedNormal = AttributeCompression.octEncode(normal, encodedNormalScratch); - tileVertices.push(encodedNormal.x, encodedNormal.y); + AttributeCompression.octEncode(normal, vertex.encodedNormal); + } else { + normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); + AttributeCompression.octEncode(normal, vertex.encodedNormal); } } - function addVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, hasVertexNormals, hasWebMercatorT, tileVertices) { - var rectangle = terrainFillMesh.tile.rectangle; - - cartographicScratch.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); - cartographicScratch.latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); - cartographicScratch.height = height; - var position = ellipsoid.cartographicToCartesian(cartographicScratch, cartesianScratch); - - tileVertices.push(position.x, position.y, position.z); - tileVertices.push(height, u, v); - - // At the corners, the geographic and web mercator vertical texture coordinate - // is the same: either 0.0 or 1.0; - tileVertices.push(v); - - if (hasVertexNormals) { - var normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); - var encodedNormal = AttributeCompression.octEncode(normal, encodedNormalScratch); - tileVertices.push(encodedNormal.x, encodedNormal.y); - } + function getVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, vertex) { + vertex.height = height; + var normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); + AttributeCompression.octEncode(normal, vertex.encodedNormal); } - function addCorner( + function getCorner( terrainFillMesh, ellipsoid, u, v, cornerTile, cornerMesh, previousEdgeTiles, previousEdgeMeshes, nextEdgeTiles, nextEdgeMeshes, - hasVertexNormals, hasWebMercatorT, - tileVertices + vertex ) { var gotCorner = - addCornerFromEdge(terrainFillMesh, ellipsoid, previousEdgeMeshes, previousEdgeTiles, false, u, v, tileVertices) || - addCornerFromEdge(terrainFillMesh, ellipsoid, nextEdgeMeshes, nextEdgeTiles, true, u, v, tileVertices); + getCornerFromEdge(terrainFillMesh, ellipsoid, previousEdgeMeshes, previousEdgeTiles, false, u, v, vertex) || + getCornerFromEdge(terrainFillMesh, ellipsoid, nextEdgeMeshes, nextEdgeTiles, true, u, v, vertex); if (gotCorner) { - return; + return vertex; } var vertexIndex; @@ -799,8 +806,8 @@ define([ // northeast destination, southwest source vertexIndex = cornerTerrainMesh.westIndicesSouthToNorth[0]; } - addVertexFromTileAtCorner(cornerTerrainMesh, vertexIndex, u, v, tileVertices); - return; + getVertexFromTileAtCorner(cornerTerrainMesh, vertexIndex, u, v, vertex); + return vertex; } // There is no precise vertex available from the corner or from either adjacent edge. @@ -863,7 +870,8 @@ define([ height = (minimumHeight + maximumHeight) * 0.5; } - addVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, hasVertexNormals, hasWebMercatorT, tileVertices); + getVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, vertex); + return vertex; } function getClosestHeightToCorner( @@ -914,15 +922,16 @@ define([ return getHeightAtCorner(oppositeCornerMesh, oppositeCornerTile, oppositeCorner, u, v); } - function addEdge(terrainFillMesh, ellipsoid, edgeTiles, edgeMeshes, tileEdge, stride, tileVertices) { + function addEdge(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles, edgeMeshes, tileEdge) { for (var i = 0; i < edgeTiles.length; ++i) { - addEdgeMesh(terrainFillMesh, ellipsoid, edgeTiles[i], edgeMeshes[i], tileEdge, stride, tileVertices); + nextIndex = addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles[i], edgeMeshes[i], tileEdge); } + return nextIndex; } var edgeDetailsScratch = new TerrainTileEdgeDetails(); - function addEdgeMesh(terrainFillMesh, ellipsoid, edgeTile, edgeMesh, tileEdge, stride, tileVertices) { + function addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTile, edgeMesh, tileEdge) { var terrainMesh = edgeMesh; // Handle copying edges across the anti-meridian. @@ -942,15 +951,21 @@ define([ var vertices = edgeDetails.vertices; - var previousVertexIndex = tileVertices.length - stride; - - // Copy all except the corner vertices. var i; var u; var v; - var lastU = tileVertices[previousVertexIndex + 4]; - var lastV = tileVertices[previousVertexIndex + 5]; - for (i = 0; i < vertices.length; i += stride) { + var lastU; + var lastV; + + if (nextIndex > 0) { + encoding.decodeTextureCoordinates(typedArray, nextIndex - 1, uvScratch); + lastU = uvScratch.x; + lastV = uvScratch.y; + } + + // Copy all except the corner vertices. + var sourceStride = 6 + (terrainMesh.encoding.hasVertexNormals ? 2 : 0) + (terrainMesh.encoding.hasWebMercatorT ? 1 : 0); + for (i = 0; i < vertices.length; i += sourceStride) { u = vertices[i + 4]; v = vertices[i + 5]; if (Math.abs(u - lastU) < CesiumMath.EPSILON5 && Math.abs(v - lastV) < CesiumMath.EPSILON5) { @@ -966,14 +981,34 @@ define([ continue; } - var end = i + stride; - for (var j = i; j < end; ++j) { - tileVertices.push(vertices[j]); + var position = Cartesian3.fromElements(vertices[i], vertices[i + 1], vertices[i + 2], cartesianScratch); + var uv = Cartesian2.fromElements(u, v, uvScratch); + var height = vertices[i + 3]; + + var webMercatorT; + var normalIndex = 6; + if (terrainMesh.encoding.hasWebMercatorT) { + webMercatorT = vertices[i + 6]; + ++normalIndex; } + var encodedNormal; + if (terrainMesh.encoding.hasVertexNormals) { + encodedNormal = Cartesian2.fromElements(vertices[i + normalIndex], vertices[i + normalIndex + 1], encodedNormalScratch); + } else { + ellipsoid.geodeticSurfaceNormal(position, normalScratch); + encodedNormal = AttributeCompression.octEncode(normalScratch, encodedNormalScratch); + } + + encoding.encode(typedArray, nextIndex * encoding.getStride(), position, uv, height, encodedNormal, webMercatorT); + lastU = u; lastV = v; + + ++nextIndex; } + + return nextIndex; } function getNearestHeightOnEdge(meshes, tiles, isNext, edge, u, v) { @@ -1055,7 +1090,7 @@ define([ return undefined; } - function addCornerFromEdge(terrainFillMesh, ellipsoid, edgeMeshes, edgeTiles, isNext, u, v, tileVertices) { + function getCornerFromEdge(terrainFillMesh, ellipsoid, edgeMeshes, edgeTiles, isNext, u, v, vertex) { var edgeVertices; var compareU; var increasing; @@ -1100,7 +1135,7 @@ define([ var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch); if (targetUv.x === u && targetUv.y === v) { // Vertex is good! - addVertexFromTileAtCorner(sourceTerrainMesh, vertexIndex, u, v, tileVertices); + getVertexFromTileAtCorner(sourceTerrainMesh, vertexIndex, u, v, vertex); return true; } @@ -1124,12 +1159,12 @@ define([ if (vertexIndexIndex > 0 && vertexIndexIndex < edgeVertices.length) { // The corner falls between two vertices, so interpolate between them. - addInterpolatedVertexAtCorner(ellipsoid, sourceTile, terrainFillMesh.tile, sourceTerrainMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, tileVertices); + getInterpolatedVertexAtCorner(ellipsoid, sourceTile, terrainFillMesh.tile, sourceTerrainMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, vertex); return true; } } else { // Found a vertex that fits in the corner exactly. - addVertexFromTileAtCorner(sourceTerrainMesh, edgeVertices[vertexIndexIndex], u, v, tileVertices); + getVertexFromTileAtCorner(sourceTerrainMesh, edgeVertices[vertexIndexIndex], u, v, vertex); return true; } } From 90e67ef85ecfb43d265ce79a57a3bcaecec06245 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 6 Nov 2018 12:08:10 +1100 Subject: [PATCH 055/131] Fix incorrect default. --- Source/Scene/QuadtreePrimitive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 60238002357..2d792d19bb3 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -151,7 +151,7 @@ define([ * Setting this to true optimizes the zoom-out experience and provides more detail in * newly-exposed areas when panning. The down side is that it requires loading more tiles. * @type {Boolean} - * @default false + * @default true */ this.preloadAncestors = true; From bb92ea45c829eafd309807572406c3e8c2037cc7 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 6 Nov 2018 15:46:37 +1100 Subject: [PATCH 056/131] Eliminate another copy. --- Source/Core/TerrainMesh.js | 221 -------------------------------- Source/Scene/TerrainFillMesh.js | 106 +++++++++------ 2 files changed, 67 insertions(+), 260 deletions(-) diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js index b170adec9c0..f0612d4ceec 100644 --- a/Source/Core/TerrainMesh.js +++ b/Source/Core/TerrainMesh.js @@ -153,226 +153,5 @@ define([ this.northIndicesWestToEast = northIndicesWestToEast; } - var positionScratch = new Cartesian3(); - var encodedNormalScratch = new Cartesian2(); - var uvScratch = new Cartesian2(); - - function getVertex(encoding, vertices, index, result, resultIndex) { - resultIndex = defaultValue(resultIndex, result.length); - - encoding.decodePosition(vertices, index, positionScratch); - result[resultIndex++] = positionScratch.x; - result[resultIndex++] = positionScratch.y; - result[resultIndex++] = positionScratch.z; - - result[resultIndex++] = encoding.decodeHeight(vertices, index); - - encoding.decodeTextureCoordinates(vertices, index, uvScratch); - result[resultIndex++] = uvScratch.x; - result[resultIndex++] = uvScratch.y; - - if (encoding.hasWebMercatorT) { - result[resultIndex++] = encoding.decodeWebMercatorT(vertices, index); - } - - if (encoding.hasVertexNormals) { - encoding.getOctEncodedNormal(vertices, index, encodedNormalScratch); - result[resultIndex++] = encodedNormalScratch.x; - result[resultIndex++] = encodedNormalScratch.y; - } - - return resultIndex; - } - - function addVertex(ellipsoid, rectangle, encoding, u, v, height, octEncodedNormal, southMercatorY, oneOverMercatorHeight, result, resultIndex) { - var longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); - var latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); - Cartesian3.fromRadians(longitude, latitude, height, ellipsoid, positionScratch); - - result[resultIndex++] = positionScratch.x; - result[resultIndex++] = positionScratch.y; - result[resultIndex++] = positionScratch.z; - result[resultIndex++] = height; - result[resultIndex++] = u; - result[resultIndex++] = v; - - if (encoding.hasWebMercatorT) { - result[resultIndex++] = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - southMercatorY) * oneOverMercatorHeight; - } - - if (encoding.hasVertexNormals) { - result[resultIndex++] = octEncodedNormal.x; - result[resultIndex++] = octEncodedNormal.y; - } - } - - function transformTextureCoordinate(toMin, toMax, fromValue) { - return (fromValue - toMin) / (toMax - toMin); - } - - var previousVertexScratch = new Array(9); - var currentVertexScratch = new Array(9); - var normalScratch1 = new Cartesian3(); - var normalScratch2 = new Cartesian3(); - var normalScratch3 = new Cartesian3(); - - TerrainMesh.prototype.getEdgeVertices = function(tileEdge, thisRectangle, clipRectangle, ellipsoid, result) { - if (result === undefined) { - result = new TerrainTileEdgeDetails(); - } - var outputVertices = result.vertices; - var minimumHeight = result.minimumHeight; - var maximumHeight = result.maximumHeight; - - var clipRectangleUMin = (clipRectangle.west - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); - var clipRectangleUMax = (clipRectangle.east - thisRectangle.west) / (thisRectangle.east - thisRectangle.west); - var clipRectangleVMin = (clipRectangle.south - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); - var clipRectangleVMax = (clipRectangle.north - thisRectangle.south) / (thisRectangle.north - thisRectangle.south); - - var indices; - var first; - var second; - var edgeCoordinate; - var compareU; - - switch (tileEdge) { - case TileEdge.WEST: - indices = this.westIndicesSouthToNorth; - first = 0.0; - second = 1.0; - edgeCoordinate = transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, 0.0); - compareU = false; - break; - case TileEdge.NORTH: - indices = this.northIndicesWestToEast; - first = 0.0; - second = 1.0; - edgeCoordinate = transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, 1.0); - compareU = true; - break; - case TileEdge.EAST: - indices = this.eastIndicesNorthToSouth; - first = 1.0; - second = 0.0; - edgeCoordinate = transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, 1.0); - compareU = false; - break; - case TileEdge.SOUTH: - indices = this.southIndicesEastToWest; - first = 1.0; - second = 0.0; - edgeCoordinate = transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, 0.0); - compareU = true; - break; - } - - var encoding = this.encoding; - var vertices = this.vertices; - var southMercatorY; - var oneOverMercatorHeight; - var lastUOrV; - - var destinationStride = 6; - if (encoding.hasVertexNormals) { - destinationStride += 2; - } - if (encoding.hasWebMercatorT) { - ++destinationStride; - } - - if (encoding.hasWebMercatorT) { - southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(clipRectangle.south); - oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(clipRectangle.north) - southMercatorY); - } - - for (var i = 0; i < indices.length; ++i) { - var index = indices[i]; - - var uv = encoding.decodeTextureCoordinates(vertices, index, uvScratch); - var u = transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, uv.x); - var v = transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, uv.y); - var uOrV = compareU ? u : v; - - var inside = uOrV >= 0.0 && uOrV <= 1.0; - var crossedFirst = uOrV < first && lastUOrV > first || uOrV > first && lastUOrV < first; - var crossedSecond = uOrV < second && lastUOrV > second || uOrV > second && lastUOrV < second; - - var previousVertex = previousVertexScratch; - var currentVertex = currentVertexScratch; - - if (crossedFirst || crossedSecond) { - var lastIndex = indices[i - 1]; - getVertex(encoding, vertices, lastIndex, previousVertex, 0); - getVertex(encoding, vertices, index, currentVertex, 0); - } - - var ratio; - var interpolatedHeight; - var interpolatedU; - var interpolatedV; - var interpolatedOctEncodedNormal; - var previousNormal; - var currentNormal; - - if (crossedFirst) { - ratio = (first - uOrV) / (lastUOrV - uOrV); - interpolatedHeight = CesiumMath.lerp(currentVertex[3], previousVertex[3], ratio); - interpolatedU = compareU ? first : edgeCoordinate; - interpolatedV = compareU ? edgeCoordinate : first; - - if (encoding.hasVertexNormals) { - previousNormal = AttributeCompression.octDecode(previousVertex[destinationStride - 2], previousVertex[destinationStride - 1], normalScratch1); - currentNormal = AttributeCompression.octDecode(currentVertex[destinationStride - 2], currentVertex[destinationStride - 1], normalScratch2); - Cartesian3.lerp(currentNormal, previousNormal, ratio, normalScratch3); - Cartesian3.normalize(normalScratch3, normalScratch3); - interpolatedOctEncodedNormal = AttributeCompression.octEncode(normalScratch3, encodedNormalScratch); - } - - addVertex(ellipsoid, clipRectangle, encoding, interpolatedU, interpolatedV, interpolatedHeight, interpolatedOctEncodedNormal, southMercatorY, oneOverMercatorHeight, outputVertices, outputVertices.length); - minimumHeight = Math.min(minimumHeight, interpolatedHeight); - maximumHeight = Math.max(maximumHeight, interpolatedHeight); - } - - if (inside) { - getVertex(encoding, vertices, index, outputVertices, outputVertices.length); - var vertexStart = outputVertices.length - destinationStride; - outputVertices[vertexStart + 4] = compareU ? transformTextureCoordinate(clipRectangleUMin, clipRectangleUMax, outputVertices[vertexStart + 4]) : edgeCoordinate; - outputVertices[vertexStart + 5] = compareU ? edgeCoordinate : transformTextureCoordinate(clipRectangleVMin, clipRectangleVMax, outputVertices[vertexStart + 5]); - if (encoding.hasWebMercatorT) { - var latitude = CesiumMath.lerp(clipRectangle.south, clipRectangle.north, v); - outputVertices[vertexStart + 6] = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - southMercatorY) * oneOverMercatorHeight; - } - minimumHeight = Math.min(minimumHeight, outputVertices[vertexStart + 3]); - maximumHeight = Math.max(maximumHeight, outputVertices[vertexStart + 3]); - } - - if (crossedSecond) { - ratio = (second - uOrV) / (lastUOrV - uOrV); - interpolatedHeight = CesiumMath.lerp(currentVertex[3], previousVertex[3], ratio); - interpolatedU = compareU ? second : edgeCoordinate; - interpolatedV = compareU ? edgeCoordinate : second; - - if (encoding.hasVertexNormals) { - previousNormal = AttributeCompression.octDecode(previousVertex[destinationStride - 2], previousVertex[destinationStride - 1], normalScratch1); - currentNormal = AttributeCompression.octDecode(currentVertex[destinationStride - 2], currentVertex[destinationStride - 1], normalScratch2); - Cartesian3.lerp(currentNormal, previousNormal, ratio, normalScratch3); - Cartesian3.normalize(normalScratch3, normalScratch3); - interpolatedOctEncodedNormal = AttributeCompression.octEncode(normalScratch3, encodedNormalScratch); - } - - addVertex(ellipsoid, clipRectangle, encoding, interpolatedU, interpolatedV, interpolatedHeight, interpolatedOctEncodedNormal, southMercatorY, oneOverMercatorHeight, outputVertices, outputVertices.length); - minimumHeight = Math.min(minimumHeight, interpolatedHeight); - maximumHeight = Math.max(maximumHeight, interpolatedHeight); - } - - lastUOrV = uOrV; - } - - result.minimumHeight = minimumHeight; - result.maximumHeight = maximumHeight; - - return result; - }; - return TerrainMesh; }); diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 26333ef59a3..02c38fd7254 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -533,10 +533,6 @@ define([ encoding.encode(typedArray, nextIndex * encoding.getStride(), center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); ++nextIndex; - if (nextIndex * encoding.getStride() > typedArray.length) { - debugger; - } - var vertexCount = nextIndex; var indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex @@ -759,6 +755,8 @@ define([ Cartesian3.normalize(normal, normal); AttributeCompression.octEncode(normal, vertex.encodedNormal); } else { + // TODO: do we need to actually compute a normal? + // It's probably going to be unused to 0,0 is just as good. normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); AttributeCompression.octEncode(normal, vertex.encodedNormal); } @@ -929,11 +927,7 @@ define([ return nextIndex; } - var edgeDetailsScratch = new TerrainTileEdgeDetails(); - function addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTile, edgeMesh, tileEdge) { - var terrainMesh = edgeMesh; - // Handle copying edges across the anti-meridian. var sourceRectangle = edgeTile.rectangle; if (tileEdge === TileEdge.EAST && terrainFillMesh.tile.x === 0) { @@ -946,14 +940,8 @@ define([ sourceRectangle.east += CesiumMath.TWO_PI; } - edgeDetailsScratch.clear(); - var edgeDetails = terrainMesh.getEdgeVertices(tileEdge, sourceRectangle, terrainFillMesh.tile.rectangle, ellipsoid, edgeDetailsScratch); + var targetRectangle = terrainFillMesh.tile.rectangle; - var vertices = edgeDetails.vertices; - - var i; - var u; - var v; var lastU; var lastV; @@ -963,13 +951,57 @@ define([ lastV = uvScratch.y; } - // Copy all except the corner vertices. - var sourceStride = 6 + (terrainMesh.encoding.hasVertexNormals ? 2 : 0) + (terrainMesh.encoding.hasWebMercatorT ? 1 : 0); - for (i = 0; i < vertices.length; i += sourceStride) { - u = vertices[i + 4]; - v = vertices[i + 5]; + var indices; + var compareU; + + switch (tileEdge) { + case TileEdge.WEST: + indices = edgeMesh.westIndicesSouthToNorth; + compareU = false; + break; + case TileEdge.NORTH: + indices = edgeMesh.northIndicesWestToEast; + compareU = true; + break; + case TileEdge.EAST: + indices = edgeMesh.eastIndicesNorthToSouth; + compareU = false; + break; + case TileEdge.SOUTH: + indices = edgeMesh.southIndicesEastToWest; + compareU = true; + break; + } + + var sourceTile = edgeTile; + var targetTile = terrainFillMesh.tile; + var sourceEncoding = edgeMesh.encoding; + var sourceVertices = edgeMesh.vertices; + var targetStride = encoding.getStride(); + + var southMercatorY; + var oneOverMercatorHeight; + if (sourceEncoding.hasWebMercatorT) { + southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(targetRectangle.south); + oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(targetRectangle.north) - southMercatorY); + } + + for (var i = 0; i < indices.length; ++i) { + var index = indices[i]; + + var uv = sourceEncoding.decodeTextureCoordinates(sourceVertices, index, uvScratch); + transformTextureCoordinates(sourceTile, targetTile, uv, uv); + var u = uv.x; + var v = uv.y; + var uOrV = compareU ? u : v; + + if (uOrV < 0.0 || uOrV > 1.0) { + // Vertex is outside the target tile - skip it. + continue; + } + if (Math.abs(u - lastU) < CesiumMath.EPSILON5 && Math.abs(v - lastV) < CesiumMath.EPSILON5) { - // Vertex is very close to the previous one, so skip it. + // Vertex is very close to the previous one - skip it. continue; } @@ -981,29 +1013,25 @@ define([ continue; } - var position = Cartesian3.fromElements(vertices[i], vertices[i + 1], vertices[i + 2], cartesianScratch); - var uv = Cartesian2.fromElements(u, v, uvScratch); - var height = vertices[i + 3]; + var position = sourceEncoding.decodePosition(sourceVertices, index, cartesianScratch); + var height = sourceEncoding.decodeHeight(sourceVertices, index); - var webMercatorT; - var normalIndex = 6; - if (terrainMesh.encoding.hasWebMercatorT) { - webMercatorT = vertices[i + 6]; - ++normalIndex; - } - - var encodedNormal; - if (terrainMesh.encoding.hasVertexNormals) { - encodedNormal = Cartesian2.fromElements(vertices[i + normalIndex], vertices[i + normalIndex + 1], encodedNormalScratch); + var normal; + if (sourceEncoding.hasVertexNormals) { + normal = sourceEncoding.getOctEncodedNormal(sourceVertices, index, octEncodedNormalScratch); } else { - ellipsoid.geodeticSurfaceNormal(position, normalScratch); - encodedNormal = AttributeCompression.octEncode(normalScratch, encodedNormalScratch); + normal = octEncodedNormalScratch; + normal.x = 0.0; + normal.y = 0.0; } - encoding.encode(typedArray, nextIndex * encoding.getStride(), position, uv, height, encodedNormal, webMercatorT); + var webMercatorT = v; + if (sourceEncoding.hasWebMercatorT) { + var latitude = CesiumMath.lerp(targetRectangle.south, targetRectangle.north, v); + webMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - southMercatorY) * oneOverMercatorHeight; + } - lastU = u; - lastV = v; + encoding.encode(typedArray, nextIndex * targetStride, position, uv, height, normal, webMercatorT); ++nextIndex; } From 1a419c808f26d83ebc58c0b64026ac061ac86d18 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 6 Nov 2018 15:59:22 +1100 Subject: [PATCH 057/131] Remove unnecessary check and allocation. --- Source/Scene/TerrainFillMesh.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 02c38fd7254..ae998cd52d3 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -694,10 +694,6 @@ define([ v = 1.0; } - if (!defined(result)) { - return new Cartesian2(u, v); - } - result.x = u; result.y = v; return result; From b253b669a729ed1c3f5b3b1f7477584d6568e1e7 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 9 Nov 2018 19:23:41 +1100 Subject: [PATCH 058/131] Cleanup. --- Source/Core/CesiumTerrainProvider.js | 15 ++++++++++----- Source/Core/HeightmapTessellator.js | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index bf73ec42905..18d9efa9386 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -599,6 +599,15 @@ define([ }); } + /** + * Gets the level of the nearest ancestor of this tile that Bounding Volume Hierarchy (BVH) + * data. + * + * @param {Number} x The X coordinate of the tile. + * @param {Number} y The Y coordinate of the tile. + * @param {Number} level The level of the tile. + * @returns {Number} The level of the nearest BVH level less than or equal to level, or -1 if no BVH data is available for this tile. + */ CesiumTerrainProvider.prototype.getNearestBvhLevel = function(x, y, level) { var layers = this._layers; var layerToUse; @@ -616,11 +625,7 @@ define([ } } - if (!defined(layerToUse)) { - return when.reject(new RuntimeError('Terrain tile doesn\'t exist')); - } - - if (!layerToUse.hasBvh) { + if (!defined(layerToUse) || !layerToUse.hasBvh) { return -1; } diff --git a/Source/Core/HeightmapTessellator.js b/Source/Core/HeightmapTessellator.js index ddbcb11a05a..ab688ede35a 100644 --- a/Source/Core/HeightmapTessellator.js +++ b/Source/Core/HeightmapTessellator.js @@ -219,7 +219,7 @@ define([ var granularityX = rectangleWidth / (width - 1); var granularityY = rectangleHeight / (height - 1); - if (!isGeographic) { + if (!isGeographic) { rectangleWidth *= oneOverGlobeSemimajorAxis; rectangleHeight *= oneOverGlobeSemimajorAxis; } @@ -448,7 +448,19 @@ define([ westIndicesSouthToNorth.push((height - i2) * arrayWidth + 1); } } else { - westIndicesSouthToNorth = southIndicesEastToWest = eastIndicesNorthToSouth = northIndicesWestToEast = []; + northIndicesWestToEast = []; + southIndicesEastToWest = []; + for (var i3 = 0; i3 < width; ++i3) { + northIndicesWestToEast.push(i3); + southIndicesEastToWest.push(width * height - 1 - i3); + } + + westIndicesSouthToNorth = []; + eastIndicesNorthToSouth = []; + for (var i4 = 0; i4 < height; ++i4) { + eastIndicesNorthToSouth.push((i4 + 1) * width - 1); + westIndicesSouthToNorth.push((height - i4 - 1) * width ); + } } return { From 685dee3bb062bfac08b69915dadee25ef1b628b3 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 9 Nov 2018 19:48:26 +1100 Subject: [PATCH 059/131] More cleanup. --- Source/Core/QuantizedMeshTerrainData.js | 29 ------------------------- 1 file changed, 29 deletions(-) diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index f73b83baadf..a4e542d40a1 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -380,11 +380,6 @@ define([ }; var upsampleTaskProcessor = new TaskProcessor('upsampleQuantizedTerrainMesh'); - upsampleTaskProcessor.scheduleTask({}); - - // var startTime; - // var notDeferredTime; - // var stopTime; /** * Upsamples this terrain data for use by a descendant tile. The resulting instance will contain a subset of the @@ -435,13 +430,6 @@ define([ return undefined; } - // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { - // if (startTime !== undefined) { - // console.log('**************** wut'); - // } - // startTime = Date.now(); - // } - var isEastChild = thisX * 2 !== descendantX; var isNorthChild = thisY * 2 === descendantY; @@ -449,10 +437,6 @@ define([ var childRectangle = tilingScheme.tileXYToRectangle(descendantX, descendantY, descendantLevel); var upsamplePromise = upsampleTaskProcessor.scheduleTask({ - startTime : Date.now(), - // level : descendantLevel, - // x : descendantX, - // y : descendantY, vertices : mesh.vertices, vertexCountWithoutSkirts : this._vertexCountWithoutSkirts, indices : mesh.indices, @@ -468,18 +452,10 @@ define([ }); if (!defined(upsamplePromise)) { - // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { - // console.log('woe'); - // } // Postponed return undefined; } - // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { - // console.log('Scheduled'); - // notDeferredTime = Date.now(); - // } - var shortestSkirt = Math.min(this._westSkirtHeight, this._eastSkirtHeight); shortestSkirt = Math.min(shortestSkirt, this._southSkirtHeight); shortestSkirt = Math.min(shortestSkirt, this._northSkirtHeight); @@ -491,11 +467,6 @@ define([ var credits = this._credits; return when(upsamplePromise).then(function(result) { - // if (descendantLevel === 10 && descendantX === 349 && descendantY === 301) { - // stopTime = Date.now(); - // console.log('core upsample: ' + (stopTime - startTime) + ' / ' + (stopTime - notDeferredTime)); - // } - var quantizedVertices = new Uint16Array(result.vertices); var indicesTypedArray = IndexDatatype.createTypedArray(quantizedVertices.length / 3, result.indices); var encodedNormals; From 0d33623989247bcae6b76b8482c5ae8b5340d429 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 12 Nov 2018 13:08:22 +1100 Subject: [PATCH 060/131] Remove TerrainTileEdgeDetails.js. --- Source/Core/QuantizedMeshTerrainData.js | 1 - Source/Core/TerrainMesh.js | 16 ++-------------- Source/Core/TerrainTileEdgeDetails.js | 17 ----------------- Source/Scene/GlobeSurfaceTileProvider.js | 2 -- Source/Scene/TerrainFillMesh.js | 2 -- 5 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 Source/Core/TerrainTileEdgeDetails.js diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index a4e542d40a1..b40597dcf35 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -499,7 +499,6 @@ define([ }; var maxShort = 32767; - var barycentricCoordinateScratch = new Cartesian3(); /** diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js index f0612d4ceec..bc880615db6 100644 --- a/Source/Core/TerrainMesh.js +++ b/Source/Core/TerrainMesh.js @@ -1,21 +1,9 @@ define([ - './AttributeCompression', - './Cartesian2', './Cartesian3', - './defaultValue', - './Math', - './TerrainTileEdgeDetails', - './TileEdge', - './WebMercatorProjection' + './defaultValue' ], function( - AttributeCompression, - Cartesian2, Cartesian3, - defaultValue, - CesiumMath, - TerrainTileEdgeDetails, - TileEdge, - WebMercatorProjection) { + defaultValue) { 'use strict'; /** diff --git a/Source/Core/TerrainTileEdgeDetails.js b/Source/Core/TerrainTileEdgeDetails.js deleted file mode 100644 index 0b3db614834..00000000000 --- a/Source/Core/TerrainTileEdgeDetails.js +++ /dev/null @@ -1,17 +0,0 @@ -define([], function() { - 'use strict'; - - function TerrainTileEdgeDetails() { - this.vertices = []; - this.minimumHeight = Number.MAX_VALUE; - this.maximumHeight = -Number.MAX_VALUE; - } - - TerrainTileEdgeDetails.prototype.clear = function() { - this.vertices.length = 0; - this.minimumHeight = Number.MAX_VALUE; - this.maximumHeight = -Number.MAX_VALUE; - }; - - return TerrainTileEdgeDetails; -}); diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 4629393d686..d21fea4e82e 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -30,7 +30,6 @@ define([ '../Core/TerrainEncoding', '../Core/TerrainMesh', '../Core/TerrainQuantization', - '../Core/TerrainTileEdgeDetails', '../Core/TileEdge', '../Core/Visibility', '../Core/WebMercatorProjection', @@ -87,7 +86,6 @@ define([ TerrainEncoding, TerrainMesh, TerrainQuantization, - TerrainTileEdgeDetails, TileEdge, Visibility, WebMercatorProjection, diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index ae998cd52d3..830557f105c 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -15,7 +15,6 @@ define([ '../Core/TileEdge', '../Core/TerrainEncoding', '../Core/TerrainMesh', - '../Core/TerrainTileEdgeDetails', '../Core/WebMercatorProjection', '../Renderer/Buffer', '../Renderer/BufferUsage', @@ -39,7 +38,6 @@ define([ TileEdge, TerrainEncoding, TerrainMesh, - TerrainTileEdgeDetails, WebMercatorProjection, Buffer, BufferUsage, From 11eb02c6bdf05d349c0f36521c82bbcf3195866d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 12 Nov 2018 15:47:24 +1100 Subject: [PATCH 061/131] The cleanup fun never ends. --- Source/Core/QuantizedMeshTerrainData.js | 6 ++---- Source/Core/TileEdge.js | 3 +++ Source/Scene/GlobeSurfaceTile.js | 1 - Source/Scene/GlobeSurfaceTileProvider.js | 13 +------------ .../createVerticesFromQuantizedTerrainMesh.js | 5 ----- .../Workers/upsampleQuantizedTerrainMesh.js | 19 ------------------- 6 files changed, 6 insertions(+), 41 deletions(-) diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index b40597dcf35..1f3857fe07a 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -13,8 +13,7 @@ define([ './OrientedBoundingBox', './TaskProcessor', './TerrainEncoding', - './TerrainMesh', - './TileEdge' + './TerrainMesh' ], function( when, BoundingSphere, @@ -30,8 +29,7 @@ define([ OrientedBoundingBox, TaskProcessor, TerrainEncoding, - TerrainMesh, - TileEdge) { + TerrainMesh) { 'use strict'; /** diff --git a/Source/Core/TileEdge.js b/Source/Core/TileEdge.js index 979d1f7ec13..d1d54ebe0b3 100644 --- a/Source/Core/TileEdge.js +++ b/Source/Core/TileEdge.js @@ -2,6 +2,9 @@ define([ ], function() { 'use strict'; + /** + * @private + */ var TileEdge = { WEST: 0, NORTH: 1, diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 34d669ef864..831a7e74f58 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -292,7 +292,6 @@ define([ // The ancestor that holds the BVH data isn't loaded yet; load it (terrain only!) instead of this tile. GlobeSurfaceTile.processStateMachine(ancestor, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, true); return; - } } } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index d21fea4e82e..963301307c7 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -30,7 +30,6 @@ define([ '../Core/TerrainEncoding', '../Core/TerrainMesh', '../Core/TerrainQuantization', - '../Core/TileEdge', '../Core/Visibility', '../Core/WebMercatorProjection', '../Renderer/Buffer', @@ -86,7 +85,6 @@ define([ TerrainEncoding, TerrainMesh, TerrainQuantization, - TileEdge, Visibility, WebMercatorProjection, Buffer, @@ -754,15 +752,6 @@ define([ // The renderable tile may have previously deferred to an ancestor. // But we know it's renderable now, so mark it as such. nearestRenderableTile.data.renderableTile = undefined; - - var myRectangle = tile.rectangle; - var ancestorRectangle = nearestRenderableTile.rectangle; - var ancestorSubset = surfaceTile.renderableTileSubset; - - ancestorSubset.x = (myRectangle.west - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); - ancestorSubset.y = (myRectangle.south - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); - ancestorSubset.z = (myRectangle.east - ancestorRectangle.west) / (ancestorRectangle.east - ancestorRectangle.west); - ancestorSubset.w = (myRectangle.north - ancestorRectangle.south) / (ancestorRectangle.north - ancestorRectangle.south); } else { this._hasLoadedTilesThisFrame = true; surfaceTile.renderableTile = undefined; @@ -807,7 +796,7 @@ define([ // be deemed to be much closer to the camera than it really is, causing us to select tiles that are too detailed. // Loading too-detailed tiles is super expensive, so we don't want to do that. We don't know where the child // tile really lies within the parent range of heights, but we _do_ know the child tile can't be any closer than - // the ancestor height surface (min or max) that is _farthest away_ from the camera. So if we computed distance + // the ancestor height surface (min or max) that is _farthest away_ from the camera. So if we compute distance // based that conservative metric, we may end up loading tiles that are not detailed enough, but that's much // better (faster) than loading tiles that are too detailed. diff --git a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js index fa863943249..3270550a005 100644 --- a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js +++ b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js @@ -46,8 +46,6 @@ define([ var scratchFromENU = new Matrix4(); function createVerticesFromQuantizedTerrainMesh(parameters, transferableObjects) { - // var startTime = performance.now(); - var quantizedVertices = parameters.quantizedVertices; var quantizedVertexCount = quantizedVertices.length / 3; var octEncodedNormals = parameters.octEncodedNormals; @@ -226,9 +224,6 @@ define([ transferableObjects.push(vertexBuffer.buffer, indexBuffer.buffer); - // var stopTime = performance.now(); - // console.log('createVerticesFromQuantizedTerrainMesh time: ' + (stopTime - startTime)); - return { vertices : vertexBuffer.buffer, indices : indexBuffer.buffer, diff --git a/Source/Workers/upsampleQuantizedTerrainMesh.js b/Source/Workers/upsampleQuantizedTerrainMesh.js index 20ca51b95fc..c78b0471a09 100644 --- a/Source/Workers/upsampleQuantizedTerrainMesh.js +++ b/Source/Workers/upsampleQuantizedTerrainMesh.js @@ -49,21 +49,7 @@ define([ var decodeTexCoordsScratch = new Cartesian2(); var octEncodedNormalScratch = new Cartesian3(); - // var startTime; - function upsampleQuantizedTerrainMesh(parameters, transferableObjects) { - // console.log('L' + parameters.level + 'X' + parameters.x + 'Y' + parameters.y); - // if (parameters.level === 10 && parameters.x === 349 && parameters.y === 301) { - // if (startTime !== undefined) { - // console.log('****** wut'); - // } - // startTime = Date.now(); - // console.log('time until start: ' + (startTime - parameters.startTime)); - // } - - // var startTime = Date.now(); - // console.log('time until start: ' + (startTime - parameters.startTime)); - var isEastChild = parameters.isEastChild; var isNorthChild = parameters.isNorthChild; @@ -333,11 +319,6 @@ define([ transferableObjects.push(vertices.buffer, indicesTypedArray.buffer); } - // if (parameters.level === 10 && parameters.x === 349 && parameters.y === 301) { - // var stopTime = Date.now(); - // console.log('upsample time: ' + (stopTime - startTime)); - // } - return { vertices : vertices.buffer, encodedNormals : encodedNormals, From 8222a0a3d0e0fce095f2a6845ff8abdf584a30cc Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 27 Nov 2018 15:39:34 +1100 Subject: [PATCH 062/131] Remove unneeded field. --- Source/Scene/GlobeSurfaceTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 831a7e74f58..22e105b40d5 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -84,7 +84,6 @@ define([ this._bvh = undefined; this.renderableTile = undefined; - this.renderableTileSubset = new Cartesian4(); /** * A bounding region used to estimate distance to the tile. The horizontal bounds are always tight-fitting, From 8e1d45c181a14443e908f3dd40434216d916cc79 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 27 Nov 2018 22:14:41 +1100 Subject: [PATCH 063/131] Eliminate frameRendered and frameVisited from QuadtreeTile. --- Source/Scene/QuadtreePrimitive.js | 104 ++++++++++++++++------------ Source/Scene/QuadtreeTile.js | 2 - Source/Scene/TerrainFillMesh.js | 2 +- Source/Scene/TileSelectionResult.js | 53 ++++++++++++-- 4 files changed, 108 insertions(+), 53 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 2d792d19bb3..e054c659b2f 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -634,47 +634,55 @@ define([ var northeastChild = tile.northeastChild; var lastFrame = primitive._lastSelectionFrameNumber; + var lastFrameSelectionResult = tile._lastSelectionResultFrame === lastFrame ? tile._lastSelectionResult : TileSelectionResult.NONE; if (meetsSse || ancestorMeetsSse) { // This tile (or an ancestor) is the one we want to render this frame, but we'll do different things depending // on the state of this tile and on what we did _last_ frame. - // 1. If this tile is completely loaded (terrain _and_ imagery), render it. - // TODO: technically we only need to ensure that the geometry and any imagery layers previously - // loaded on descendants are loaded on this tile. It doesn't need to be really, totally - // done loading. Hard to determine this though. - // 2. If this tile's children were not visited last frame because this tile was culled - // or because this tile or an ancestor tile met the SSE, then render this tile, or a fill if - // this tile isn't renderable yet. - var oneCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE; - var twoNoChildrenVisitedLastFrame = - southwestChild._frameVisited !== lastFrame && - southeastChild._frameVisited !== lastFrame && - northwestChild._frameVisited !== lastFrame && - northeastChild._frameVisited !== lastFrame; - - if (oneCompletelyLoaded || twoNoChildrenVisitedLastFrame) { + // We can render it if _any_ of the following are true: + // 1. We rendered it (or kicked it) last frame. + // 2. a) Terrain is ready, and + // b) All necessary iagery is ready. Necessary imagery is imagery that was rendered with this tile + // or any descendants last frame. Such imagery is required because rendering this tile without + // it would cause detail to disappear. But determining which imagery is actually necessary is + // challenging, so instead we are currently requiring that _all_ imagery is ready. + // 3. This tile was culled last frame, or it wasn't even visited because an ancestor was culled. + // + // Note that even if we decide to render a tile here, it may later get "kicked" in favor of an ancestor. + + var oneRenderedLastFrame = TileSelectionResult.originalResult(lastFrameSelectionResult) === TileSelectionResult.RENDERED; + var twoCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE; + var threeCulledOrNotVisited = lastFrameSelectionResult === TileSelectionResult.CULLED || lastFrameSelectionResult === TileSelectionResult.NONE; + + if (oneRenderedLastFrame || twoCompletelyLoaded || threeCulledOrNotVisited) { // Only load this tile if it (not just an ancestor) meets the SSE. if (meetsSse) { queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); } addTileToRenderList(primitive, tile, nearestRenderableTile); + traversalDetails.allAreRenderable = tile.renderable; + traversalDetails.anyWereRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED; + traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; + tile._lastSelectionResultFrame = frameState.frameNumber; tile._lastSelectionResult = TileSelectionResult.RENDERED; - traversalDetails.allAreRenderable = tile.renderable; - traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; - traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; + if (!traversalDetails.anyWereRenderedLastFrame) { + // Tile is newly-rendered this frame, so update its heights. + primitive._tileToUpdateHeights.push(tile); + } + return; } - // 3. Otherwise, we can't render this tile (or its fill) because doing so would cause detail to disappear - // that was visible last frame. Instead, keep rendering any still-visible descendants that were rendered - // last frame and render fills for newly-visible descendants. E.g. if we were rendering level 15 last - // frame but this frame we want level 14 and the closest renderable level <= 14 is 0, rendering level - // zero would be pretty jarring so instead we keep rendering level 15 even though its SSE is better - // than required. So fall through to continue traversal... + // Otherwise, we can't render this tile (or its fill) because doing so would cause detail to disappear + // that was visible last frame. Instead, keep rendering any still-visible descendants that were rendered + // last frame and render fills for newly-visible descendants. E.g. if we were rendering level 15 last + // frame but this frame we want level 14 and the closest renderable level <= 14 is 0, rendering level + // zero would be pretty jarring so instead we keep rendering level 15 even though its SSE is better + // than required. So fall through to continue traversal... ancestorMeetsSse = true; // Load this blocker tile with high priority, but only if this tile (not just an ancestor) meets the SSE. @@ -690,9 +698,6 @@ define([ northwestChild.upsampledFromParent && northeastChild.upsampledFromParent; if (allAreUpsampled) { - tile._lastSelectionResultFrame = frameState.frameNumber; - tile._lastSelectionResult = TileSelectionResult.RENDERED; - // No point in rendering the children because they're all upsampled. Render this tile instead. addTileToRenderList(primitive, tile, nearestRenderableTile); @@ -706,8 +711,17 @@ define([ primitive._tileReplacementQueue.markTileRendered(northeastChild); traversalDetails.allAreRenderable = tile.renderable; - traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + traversalDetails.anyWereRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED; traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; + + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.RENDERED; + + if (!traversalDetails.anyWereRenderedLastFrame) { + // Tile is newly-rendered this frame, so update its heights. + primitive._tileToUpdateHeights.push(tile); + } + return; } @@ -719,6 +733,7 @@ define([ var loadIndexLow = primitive._tileLoadQueueLow.length; var loadIndexMedium = primitive._tileLoadQueueMedium.length; var loadIndexHigh = primitive._tileLoadQueueHigh.length; + var tilesToUpdateHeightsIndex = primitive._tileToUpdateHeights.length; // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile, ancestorMeetsSse, traversalDetails); @@ -743,14 +758,15 @@ define([ for (var i = firstRenderedDescendantIndex; i < renderList.length; ++i) { var workTile = renderList[i]; while (workTile !== undefined && workTile._lastSelectionResult !== TileSelectionResult.KICKED && workTile !== tile) { - workTile._lastSelectionResult = TileSelectionResult.KICKED; + workTile._lastSelectionResult = TileSelectionResult.kick(workTile._lastSelectionResult); workTile = workTile.parent; } } - // Remove all descendants from the render list. + // Remove all descendants from the render list and update list. primitive._tilesToRender.length = firstRenderedDescendantIndex; primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; + primitive._tileToUpdateHeights.length = tilesToUpdateHeightsIndex; addTileToRenderList(primitive, tile, nearestRenderableTile); tile._lastSelectionResult = TileSelectionResult.RENDERED; @@ -758,7 +774,7 @@ define([ // If we're waiting on heaps of descendants, the above will take too long. So in that case, // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting // on this tile. Keep doing this until we actually manage to render this tile. - var wasRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + var wasRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED; if (!wasRenderedLastFrame && notYetRenderableCount > primitive.loadingDescendantLimit) { // Remove all descendants from the load queues. primitive._tileLoadQueueLow.length = loadIndexLow; @@ -768,12 +784,13 @@ define([ traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; } - // TODO: consolidate _frameRendered and _lastSelectionResultFrame. - // Will probably need to set anyWereRenderedLastFrame earlier, before we overwrite - // with the new selection result. - traversalDetails.allAreRenderable = tile.renderable; - traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + traversalDetails.anyWereRenderedLastFrame = wasRenderedLastFrame; + + if (!wasRenderedLastFrame) { + // Tile is newly-rendered this frame, so update its heights. + primitive._tileToUpdateHeights.push(tile); + } ++debug.tilesWaitingForChildren; } @@ -797,7 +814,7 @@ define([ queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); traversalDetails.allAreRenderable = tile.renderable; - traversalDetails.anyWereRenderedLastFrame = tile._frameRendered === primitive._lastSelectionFrameNumber; + traversalDetails.anyWereRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED; traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; } @@ -844,8 +861,6 @@ define([ } function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { - tile._frameVisited = frameState.frameNumber; - if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { return visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails); } @@ -973,7 +988,11 @@ define([ while (tilesToUpdateHeights.length > 0) { var tile = tilesToUpdateHeights[0]; if (tile.state !== QuadtreeTileLoadState.DONE) { - tryNextFrame.push(tile); + // Tile isn't loaded yet, so try again next frame if this tile is still + // being rendered. + if (tile._lastSelectionResultFrame === primitive._lastSelectionFrameNumber && tile._lastSelectionResult === TileSelectionResult.RENDERED) { + tryNextFrame.push(tile); + } tilesToUpdateHeights.shift(); primitive._lastTileIndex = 0; continue; @@ -1075,11 +1094,6 @@ define([ for (var i = 0, len = tilesToRender.length; i < len; ++i) { var tile = tilesToRender[i]; tileProvider.showTileThisFrame(tile, frameState, nearestRenderableTiles[i]); - - if (tile._frameRendered !== frameState.frameNumber - 1) { - tilesToUpdateHeights.push(tile); - } - tile._frameRendered = frameState.frameNumber; } } diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 222bfd03481..3544f42f222 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -72,8 +72,6 @@ define([ this._customData = []; this._frameUpdated = undefined; - this._frameRendered = undefined; - this._frameVisited = undefined; this._lastSelectionResult = TileSelectionResult.CULLED; this._lastSelectionResultFrame = undefined; this._loadedCallbacks = {}; diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 830557f105c..7df2f53491b 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -145,7 +145,7 @@ define([ } var tile = startTile; - while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || tile._lastSelectionResult === TileSelectionResult.KICKED || tile._lastSelectionResult === TileSelectionResult.CULLED)) { + while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || TileSelectionResult.wasKicked(tile._lastSelectionResult) || tile._lastSelectionResult === TileSelectionResult.CULLED)) { // This tile wasn't visited or it was visited and then kicked, so walk up to find the closest ancestor that was rendered. // We also walk up if the tile was culled, because if siblings were kicked an ancestor may have been rendered. if (downOnly) { diff --git a/Source/Scene/TileSelectionResult.js b/Source/Scene/TileSelectionResult.js index 1caf42a68dd..1773db78167 100644 --- a/Source/Scene/TileSelectionResult.js +++ b/Source/Scene/TileSelectionResult.js @@ -7,26 +7,69 @@ define([ * @private */ var TileSelectionResult = { + /** + * There was no selection result, perhaps because the tile wasn't visited + * last frame. + */ + NONE: 0, + /** * This tile was deemed not visible and culled. */ - CULLED: 0, + CULLED: 1, /** * The tile was selected for rendering. */ - RENDERED: 1, + RENDERED: 2, /** * This tile did not meet the required screen-space error and was refined. */ - REFINED: 2, + REFINED: 3, /** - * This tile was originally refined or rendered, but it or its descendants got kicked out of the render list + * This tile was originally rendered, but it got kicked out of the render list * in favor of an ancestor because it is not yet renderable. */ - KICKED: 3 + RENDERED_AND_KICKED: 2 | 4, + + /** + * This tile was originally refined, but its rendered descendants got kicked out of the + * render list in favor of an ancestor because it is not yet renderable. + */ + REFINED_AND_KICKED: 3 | 4, + + /** + * Determines if a selection result indicates that this tile or its descendants were + * kicked from the render list. In other words, if it is RENDERED_AND_KICKED + * or REFINED_AND_KICKED. + * + * @param {TileSelectionResult} value The selection result to test. + * @returns {Boolean} true if the tile was kicked, no matter if it was originally rendered or refined. + */ + wasKicked: function(value) { + return value >= TileSelectionResult.RENDERED_AND_KICKED; + }, + + /** + * Determines the original selection result prior to being kicked. + * If the tile wasn't kicked, the original value is returned. + * @param {TileSelectionResult} value The selection result. + * @returns {TileSelectionResult} The original selection result prior to kicking. + */ + originalResult: function(value) { + return value & 3; + }, + + /** + * Converts this selection result to a kick. + * @param {TileSelectionResult} value The original selection result. + * @returns {TileSelectionResult} The kicked form of the selection result. + */ + kick: function(value) { + return value | 4; + } }; return TileSelectionResult; From a756cb06001ff809fd737ce9c7bbf69c1c6bdb81 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 28 Nov 2018 16:53:47 +1100 Subject: [PATCH 064/131] Remove unused and expensive tileLoadedEvent. --- Source/Scene/Globe.js | 12 ------------ Source/Scene/GlobeSurfaceTileProvider.js | 19 ------------------- Source/Scene/QuadtreeTile.js | 2 +- Specs/DataSources/EntityClusterSpec.js | 1 - Specs/DataSources/PointVisualizerSpec.js | 1 - Specs/Scene/ModelSpec.js | 1 - Specs/createGlobe.js | 1 - 7 files changed, 1 insertion(+), 36 deletions(-) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index f57c52bc6f2..d21368be92a 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -275,18 +275,6 @@ define([ return this._surface.tileProvider.imageryLayersUpdatedEvent; } }, - /** - * Gets an event that's raised when a surface tile is loaded and ready to be rendered. - * - * @memberof Globe.prototype - * @type {Event} - * @readonly - */ - tileLoadedEvent : { - get : function() { - return this._surface.tileProvider.tileLoadedEvent; - } - }, /** * Returns true when the tile load queue is empty, false otherwise. When the load queue is empty, * all terrain and imagery for the current view have been loaded. diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 4292a6714a6..c2c6a3e32a6 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -171,7 +171,6 @@ define([ this._imageryLayers.layerRemoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerRemoved, this); this._imageryLayers.layerMoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerMoved, this); this._imageryLayers.layerShownOrHidden.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerShownOrHidden, this); - this._tileLoadedEvent = new Event(); this._imageryLayersUpdatedEvent = new Event(); this._layerOrderChanged = false; @@ -288,17 +287,6 @@ define([ } }, - /** - * Gets an event that is raised when an globe surface tile is loaded and ready to be rendered. - * @memberof GlobeSurfaceTileProvider.prototype - * @type {Event} - */ - tileLoadedEvent : { - get : function() { - return this._tileLoadedEvent; - } - }, - /** * Gets an event that is raised when an imagery layer is added, shown, hidden, moved, or removed. * @memberof GlobeSurfaceTileProvider.prototype @@ -554,13 +542,6 @@ define([ return; } GlobeSurfaceTile.processStateMachine(tile, frameState, this._terrainProvider, this._imageryLayers, this._vertexArraysToDestroy); - var tileLoadedEvent = this._tileLoadedEvent; - - // TODO: creating a new function for every loaded tile every frame?! - tile._loadedCallbacks['tileLoadedEvent'] = function (tile) { - tileLoadedEvent.raiseEvent(); - return true; - }; }; var boundingSphereScratch = new BoundingSphere(); diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 3544f42f222..1d2bb69018b 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -72,7 +72,7 @@ define([ this._customData = []; this._frameUpdated = undefined; - this._lastSelectionResult = TileSelectionResult.CULLED; + this._lastSelectionResult = TileSelectionResult.NONE; this._lastSelectionResultFrame = undefined; this._loadedCallbacks = {}; diff --git a/Specs/DataSources/EntityClusterSpec.js b/Specs/DataSources/EntityClusterSpec.js index 0770d508f5a..ef98d250f96 100644 --- a/Specs/DataSources/EntityClusterSpec.js +++ b/Specs/DataSources/EntityClusterSpec.js @@ -50,7 +50,6 @@ defineSuite([ tilesWaitingForChildren : 0 } }, - tileLoadedEvent : new Event(), terrainProviderChanged : new Event(), imageryLayersUpdatedEvent : new Event(), beginFrame : function() {}, diff --git a/Specs/DataSources/PointVisualizerSpec.js b/Specs/DataSources/PointVisualizerSpec.js index 07853ca88d2..de57e0ecbbf 100644 --- a/Specs/DataSources/PointVisualizerSpec.js +++ b/Specs/DataSources/PointVisualizerSpec.js @@ -51,7 +51,6 @@ defineSuite([ scene.globe = { ellipsoid : Ellipsoid.WGS84, _surface : {}, - tileLoadedEvent : new Event(), imageryLayersUpdatedEvent : new Event(), terrainProviderChanged : new Event() }; diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js index f441a1a05d0..4dee6633ab2 100644 --- a/Specs/Scene/ModelSpec.js +++ b/Specs/Scene/ModelSpec.js @@ -3147,7 +3147,6 @@ defineSuite([ tilesWaitingForChildren : 0 } }, - tileLoadedEvent : new Event(), imageryLayersUpdatedEvent : new Event(), destroy : function() {} }; diff --git a/Specs/createGlobe.js b/Specs/createGlobe.js index f55778f9d7c..b0d40c30e77 100644 --- a/Specs/createGlobe.js +++ b/Specs/createGlobe.js @@ -25,7 +25,6 @@ define([ return 0.0; }, _surface : {}, - tileLoadedEvent : new Event(), imageryLayersUpdatedEvent : new Event(), _terrainProvider : undefined, terrainProviderChanged : new Event(), From edc60593d5a211f4ae3eee55222651680d405c7b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 11 Dec 2018 15:57:26 +1100 Subject: [PATCH 065/131] Remove nonsensical tests, clean up some things. --- Source/Scene/GlobeSurfaceTile.js | 5 +- Specs/Core/TerrainEncodingSpec.js | 2 +- Specs/Scene/GlobeSurfaceTileSpec.js | 51 ++------------- .../upsampleQuantizedTerrainMeshSpec.js | 65 ------------------- 4 files changed, 7 insertions(+), 116 deletions(-) delete mode 100644 Specs/Workers/upsampleQuantizedTerrainMeshSpec.js diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 22e105b40d5..ce582f08ffb 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -98,7 +98,6 @@ define([ this.terrainState = TerrainState.UNLOADED; this.mesh = undefined; this.fill = undefined; - this.vertexArray = undefined; // TODO: probably better to have a bounding sphere for 2D rather than one for picking. this.pickBoundingSphere = new BoundingSphere(); @@ -435,8 +434,8 @@ define([ var surfaceTile = tile.data; var parent = tile.parent; - if (parent !== undefined && !parent.data.isChildAvailable(parent.x, parent.y, tile.x, tile.y)) { - // Start upsampling right away. + if (parent !== undefined && parent.data.isChildAvailable(parent.x, parent.y, tile.x, tile.y) === false) { + // This tile is not available, so mark it failed so we start upsampling right away. surfaceTile.terrainState = TerrainState.FAILED; } diff --git a/Specs/Core/TerrainEncodingSpec.js b/Specs/Core/TerrainEncodingSpec.js index 34dfc5a5e26..33af2e708fb 100644 --- a/Specs/Core/TerrainEncodingSpec.js +++ b/Specs/Core/TerrainEncodingSpec.js @@ -38,7 +38,7 @@ defineSuite([ it('default constructs', function() { var encoding = new TerrainEncoding(); - expect(encoding.quantization).not.toBeDefined(); + expect(encoding.quantization).toBe(TerrainQuantization.NONE); expect(encoding.minimumHeight).not.toBeDefined(); expect(encoding.maximumHeight).not.toBeDefined(); expect(encoding.center).not.toBeDefined(); diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 0774ebf6516..2e0d44c2b0f 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -128,13 +128,10 @@ defineSuite([ expect(rootTile.state).toBe(QuadtreeTileLoadState.LOADING); }); - it('non-root tiles get neither loadedTerrain nor upsampledTerrain when their parent is not loaded nor upsampled', function() { - var children = rootTile.children; - for (var i = 0; i < children.length; ++i) { - GlobeSurfaceTile.processStateMachine(children[i], scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - expect(children[i].data.loadedTerrain).toBeUndefined(); - expect(children[i].data.upsampledTerrain).toBeUndefined(); - } + it('transitions to FAILED state immediately if this tile is not available', function() { + rootTile.childTileMask = 0; // no child tiles available + GlobeSurfaceTile.processStateMachine(rootTile.southwestChild, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); + expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); }); it('once a root tile is loaded, its children get both loadedTerrain and upsampledTerrain', function() { @@ -234,46 +231,6 @@ defineSuite([ }); }); - xit('improved upsampled terrain triggers re-upsampling of children', function() { - var childTile = rootTile.children[0]; - var grandchildTile = childTile.children[0]; - var greatGrandchildTile = grandchildTile.children[0]; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }); - }).then(function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return grandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }); - }).then(function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(greatGrandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return greatGrandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }); - }).then(function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - return childTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }); - }).then(function() { - var greatGrandchildUpsampledTerrain = grandchildTile.data.upsampledTerrain; - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return grandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - expect(greatGrandchildTile.data.upsampledTerrain).toBeDefined(); - expect(greatGrandchildTile.data.upsampledTerrain).not.toBe(greatGrandchildUpsampledTerrain); - }); - }); - }); - it('releases previous upsampled water mask when a real one is loaded', function() { var childTile = rootTile.children[0]; diff --git a/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js b/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js deleted file mode 100644 index 0592bb407f6..00000000000 --- a/Specs/Workers/upsampleQuantizedTerrainMeshSpec.js +++ /dev/null @@ -1,65 +0,0 @@ -defineSuite([ - 'Workers/upsampleQuantizedTerrainMesh', - 'Core/createWorldTerrain' - ], function( - upsampleQuantizedTerrainMesh, - createWorldTerrain) { - 'use strict'; - - it('time', function() { - var worldTerrain = createWorldTerrain({ - requestWaterMask: true, - requestVertexNormals: true - }); - - var total = 100; - var remaining = total; - var tiles = []; - function next() { - var current = total - remaining; - --remaining; - return worldTerrain.requestTileGeometry(1375 + current, 1189, 12).then(function(terrainData) { - return terrainData.createMesh(worldTerrain.tilingScheme, 1375, 1189, 12, 1.0).then(function(mesh) { - tiles[current] = { - terrainData: terrainData, - mesh: mesh - }; - if (remaining === 0) { - return tiles; - } - return next(); - }); - }); - } - - var promise = worldTerrain.readyPromise.then(function() { - return next(); - }); - - return promise.then(function(tiles) { - var childRectangle = worldTerrain.tilingScheme.tileXYToRectangle(2750, 2378, 13); - console.log(tiles.length); - var start = performance.now(); - for (var i = 0; i < tiles.length; ++i) { - var tile = tiles[i % tiles.length]; - upsampleQuantizedTerrainMesh._workerFunction({ - isEastChild: false, - isNorthChild: true, - vertices: tile.mesh.vertices, - indices: tile.mesh.indices, - skirtIndex: tile.terrainData._skirtIndex, - encoding: tile.mesh.encoding, - exaggeration: tile.mesh.exaggeration, - vertexCountWithoutSkirts: tile.terrainData._vertexCountWithoutSkirts, - minimumHeight: tile.terrainData._minimumHeight, - maximumHeight: tile.terrainData._maximumHeight, - ellipsoid: worldTerrain.tilingScheme.ellipsoid, - childRectangle: childRectangle - }, []); - } - var stop = performance.now(); - console.log(stop - start); - //alert(stop - start); - }); - }); -}); From b54e9f2693c933533d4024e1513a928abdaa692c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 12 Dec 2018 22:58:15 +1100 Subject: [PATCH 066/131] Tests and cleanup. --- Source/Core/CesiumTerrainProvider.js | 7 +- Source/Core/HeightmapTerrainData.js | 9 +- Source/Scene/GlobeSurfaceTile.js | 90 +--- Source/Scene/GlobeSurfaceTileProvider.js | 18 +- Specs/Scene/GlobeSurfaceTileSpec.js | 628 ++++++++++++++--------- 5 files changed, 405 insertions(+), 347 deletions(-) diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index a1a254a8cce..71ee3b2175d 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -997,7 +997,10 @@ define([ * Gets an object that can be used to determine availability of terrain from this provider, such as * at points and in rectangles. This function should not be called before * {@link CesiumTerrainProvider#ready} returns true. This property may be undefined if availability - * information is not available. + * information is not available. Note that this reflects tiles that are known to be available currently. + * Additional tiles may be discovered to be available in the future, e.g. if availability information + * exists deeper in the tree rather than it all being discoverable at the root. However, a tile that + * is available now will not become unavailable in the future. * @memberof CesiumTerrainProvider.prototype * @type {TileAvailability} */ @@ -1029,7 +1032,7 @@ define([ * @param {Number} x The X coordinate of the tile for which to request geometry. * @param {Number} y The Y coordinate of the tile for which to request geometry. * @param {Number} level The level of the tile for which to request geometry. - * @returns {Boolean} Undefined if not supported, otherwise true or false. + * @returns {Boolean} Undefined if not supported or availability is unknown, otherwise true or false. */ CesiumTerrainProvider.prototype.getTileDataAvailable = function(x, y, level) { if (!defined(this._availability)) { diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index e73ab99c2f1..572ae3609af 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -343,6 +343,11 @@ define([ } //>>includeEnd('debug'); + var meshData = this._mesh; + if (!defined(meshData)) { + return undefined; + } + var width = this._width; var height = this._height; var structure = this._structure; @@ -350,10 +355,6 @@ define([ var stride = structure.stride; var heights = new this._bufferType(width * height * stride); - var meshData = this._mesh; - if (!defined(meshData)) { - return undefined; - } var buffer = meshData.vertices; var encoding = meshData.encoding; diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index ce582f08ffb..2e836743717 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -105,8 +105,6 @@ define([ this.surfaceShader = undefined; this.isClipped = true; - this.childTileMask = undefined; - this.clippedByBoundaries = false; } @@ -140,7 +138,7 @@ define([ } }); - GlobeSurfaceTile.prototype.getBvh = function(tile, terrainProvider) { + GlobeSurfaceTile.prototype.getBoundingVolumeHierarchy = function(tile) { if (this._bvh === undefined) { var terrainData = this.terrainData; if (terrainData !== undefined && terrainData.bvh !== undefined) { @@ -149,7 +147,7 @@ define([ var parent = tile.parent; if (parent !== undefined && parent.data !== undefined) { - var parentBvh = parent.data.getBvh(parent, terrainProvider); + var parentBvh = parent.data.getBoundingVolumeHierarchy(parent); if (parentBvh !== undefined && parentBvh.length > 2) { var subsetLength = (parentBvh.length - 2) / 4; var childIndex = (tile.y === parent.y * 2 ? 2 : 0) + (tile.x === parent.x * 2 ? 0 : 1); @@ -160,7 +158,7 @@ define([ } return this._bvh; -}; + }; function getPosition(encoding, mode, projection, vertices, index, result) { encoding.decodePosition(vertices, index, result); @@ -241,7 +239,7 @@ define([ this.vertexArray = this.vertexArray.destroy(); - if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { + if (defined(indexBuffer) && !indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { --indexBuffer.referenceCount; if (indexBuffer.referenceCount === 0) { indexBuffer.destroy(); @@ -254,7 +252,7 @@ define([ this.wireframeVertexArray = this.wireframeVertexArray.destroy(); - if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { + if (defined(indexBuffer) && !indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { --indexBuffer.referenceCount; if (indexBuffer.referenceCount === 0) { indexBuffer.destroy(); @@ -263,7 +261,7 @@ define([ } }; - GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, terrainOnly) { + GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, terrainOnly) { var surfaceTile = tile.data; if (!defined(surfaceTile)) { surfaceTile = tile.data = new GlobeSurfaceTile(); @@ -288,14 +286,14 @@ define([ if (ancestor.data === undefined || ancestor.data.terrainData === undefined) { // The ancestor that holds the BVH data isn't loaded yet; load it (terrain only!) instead of this tile. - GlobeSurfaceTile.processStateMachine(ancestor, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, true); + GlobeSurfaceTile.processStateMachine(ancestor, frameState, terrainProvider, imageryLayerCollection, true); return; } } } if (tile.state === QuadtreeTileLoadState.LOADING) { - processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); + processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection); } // From here down we're loading imagery, not terrain. We don't want to load imagery until @@ -311,7 +309,7 @@ define([ // The terrain is renderable as soon as we have a valid vertex array. var isRenderable = defined(surfaceTile.vertexArray); - // But it's not done loading until our two state machines are terminated. + // But it's not done loading until it's in the READY state. var isDoneLoading = surfaceTile.terrainState === TerrainState.READY; // If this tile's terrain and imagery are just upsampled from its parent, mark the tile as @@ -379,64 +377,10 @@ define([ } }; - /** - * Determines if a given child tile is available. The given child tile coordinates are assumed - * to be one of the four children of this tile. If non-child tile coordinates are - * given, the availability of the southeast child tile is returned. This function determines - * the presence of child tiles from `terrainProvider.availability` or from `this.terrainData.childTileMask`. - * - * @param {TerrainProvider} terrainProvider The terrain provider. - * @param {QuatreeTile} tile The parent tile. - * @param {Number} childX The tile X coordinate of the child tile to check for availability. - * @param {Number} childY The tile Y coordinate of the child tile to check for availability. - * @returns {Boolean|undefined} True if the child tile is available; otherwise, false. If tile availability - * cannot be determined, this function returns undefined. - */ - GlobeSurfaceTile.prototype.isChildAvailable = function(terrainProvider, tile, childX, childY) { - //>>includeStart('debug', pragmas.debug); - if (!defined(terrainProvider)) { - throw new DeveloperError('terrainProvider is required.'); - } - if (!defined(tile)) { - throw new DeveloperError('tile is required.'); - } - if (!defined(childX)) { - throw new DeveloperError('childX is required.'); - } - if (!defined(childY)) { - throw new DeveloperError('childY is required.'); - } - //>>includeEnd('debug'); - - if (this.childTileMask === undefined) { - if (terrainProvider.availability !== undefined) { - this.childTileMask = terrainProvider.availability.computeChildMaskForTile(tile.level, tile.x, tile.y); - } else if (this.terrainData !== undefined && this.terrainData.childTileMask !== undefined) { - this.childTileMask = this.terrainData.childTileMask; - } else { - // No idea if children exist or not. - return undefined; - } - } - - var bitNumber = 2; // northwest child - if (childX !== tile.x * 2) { - ++bitNumber; // east child - } - if (childY !== tile.y * 2) { - bitNumber -= 2; // south child - } - - return (this.childTileMask & (1 << bitNumber)) !== 0; - }; - function prepareNewTile(tile, terrainProvider, imageryLayerCollection) { - var surfaceTile = tile.data; - - var parent = tile.parent; - if (parent !== undefined && parent.data.isChildAvailable(parent.x, parent.y, tile.x, tile.y) === false) { + if (terrainProvider.getTileDataAvailable(tile.x, tile.y, tile.level) === false) { // This tile is not available, so mark it failed so we start upsampling right away. - surfaceTile.terrainState = TerrainState.FAILED; + tile.data.terrainState = TerrainState.FAILED; } // Map imagery tiles to this terrain tile @@ -448,17 +392,16 @@ define([ } } - function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { + function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection) { var surfaceTile = tile.data; // If this tile is FAILED, we'll need to upsample from the parent. If the parent isn't // ready for that, let's push it along. var parent = tile.parent; if (surfaceTile.terrainState === TerrainState.FAILED && parent !== undefined) { - var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined && parent.data.terrainData._mesh !== undefined; + var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined; if (!parentReady) { - //console.log('Waiting on L' + parent.level + 'X' + parent.x + 'Y' + parent.y); - GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, true); + GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, true); } } @@ -491,7 +434,8 @@ define([ function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level) { var parent = tile.parent; if (!parent) { - // Trying to upsample from a root tile. No can do. + // Trying to upsample from a root tile. No can do. This tile is a failure. + tile.state = QuadtreeTileLoadState.FAILED; return; } @@ -500,7 +444,7 @@ define([ var sourceY = parent.y; var sourceLevel = parent.level; - if (sourceData === undefined || sourceData._mesh === undefined) { + if (!defined(sourceData)) { // Parent is not available, so we can't upsample this tile yet. return; } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index c2c6a3e32a6..5af77ef96e1 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -180,8 +180,6 @@ define([ this._uniformMaps = []; this._usedDrawCommands = 0; - this._vertexArraysToDestroy = []; - this._debug = { wireframe : false, boundingSphereTile : undefined @@ -410,13 +408,6 @@ define([ // Add credits for terrain and imagery providers. updateCredits(this, frameState); - - var vertexArraysToDestroy = this._vertexArraysToDestroy; - var length = vertexArraysToDestroy.length; - for (var j = 0; j < length; ++j) { - freeVertexArray(vertexArraysToDestroy[j]); - } - vertexArraysToDestroy.length = 0; }; /** @@ -541,7 +532,7 @@ define([ if (stopLoad) { return; } - GlobeSurfaceTile.processStateMachine(tile, frameState, this._terrainProvider, this._imageryLayers, this._vertexArraysToDestroy); + GlobeSurfaceTile.processStateMachine(tile, frameState, this._terrainProvider, this._imageryLayers); }; var boundingSphereScratch = new BoundingSphere(); @@ -659,7 +650,10 @@ define([ // For a tileset with `availability`, we'll always be able to refine. // We can ask for availability of _any_ child tile because we only need to confirm // that we get a yes or no answer, it doesn't matter what the answer is. - var childAvailable = tile.data.isChildAvailable(this.terrainProvider, tile, 0, 0); + if (defined(tile.data.terrainData)) { + return true; + } + var childAvailable = this.terrainProvider.getTileDataAvailable(tile.x * 2, tile.y * 2, tile.level + 1); return childAvailable !== undefined; }; @@ -861,7 +855,7 @@ define([ return tile; } - var bvh = surfaceTile.getBvh(tile, terrainProvider.terrainProvider); + var bvh = surfaceTile.getBoundingVolumeHierarchy(tile); if (bvh !== undefined && bvh[0] === bvh[0] && bvh[1] === bvh[1]) { // Have a BVH that covers this tile and the heights are not NaN. tileBoundingRegion.minimumHeight = bvh[0] * frameState.terrainExaggeration; diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 2e0d44c2b0f..46e742a2812 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -7,9 +7,11 @@ defineSuite([ 'Core/defined', 'Core/Ellipsoid', 'Core/GeographicTilingScheme', + 'Core/HeightmapTerrainData', 'Core/Ray', 'Core/Rectangle', 'Core/RequestScheduler', + 'Core/TileAvailability', 'Scene/Imagery', 'Scene/ImageryLayer', 'Scene/ImageryLayerCollection', @@ -30,9 +32,11 @@ defineSuite([ defined, Ellipsoid, GeographicTilingScheme, + HeightmapTerrainData, Ray, Rectangle, RequestScheduler, + TileAvailability, Imagery, ImageryLayer, ImageryLayerCollection, @@ -47,67 +51,36 @@ defineSuite([ 'use strict'; describe('processStateMachine', function() { - var scene; - var alwaysDeferTerrainProvider; - var alwaysFailTerrainProvider; - var realTerrainProvider; + var frameState = {}; + var terrainProvider; var tilingScheme; var rootTiles; var rootTile; var imageryLayerCollection; - beforeAll(function() { - scene = createScene(); + beforeEach(function() { + tilingScheme = new GeographicTilingScheme(); + rootTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + rootTile = rootTiles[0]; + imageryLayerCollection = new ImageryLayerCollection(); - alwaysDeferTerrainProvider = { + terrainProvider = { requestTileGeometry : function(x, y, level) { return undefined; }, tilingScheme : tilingScheme, + availability : new TileAvailability(tilingScheme, 10), hasWaterMask : function() { return true; }, getTileDataAvailable : function(x, y, level) { return undefined; - } - }; - - alwaysFailTerrainProvider = { - requestTileGeometry : function(x, y, level) { - var deferred = when.defer(); - deferred.reject(); - return deferred.promise; }, - tilingScheme : tilingScheme, - hasWaterMask : function() { - return true; - }, - getTileDataAvailable : function(x, y, level) { - return undefined; + getNearestBvhLevel : function(x, y, level) { + return -1; } }; - - realTerrainProvider = new CesiumTerrainProvider({ - url : 'https://s3.amazonaws.com/cesiumjs/smallTerrain' - }); - }); - - afterAll(function() { - scene.destroyForSpecs(); - }); - - beforeEach(function() { - tilingScheme = new GeographicTilingScheme(); - alwaysDeferTerrainProvider.tilingScheme = tilingScheme; - alwaysFailTerrainProvider.tilingScheme = tilingScheme; - rootTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); - rootTile = rootTiles[0]; - imageryLayerCollection = new ImageryLayerCollection(); - - return pollToPromise(function() { - return realTerrainProvider.ready; - }); }); afterEach(function() { @@ -123,242 +96,167 @@ defineSuite([ } }); - it('transitions to the LOADING state immediately', function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); + it('transitions to the LOADING state immediately if this tile is available', function() { + GlobeSurfaceTile.processStateMachine(rootTile, frameState, terrainProvider, imageryLayerCollection); expect(rootTile.state).toBe(QuadtreeTileLoadState.LOADING); + expect(rootTile.data.terrainState).toBe(TerrainState.UNLOADED); }); - it('transitions to FAILED state immediately if this tile is not available', function() { - rootTile.childTileMask = 0; // no child tiles available - GlobeSurfaceTile.processStateMachine(rootTile.southwestChild, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); + it('transitions to the LOADING tile state and FAILED terrain state immediately if this tile is NOT available', function() { + makeTerrainReady(rootTile); + spyOn(terrainProvider, 'getTileDataAvailable').and.returnValue(false); + GlobeSurfaceTile.processStateMachine(rootTile.southwestChild, frameState, terrainProvider, imageryLayerCollection); + expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING); + expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.FAILED); }); - it('once a root tile is loaded, its children get both loadedTerrain and upsampledTerrain', function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return rootTile.state === QuadtreeTileLoadState.DONE; - }).then(function() { - var children = rootTile.children; - for (var i = 0; i < children.length; ++i) { - GlobeSurfaceTile.processStateMachine(children[i], scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - expect(children[i].data.loadedTerrain).toBeDefined(); - expect(children[i].data.upsampledTerrain).toBeDefined(); - } - }); + it('pushes parent along if waiting on it to be able to upsample', function() { + makeTerrainUnloaded(rootTile); + spyOn(terrainProvider, 'getTileDataAvailable').and.returnValue(false); + spyOn(terrainProvider, 'requestTileGeometry'); + GlobeSurfaceTile.processStateMachine(rootTile.southwestChild, frameState, terrainProvider, imageryLayerCollection); + expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); }); - it('loaded terrainData is copied to the tile once it is available', function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - expect(rootTile.data.terrainData).toBeDefined(); - }); + it('does nothing when attempting to upsample a failed root tile', function() { + makeTerrainFailed(rootTile); + GlobeSurfaceTile.processStateMachine(rootTile, frameState, terrainProvider, imageryLayerCollection); + expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); + expect(rootTile.data.terrainState).toBe(TerrainState.FAILED); }); - xit('upsampled terrainData is copied to the tile once it is available', function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - return pollToPromise(function() { - var childTile = rootTile.children[0]; - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - expect(rootTile.children[0].data.terrainData).toBeDefined(); - }); - }); + it('upsamples failed tiles from parent TerrainData', function() { + makeTerrainReceived(rootTile); + spyOn(terrainProvider, 'getTileDataAvailable').and.returnValue(false); + var sw = rootTile.southwestChild; + GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + expect(rootTile.data.terrainData.upsample).toHaveBeenCalledWith(tilingScheme, rootTile.x, rootTile.y, rootTile.level, sw.x, sw.y, sw.level); }); - xit('loaded terrain data replaces upsampled terrain data', function() { - var childTile = rootTile.children[0]; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - var upsampledTerrainData; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - upsampledTerrainData = childTile.data.terrainData; - expect(upsampledTerrainData).toBeDefined(); - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - return childTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - expect(childTile.data.terrainData).not.toBe(upsampledTerrainData); - }); - }); - }); + it('loads available tiles', function() { + var sw = rootTile.southwestChild; + terrainProvider.availability.addAvailableTileRange(sw.level, sw.x, sw.y, sw.x, sw.y); + spyOn(terrainProvider, 'requestTileGeometry'); + GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); }); - xit('loaded terrain replacing upsampled terrain triggers re-upsampling and re-loading of children', function() { - var childTile = rootTile.children[0]; - var grandchildTile = childTile.children[0]; + it('loads BVH nodes instead when the tile\'s bounding volume is unreliable', function() { + var sw = rootTile.southwestChild; + makeTerrainUnloaded(rootTile); + makeTerrainUnloaded(sw); - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }); - }).then(function() { - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - return grandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED; - }); - }).then(function() { - var grandchildUpsampledTerrain = grandchildTile.data.upsampledTerrain; - expect(grandchildTile.data.loadedTerrain).toBeUndefined(); - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - return childTile.data.loadedTerrain.state >= TerrainState.RECEIVED; - }).then(function() { - expect(grandchildTile.data.upsampledTerrain).not.toBe(grandchildUpsampledTerrain); - expect(grandchildTile.data.loadedTerrain).toBeDefined(); - }); - }); - }); + // Indicate that the SW tile's bounding volume comes from the root. + sw.data.boundingVolumeSourceTile = rootTile; - it('releases previous upsampled water mask when a real one is loaded', function() { - var childTile = rootTile.children[0]; + // Indicate that level 0 has BVH data for the SW tile. + spyOn(terrainProvider, 'getNearestBvhLevel').and.returnValue(0); + spyOn(terrainProvider, 'requestTileGeometry'); - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return rootTile.renderable && childTile.renderable; - }).then(function() { - expect(childTile.data.waterMaskTexture).toBeDefined(); - var childWaterMaskTexture = childTile.data.waterMaskTexture; - var referenceCount = childWaterMaskTexture.referenceCount; - var vertexArraysToDestroy = []; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToDestroy); - RequestScheduler.update(); - return childTile.state === QuadtreeTileLoadState.DONE; - }).then(function() { - expect(childTile.data.waterMaskTexture).toBeDefined(); - expect(childTile.data.waterMaskTexture).not.toBe(childWaterMaskTexture); - expect(childWaterMaskTexture.referenceCount + 1).toBe(referenceCount); - expect(vertexArraysToDestroy.length).toEqual(1); - }); - }); + GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + + // Expect that the rootTile is loaded, not the SW tile. + expect(terrainProvider.getNearestBvhLevel).toHaveBeenCalledWith(sw.x, sw.y, sw.level); + expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); }); - it('upsampled terrain is used when real terrain fails to load', function() { - var childTile = rootTile.children[0]; + it('loads this tile if the nearest BVH level is unknown', function() { + var sw = rootTile.southwestChild; + makeTerrainUnloaded(rootTile); + makeTerrainUnloaded(sw); - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysFailTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return rootTile.renderable && childTile.renderable; - }).then(function() { - expect(childTile.data.loadedTerrain).toBeUndefined(); - expect(childTile.upsampledFromParent).toBe(true); - }); - }); + // Indicate that the SW tile's bounding volume comes from the root. + sw.data.boundingVolumeSourceTile = rootTile; - it('child of loaded tile is not re-upsampled or re-loaded if it is already loaded', function() { - var childTile = rootTile.children[0]; - var grandchildTile = childTile.children[0]; + // Indicate that no BVH data is available for the SW tile + spyOn(terrainProvider, 'getNearestBvhLevel').and.returnValue(-1); + spyOn(terrainProvider, 'requestTileGeometry'); - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return defined(rootTile.data.terrainData) && defined(rootTile.data.terrainData._mesh) && - defined(childTile.data.terrainData); - }).then(function() { - // Mark the grandchild as present even though the child is upsampled. - childTile.data.terrainData._childTileMask = 15; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return grandchildTile.state === QuadtreeTileLoadState.DONE; - }).then(function() { - expect(grandchildTile.data.loadedTerrain).toBeUndefined(); - expect(grandchildTile.data.upsampledTerrain).toBeUndefined(); - - var vertexArraysToDestroy = []; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToDestroy); - RequestScheduler.update(); - return childTile.state === QuadtreeTileLoadState.DONE; - }).then(function() { - expect(grandchildTile.state).toBe(QuadtreeTileLoadState.DONE); - expect(grandchildTile.data.loadedTerrain).toBeUndefined(); - expect(grandchildTile.data.upsampledTerrain).toBeUndefined(); - expect(vertexArraysToDestroy.length).toEqual(1); - }); - }); - }); + GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + + // Expect that the SW tile is loaded. + expect(terrainProvider.getNearestBvhLevel).toHaveBeenCalledWith(sw.x, sw.y, sw.level); + expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); }); - it('child of upsampled tile is not re-upsampled if it is already loaded', function() { - var childTile = rootTile.children[0]; - var grandchildTile = childTile.children[0]; - var greatGrandchildTile = grandchildTile.children[0]; + it('loads this tile if the terrain provider does not implement getNearestBvhLevel', function() { + var sw = rootTile.southwestChild; + makeTerrainUnloaded(rootTile); + makeTerrainUnloaded(sw); - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return defined(rootTile.data.terrainData) && defined(rootTile.data.terrainData._mesh) && - defined(childTile.data.terrainData) && defined(childTile.data.terrainData._mesh) && - defined(grandchildTile.data.terrainData); - }).then(function() { - // Mark the great-grandchild as present even though the grandchild is upsampled. - grandchildTile.data.terrainData._childTileMask = 15; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(greatGrandchildTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - RequestScheduler.update(); - return greatGrandchildTile.state === QuadtreeTileLoadState.DONE; - }).then(function() { - expect(greatGrandchildTile.data.loadedTerrain).toBeUndefined(); - expect(greatGrandchildTile.data.upsampledTerrain).toBeUndefined(); - - var vertexArraysToBeDestroyed = []; - - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToBeDestroyed); - GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, vertexArraysToBeDestroyed); - RequestScheduler.update(); - return childTile.state === QuadtreeTileLoadState.DONE && - !defined(grandchildTile.data.upsampledTerrain); - }).then(function() { - expect(greatGrandchildTile.state).toBe(QuadtreeTileLoadState.DONE); - expect(greatGrandchildTile.data.loadedTerrain).toBeUndefined(); - expect(greatGrandchildTile.data.upsampledTerrain).toBeUndefined(); - expect(vertexArraysToBeDestroyed.length).toEqual(2); - }); - }); + // Indicate that the SW tile's bounding volume comes from the root. + sw.data.boundingVolumeSourceTile = rootTile; + + // Indicate that no BVH data is available for the SW tile + terrainProvider.getNearestBvhLevel = undefined; + spyOn(terrainProvider, 'requestTileGeometry'); + + GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + + // Expect that the SW tile is loaded. + expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); + }); + + it('loads only terrain (not imagery) when loading BVH nodes', function() { + var sw = rootTile.southwestChild; + makeTerrainUnloaded(rootTile); + makeTerrainUnloaded(sw); + + // Indicate that the SW tile's bounding volume comes from the root. + sw.data.boundingVolumeSourceTile = rootTile; + + // Indicate that level 0 has BVH data for the SW tile. + spyOn(terrainProvider, 'getNearestBvhLevel').and.returnValue(0); + spyOn(terrainProvider, 'requestTileGeometry'); + + var _createTileImagerySkeletons = jasmine.createSpy('_createTileImagerySkeletons'); + sw.data.imagery.push({ + loadingImagery : { + state : ImageryState.PLACEHOLDER, + imageryLayer : { + imageryProvider : { + ready : true + }, + _createTileImagerySkeletons : _createTileImagerySkeletons + } + }, + freeResources: function() {} }); + + GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + + // Expect that the rootTile is loaded, not the SW tile. + expect(terrainProvider.getNearestBvhLevel).toHaveBeenCalledWith(sw.x, sw.y, sw.level); + expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); + expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); + + // Expect that _createTileImagerySkeletons was _not_ called + expect(_createTileImagerySkeletons).not.toHaveBeenCalled(); }); - it('entirely upsampled tile is marked as such', function() { + /*it('entirely upsampled tile is marked as such', function() { var childTile = rootTile.children[0]; return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysFailTerrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(rootTile, frameState, realTerrainProvider, imageryLayerCollection); + GlobeSurfaceTile.processStateMachine(childTile, frameState, alwaysFailTerrainProvider, imageryLayerCollection); RequestScheduler.update(); return rootTile.state >= QuadtreeTileLoadState.DONE && childTile.state >= QuadtreeTileLoadState.DONE; @@ -394,10 +292,10 @@ defineSuite([ return pollToPromise(function() { if (rootTile.state !== QuadtreeTileLoadState.DONE) { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, allWaterTerrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(rootTile, frameState, allWaterTerrainProvider, imageryLayerCollection); return false; } - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, allWaterTerrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(childTile, frameState, allWaterTerrainProvider, imageryLayerCollection); return childTile.state === QuadtreeTileLoadState.DONE; }).then(function() { expect(childTile.data.waterMaskTexture).toBeDefined(); @@ -431,10 +329,10 @@ defineSuite([ return pollToPromise(function() { if (rootTile.state !== QuadtreeTileLoadState.DONE) { - GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, allLandTerrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(rootTile, frameState, allLandTerrainProvider, imageryLayerCollection); return false; } - GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, allLandTerrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(childTile, frameState, allLandTerrainProvider, imageryLayerCollection); return childTile.state === QuadtreeTileLoadState.DONE; }).then(function() { expect(childTile.data.waterMaskTexture).toBeUndefined(); @@ -451,7 +349,7 @@ defineSuite([ var imageryLayerCollection = new ImageryLayerCollection(); - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(tile, frameState, alwaysDeferTerrainProvider, imageryLayerCollection); var layer = new ImageryLayer({ requestImage : function() { @@ -464,11 +362,84 @@ defineSuite([ expect(imagery.parent.state).toBe(ImageryState.UNLOADED); return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(tile, frameState, alwaysDeferTerrainProvider, imageryLayerCollection); return imagery.parent.state !== ImageryState.UNLOADED; }); + });*/ + }); + + describe('getBoundingVolumeHierarchy', function() { + it('gets the BVH from the TerrainData if available', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + makeTerrainReceived(tile); + + var bvh = [1.0, 2.0]; + tile.data.terrainData.bvh = bvh; + expect(tile.data.getBoundingVolumeHierarchy(tile)).toBe(bvh); }); - }, 'WebGL'); + + it('gets the BVH from the parent tile if available', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + makeTerrainReceived(tile); + tile.data.terrainData.bvh = new Float32Array([1.0, 10.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]); + + var sw = tile.southwestChild; + makeLoading(sw); + expect(sw.data.getBoundingVolumeHierarchy(sw)).toEqual([3.0, 4.0]); + + var se = tile.southeastChild; + makeLoading(se); + expect(se.data.getBoundingVolumeHierarchy(se)).toEqual([5.0, 6.0]); + + var nw = tile.northwestChild; + makeLoading(nw); + expect(nw.data.getBoundingVolumeHierarchy(nw)).toEqual([7.0, 8.0]); + + var ne = tile.northeastChild; + makeLoading(ne); + expect(ne.data.getBoundingVolumeHierarchy(ne)).toEqual([9.0, 10.0]); + }); + + it('returns undefined if the parent BVH does not extend to this tile', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + makeTerrainReceived(tile); + tile.data.terrainData.bvh = new Float32Array([1.0, 10.0]); + + var sw = tile.southwestChild; + makeLoading(sw); + expect(sw.data.getBoundingVolumeHierarchy(sw)).toBeUndefined(); + }); + + it('returns undefined if the parent does not have a BVH', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + makeTerrainReceived(tile); + tile.data.terrainData.bvh = undefined; + + var sw = tile.southwestChild; + makeLoading(sw); + expect(sw.data.getBoundingVolumeHierarchy(sw)).toBeUndefined(); + }); + }); describe('pick', function() { var scene; @@ -496,12 +467,15 @@ defineSuite([ var imageryLayerCollection = new ImageryLayerCollection(); + makeLoading(tile); + tile.data.boundingVolumeSourceTile = tile; + return pollToPromise(function() { if (!terrainProvider.ready) { return false; } - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, imageryLayerCollection, []); + GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, imageryLayerCollection); RequestScheduler.update(); return tile.state === QuadtreeTileLoadState.DONE; }).then(function() { @@ -514,4 +488,146 @@ defineSuite([ }); }); }, 'WebGL'); + + describe('eligibleForUnloading', function() { + it('returns true when no loading has been done', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + makeLoading(tile); + expect(tile.data.eligibleForUnloading).toBe(true); + }); + + it('returns true when some loading has been done', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + + makeTerrainReceived(tile); + expect(tile.data.eligibleForUnloading).toBe(true); + + makeTerrainTransformed(tile); + expect(tile.data.eligibleForUnloading).toBe(true); + + makeTerrainReady(tile); + expect(tile.data.eligibleForUnloading).toBe(true); + }); + + it('returns false when RECEIVING or TRANSITIONING', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + + makeTerrainUnloaded(tile); + expect(tile.data.eligibleForUnloading).toBe(true); + + tile.data.terrainState = TerrainState.RECEIVING; + expect(tile.data.eligibleForUnloading).toBe(false); + + tile.data.terrainState = TerrainState.TRANSFORMING; + expect(tile.data.eligibleForUnloading).toBe(false); + }); + + it('returns false when imagery is TRANSITIONING, true otherwise', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + makeTerrainReady(tile); + expect(tile.data.eligibleForUnloading).toBe(true); + + tile.data.imagery.push({ + loadingImagery : { + state : undefined, + }, + }); + + Object.keys(ImageryState).forEach(function(state) { + tile.data.imagery[0].loadingImagery.state = state; + if (state === ImageryState.TRANSITIONING) { + expect(tile.data.eligibleForUnloading).toBe(false); + } else { + expect(tile.data.eligibleForUnloading).toBe(true); + } + }); + }); + }); + + function makeLoading(tile) { + if (!defined(tile.data)) { + tile.data = new GlobeSurfaceTile(); + } + tile.state = QuadtreeTileLoadState.LOADING; + } + + function makeTerrainFailed(tile) { + makeLoading(tile); + tile.data.terrainState = TerrainState.FAILED; + tile.data.terrainData = undefined; + tile.data.vertexArray = undefined; + tile.data.mesh = undefined; + } + + function makeTerrainUnloaded(tile) { + makeLoading(tile); + tile.data.terrainState = TerrainState.UNLOADED; + tile.data.terrainData = undefined; + tile.data.vertexArray = undefined; + tile.data.mesh = undefined; + } + + function makeTerrainReceived(tile) { + makeLoading(tile); + tile.data.terrainState = TerrainState.RECEIVED; + tile.data.terrainData = jasmine.createSpyObj('TerrainData', ['createMesh', 'upsample', 'wasCreatedByUpsampling']); + tile.data.terrainData.wasCreatedByUpsampling.and.returnValue(false); + + tile.data.mesh = undefined; + tile.data.vertexArray = undefined; + } + + function makeTerrainUpsampled(tile) { + makeLoading(tile); + tile.data.terrainState = TerrainState.RECEIVED; + tile.data.terrainData = jasmine.createSpyObj('TerrainData', ['createMesh', 'upsample', 'wasCreatedByUpsampling']); + tile.data.terrainData.wasCreatedByUpsampling.and.returnValue(true); + + tile.data.mesh = undefined; + tile.data.vertexArray = undefined; + } + + function makeTerrainTransformed(tile) { + makeLoading(tile); + + if (!defined(tile.data.terrainData)) { + makeTerrainReceived(tile); + } + + tile.data.terrainState = TerrainState.TRANSFORMED; + tile.data.mesh = {}; + } + + function makeTerrainReady(tile) { + makeLoading(tile); + + if (!defined(tile.data.mesh)) { + makeTerrainTransformed(tile); + } + + tile.data.terrainState = TerrainState.DONE; + tile.data.vertexArray = jasmine.createSpyObj('VertexArray', ['destroy', 'isDestroyed']); + tile.data.vertexArray.isDestroyed.and.returnValue(false); + tile.data.fill = tile.data.fill && tile.data.fill.destroy(); + } }); From d0e63aa62cc619913e6abe141e4a2b3a58724f5e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 15 Dec 2018 20:58:31 +1100 Subject: [PATCH 067/131] Much better GlobeSurfaceTileSpecs, minor refactoring for testing. --- Source/Scene/GlobeSurfaceTile.js | 57 +-- Source/Scene/ImageryLayer.js | 72 ++-- Specs/MockImageryProvider.js | 65 +++ Specs/MockTerrainProvider.js | 158 +++++++ Specs/Scene/GlobeSurfaceTileSpec.js | 619 +++++++++++++++++++--------- Specs/createTileKey.js | 15 + Specs/runLater.js | 24 ++ 7 files changed, 734 insertions(+), 276 deletions(-) create mode 100644 Specs/MockImageryProvider.js create mode 100644 Specs/MockTerrainProvider.js create mode 100644 Specs/createTileKey.js create mode 100644 Specs/runLater.js diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 2e836743717..c5023667234 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -272,7 +272,7 @@ define([ tile.state = QuadtreeTileLoadState.LOADING; } - if (surfaceTile.boundingVolumeSourceTile !== tile && terrainProvider.getNearestBvhLevel !== undefined) { + if (defined(surfaceTile.boundingVolumeSourceTile) && surfaceTile.boundingVolumeSourceTile !== tile && terrainProvider.getNearestBvhLevel !== undefined) { // So here we are loading this tile, but we know our bounding volume isn't very good, and so our // judgement that it's visible is kind of suspect. If this terrain source has bounding volume data // outside of individual tiles, let's get our hands on that before we waste time downloading @@ -302,7 +302,7 @@ define([ // only, which happens in these two scenarios: // * we want ancestor BVH data from this tile but don't plan to render it (see code above). // * we want to upsample from this tile but don't plan to render it (see processTerrainStateMachine). - if (terrainOnly || (tile.level !== 0 && tile.data.boundingVolumeSourceTile !== tile)) { + if (terrainOnly || (tile.level !== 0 && defined(surfaceTile.boundingVolumeSourceTile) && surfaceTile.boundingVolumeSourceTile !== tile)) { return; } @@ -352,28 +352,22 @@ define([ (tileImagery.loadingImagery.state === ImageryState.FAILED || tileImagery.loadingImagery.state === ImageryState.INVALID); } + tile.renderable = isRenderable; tile.upsampledFromParent = isUpsampledOnly; - // The tile becomes renderable when the terrain and all imagery data are loaded. - if (i === len) { - if (isRenderable) { - tile.renderable = true; - } - - if (isDoneLoading) { - var callbacks = tile._loadedCallbacks; - var newCallbacks = {}; - for(var layerId in callbacks) { - if (callbacks.hasOwnProperty(layerId)) { - if(!callbacks[layerId](tile)) { - newCallbacks[layerId] = callbacks[layerId]; - } + if (isDoneLoading) { + var callbacks = tile._loadedCallbacks; + var newCallbacks = {}; + for(var layerId in callbacks) { + if (callbacks.hasOwnProperty(layerId)) { + if(!callbacks[layerId](tile)) { + newCallbacks[layerId] = callbacks[layerId]; } } - tile._loadedCallbacks = newCallbacks; - - tile.state = QuadtreeTileLoadState.DONE; } + tile._loadedCallbacks = newCallbacks; + + tile.state = QuadtreeTileLoadState.DONE; } }; @@ -542,9 +536,7 @@ define([ }); } - function createResources(surfaceTile, context, terrainProvider, x, y, level) { - var mesh = surfaceTile.mesh; - + GlobeSurfaceTile._createVertexArrayForMesh = function(context, mesh) { var typedArray = mesh.vertices; var buffer = Buffer.createVertexBuffer({ context : context, @@ -567,19 +559,21 @@ define([ indexBuffer.vertexArrayDestroyable = false; indexBuffer.referenceCount = 1; indexBuffers[context.id] = indexBuffer; - surfaceTile.mesh.indices.indexBuffers = indexBuffers; + mesh.indices.indexBuffers = indexBuffers; } else { ++indexBuffer.referenceCount; } - surfaceTile.vertexArray = new VertexArray({ + return new VertexArray({ context : context, attributes : attributes, indexBuffer : indexBuffer }); + }; + function createResources(surfaceTile, context, terrainProvider, x, y, level) { + surfaceTile.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, surfaceTile.mesh); surfaceTile.terrainState = TerrainState.READY; - surfaceTile.fill = surfaceTile.fill && surfaceTile.fill.destroy(); } @@ -621,20 +615,7 @@ define([ } function createWaterMaskTextureIfNeeded(context, surfaceTile) { - var previousTexture = surfaceTile.waterMaskTexture; - if (defined(previousTexture)) { - --previousTexture.referenceCount; - if (previousTexture.referenceCount === 0) { - previousTexture.destroy(); - } - surfaceTile.waterMaskTexture = undefined; - } - var waterMask = surfaceTile.terrainData.waterMask; - if (!defined(waterMask)) { - return; - } - var waterMaskData = getContextWaterMaskData(context); var texture; diff --git a/Source/Scene/ImageryLayer.js b/Source/Scene/ImageryLayer.js index ac08fd37952..4618414bd55 100644 --- a/Source/Scene/ImageryLayer.js +++ b/Source/Scene/ImageryLayer.js @@ -789,6 +789,34 @@ define([ doRequest(); }; + ImageryLayer.prototype._createTextureWebGL = function(context, imagery) { + var sampler = new Sampler({ + minificationFilter : this.minificationFilter, + magnificationFilter : this.magnificationFilter + }); + + var image = imagery.image; + + if (defined(image.internalFormat)) { + return new Texture({ + context : context, + pixelFormat : image.internalFormat, + width : image.width, + height : image.height, + source : { + arrayBufferView : image.bufferView + }, + sampler : sampler + }); + } + return new Texture({ + context : context, + source : image, + pixelFormat : this._imageryProvider.hasAlphaChannel ? PixelFormat.RGBA : PixelFormat.RGB, + sampler : sampler + }); + }; + /** * Create a WebGL texture for a given {@link Imagery} instance. * @@ -828,32 +856,8 @@ define([ } //>>includeEnd('debug'); - var sampler = new Sampler({ - minificationFilter : this.minificationFilter, - magnificationFilter : this.magnificationFilter - }); - // Imagery does not need to be discarded, so upload it to WebGL. - var texture; - if (defined(image.internalFormat)) { - texture = new Texture({ - context : context, - pixelFormat : image.internalFormat, - width : image.width, - height : image.height, - source : { - arrayBufferView : image.bufferView - }, - sampler : sampler - }); - } else { - texture = new Texture({ - context : context, - source : image, - pixelFormat : imageryProvider.hasAlphaChannel ? PixelFormat.RGBA : PixelFormat.RGB, - sampler : sampler - }); - } + var texture = this._createTextureWebGL(context, imagery); if (imageryProvider.tilingScheme.projection instanceof WebMercatorProjection) { imagery.textureWebMercator = texture; @@ -868,16 +872,16 @@ define([ return minificationFilter + ':' + magnificationFilter + ':' + maximumAnisotropy; } - function finalizeReprojectTexture(imageryLayer, context, imagery, texture) { - var minificationFilter = imageryLayer.minificationFilter; - var magnificationFilter = imageryLayer.magnificationFilter; + ImageryLayer.prototype._finalizeReprojectTexture = function(context, texture) { + var minificationFilter = this.minificationFilter; + var magnificationFilter = this.magnificationFilter; var usesLinearTextureFilter = minificationFilter === TextureMinificationFilter.LINEAR && magnificationFilter === TextureMagnificationFilter.LINEAR; // Use mipmaps if this texture has power-of-two dimensions. // In addition, mipmaps are only generated if the texture filters are both LINEAR. if (usesLinearTextureFilter && !PixelFormat.isCompressedFormat(texture.pixelFormat) && CesiumMath.isPowerOfTwo(texture.width) && CesiumMath.isPowerOfTwo(texture.height)) { minificationFilter = TextureMinificationFilter.LINEAR_MIPMAP_LINEAR; var maximumSupportedAnisotropy = ContextLimits.maximumTextureFilterAnisotropy; - var maximumAnisotropy = Math.min(maximumSupportedAnisotropy, defaultValue(imageryLayer._maximumAnisotropy, maximumSupportedAnisotropy)); + var maximumAnisotropy = Math.min(maximumSupportedAnisotropy, defaultValue(this._maximumAnisotropy, maximumSupportedAnisotropy)); var mipmapSamplerKey = getSamplerKey(minificationFilter, magnificationFilter, maximumAnisotropy); var mipmapSamplers = context.cache.imageryLayerMipmapSamplers; if (!defined(mipmapSamplers)) { @@ -914,9 +918,7 @@ define([ } texture.sampler = nonMipmapSampler; } - - imagery.state = ImageryState.READY; - } + }; /** * Enqueues a command re-projecting a texture to a {@link GeographicProjection} on the next update, if necessary, and generate @@ -954,7 +956,8 @@ define([ }, postExecute : function(outputTexture) { imagery.texture = outputTexture; - finalizeReprojectTexture(that, context, imagery, outputTexture); + that._finalizeReprojectTexture(context, outputTexture); + imagery.state = ImageryState.READY; imagery.releaseReference(); } }); @@ -963,7 +966,8 @@ define([ if (needGeographicProjection) { imagery.texture = texture; } - finalizeReprojectTexture(this, context, imagery, texture); + this._finalizeReprojectTexture(context, texture); + imagery.state = ImageryState.READY; } }; diff --git a/Specs/MockImageryProvider.js b/Specs/MockImageryProvider.js new file mode 100644 index 00000000000..45e1bc6b837 --- /dev/null +++ b/Specs/MockImageryProvider.js @@ -0,0 +1,65 @@ +define([ + 'Core/defaultValue', + 'Core/GeographicTilingScheme', + 'Core/Resource', + 'Core/RuntimeError', + 'ThirdParty/when', + './createTileKey', + './runLater' +], function( + defaultValue, + GeographicTilingScheme, + Resource, + RuntimeError, + when, + createTileKey, + runLater) { + 'use strict'; + + function MockImageryProvider() { + this.tilingScheme = new GeographicTilingScheme(); + this.ready = false; + this.rectangle = this.tilingScheme.rectangle; + this.tileWidth = 256; + this.tileHeight = 256; + this._requestImageWillSucceed = {}; + + var that = this; + Resource.fetchImage('./Data/Images/Green.png').then(function(image) { + that.ready = true; + that._image = image; + }); + } + + MockImageryProvider.prototype.requestImage = function(x, y, level, request) { + var willSucceed = this._requestImageWillSucceed[createTileKey(x, y, level)]; + if (willSucceed === undefined) { + return undefined; // defer by default + } + + var that = this; + return runLater(function() { + if (willSucceed) { + return that._image; + } + throw new RuntimeError('requestImage failed as request.'); + }); + }; + + MockImageryProvider.prototype.requestImageWillSucceed = function(xOrTile, y, level) { + this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = true; + return this; + }; + + MockImageryProvider.prototype.requestImageWillFail = function(xOrTile, y, level) { + this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = false; + return this; + }; + + MockImageryProvider.prototype.requestImageWillDefer = function(xOrTile, y, level) { + this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = undefined; + return this; + }; + + return MockImageryProvider; +}); diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js new file mode 100644 index 00000000000..895a5cdfaa1 --- /dev/null +++ b/Specs/MockTerrainProvider.js @@ -0,0 +1,158 @@ +define([ + 'Core/defaultValue', + 'Core/GeographicTilingScheme', + 'Core/RuntimeError', + 'Core/TerrainProvider', + './createTileKey', + './runLater' +], function( + defaultValue, + GeographicTilingScheme, + RuntimeError, + TerrainProvider, + createTileKey, + runLater) { + 'use strict'; + + function MockTerrainProvider() { + this.tilingScheme = new GeographicTilingScheme(); + this.heightmapWidth = 65; + this.levelZeroMaximumGeometricError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(this.tilingScheme.ellipsoid, this.heightmapWidth, this.tilingScheme.getNumberOfXTilesAtLevel(0)); + this.ready = true; + + this._tileDataAvailable = {}; + this._requestTileGeometryWillSucceed = {}; + this._createMeshWillSucceed = {}; + this._upsampleWillSucceed = {}; + this._nearestBvhLevel = {}; + } + + MockTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { + var willSucceed = this._requestTileGeometryWillSucceed[createTileKey(x, y, level)]; + if (willSucceed === undefined) { + return undefined; // defer by default + } + + var that = this; + return runLater(function() { + if (willSucceed) { + return createTerrainData(that, x, y, level, false); + } + throw new RuntimeError('requestTileGeometry failed as requested.'); + }); + }; + + MockTerrainProvider.prototype.getTileDataAvailable = function(xOrTile, y, level) { + return this._tileDataAvailable[createTileKey(xOrTile, y, level)]; + }; + + MockTerrainProvider.prototype.getNearestBvhLevel = function(x, y, level) { + return this._nearestBvhLevel[createTileKey(x, y, level)]; + }; + + MockTerrainProvider.prototype.getLevelMaximumGeometricError = function(level) { + return this.levelZeroMaximumGeometricError / (1 << level); + }; + + MockTerrainProvider.prototype.requestTileGeometryWillSucceed = function(xOrTile, y, level) { + this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = true; + return this; + }; + + MockTerrainProvider.prototype.requestTileGeometryWillFail = function(xOrTile, y, level) { + this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = false; + return this; + }; + + MockTerrainProvider.prototype.requestTileGeometryWillDefer = function(xOrTile, y, level) { + this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = undefined; + return this; + }; + + MockTerrainProvider.prototype.createMeshWillSucceed = function(xOrTile, y, level) { + this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = true; + return this; + }; + + MockTerrainProvider.prototype.createMeshWillFail = function(xOrTile, y, level) { + this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = false; + return this; + }; + + MockTerrainProvider.prototype.createMeshWillDefer = function(xOrTile, y, level) { + this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = undefined; + return this; + }; + + MockTerrainProvider.prototype.upsampleWillSucceed = function(xOrTile, y, level) { + this._upsampleWillSucceed[createTileKey(xOrTile, y, level)] = true; + return this; + }; + + MockTerrainProvider.prototype.upsampleWillFail = function(xOrTile, y, level) { + this._upsampleWillSucceed[createTileKey(xOrTile, y, level)] = false; + return this; + }; + + MockTerrainProvider.prototype.upsampleWillDefer = function(xOrTile, y, level) { + this._upsampleWillSucceed[createTileKey(xOrTile, y, level)] = undefined; + return this; + }; + + MockTerrainProvider.prototype.willBeAvailable = function(xOrTile, y, level) { + this._tileDataAvailable[createTileKey(xOrTile, y, level)] = true; + return this; + }; + + MockTerrainProvider.prototype.willBeUnavailable = function(xOrTile, y, level) { + this._tileDataAvailable[createTileKey(xOrTile, y, level)] = false; + return this; + }; + + MockTerrainProvider.prototype.willBeUnknownAvailability = function(xOrTile, y, level) { + this._tileDataAvailable[createTileKey(xOrTile, y, level)] = undefined; + return this; + }; + + MockTerrainProvider.prototype.willHaveNearestBvhLevel = function(nearestBvhLevel, xOrTile, y, level) { + this._nearestBvhLevel[createTileKey(xOrTile, y, level)] = nearestBvhLevel; + return this; + }; + + function createTerrainData(terrainProvider, x, y, level, upsampled) { + var terrainData = jasmine.createSpyObj('MockTerrainData', ['createMesh', 'upsample', 'wasCreatedByUpsampling']); + terrainData.wasCreatedByUpsampling.and.returnValue(upsampled); + + terrainData.createMesh.and.callFake(function(tilingScheme, x, y, level) { + var willSucceed = terrainProvider._createMeshWillSucceed[createTileKey(x, y, level)]; + if (willSucceed === undefined) { + return undefined; // defer by default + } + + return runLater(function() { + if (willSucceed) { + return {}; + } + throw new RuntimeError('createMesh failed as requested.'); + }); + }); + + terrainData.upsample.and.callFake(function(tilingScheme, thisX, thisY, thisLevel, descendantX, descendantY) { + var willSucceed = terrainProvider._upsampleWillSucceed[createTileKey(descendantX, descendantY, thisLevel + 1)]; + if (willSucceed === undefined) { + return undefined; // defer by default + } + + return runLater(function() { + if (willSucceed) { + return createTerrainData(terrainProvider, descendantX, descendantY, thisLevel + 1, true); + } + throw new RuntimeError('upsample failed as requested.'); + }); + }); + + return terrainData; + } + + return MockTerrainProvider; +}); diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 46e742a2812..e16216caec5 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -11,7 +11,10 @@ defineSuite([ 'Core/Ray', 'Core/Rectangle', 'Core/RequestScheduler', + 'Core/TerrainEncoding', + 'Core/TerrainMesh', 'Core/TileAvailability', + 'Renderer/Buffer', 'Scene/Imagery', 'Scene/ImageryLayer', 'Scene/ImageryLayerCollection', @@ -22,7 +25,9 @@ defineSuite([ 'Scene/TileImagery', 'Specs/createScene', 'Specs/pollToPromise', - 'ThirdParty/when' + 'ThirdParty/when', + '../MockImageryProvider', + '../MockTerrainProvider' ], function( GlobeSurfaceTile, Cartesian3, @@ -36,7 +41,10 @@ defineSuite([ Ray, Rectangle, RequestScheduler, + TerrainEncoding, + TerrainMesh, TileAvailability, + Buffer, Imagery, ImageryLayer, ImageryLayerCollection, @@ -47,17 +55,25 @@ defineSuite([ TileImagery, createScene, pollToPromise, - when) { + when, + MockImageryProvider, + MockTerrainProvider) { 'use strict'; describe('processStateMachine', function() { - var frameState = {}; + var frameState = { + context: { + cache: {}, + _gl: {} + } + }; var terrainProvider; var tilingScheme; var rootTiles; var rootTile; var imageryLayerCollection; + var mockTerrain; beforeEach(function() { tilingScheme = new GeographicTilingScheme(); @@ -81,6 +97,23 @@ defineSuite([ return -1; } }; + + mockTerrain = new MockTerrainProvider(); + + // Skip the WebGL bits + spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() { + var vertexArray = jasmine.createSpyObj('VertexArray', ['destroy']); + return vertexArray; + }); + + spyOn(ImageryLayer.prototype, '_createTextureWebGL').and.callFake(function(context, imagery) { + var texture = jasmine.createSpyObj('Texture', ['destroy']); + texture.width = imagery.image.width; + texture.height = imagery.image.height; + return texture; + }); + + spyOn(ImageryLayer.prototype, '_finalizeReprojectTexture'); }); afterEach(function() { @@ -96,277 +129,456 @@ defineSuite([ } }); + // Processes the given list of tiles until all terrain and imagery states stop changing. + function processTiles(tiles) { + var deferred = when.defer(); + + function getState(tile) { + return [ + tile.state, + tile.data ? tile.data.terrainState : undefined, + tile.data && tile.data.imagery ? tile.data.imagery.map(function(imagery) { + return [ + imagery.readyImagery ? imagery.readyImagery.state : undefined, + imagery.loadingImagery ? imagery.loadingImagery.state : undefined + ]; + }) : [] + ]; + } + + function statesAreSame(a, b) { + if (a.length !== b.length) { + return false; + } + + var same = true; + for (var i = 0; i < a.length; ++i) { + if (Array.isArray(a[i]) && Array.isArray(b[i])) { + same = same && statesAreSame(a[i], b[i]); + } else if (Array.isArray(a[i]) || Array.isArray(b[i])) { + same = false; + } else { + same = same && a[i] === b[i]; + } + } + + return same; + } + + function next() { + // Keep going until all terrain and imagery provider are ready and states are no longer changing. + var changed = !mockTerrain.ready; + + for (var i = 0; i < imageryLayerCollection.length; ++i) { + changed = changed || !imageryLayerCollection.get(i).imageryProvider.ready; + } + + tiles.forEach(function(tile) { + var beforeState = getState(tile); + GlobeSurfaceTile.processStateMachine(tile, frameState, mockTerrain, imageryLayerCollection); + var afterState = getState(tile); + changed = changed || !statesAreSame(beforeState, afterState); + }); + + if (changed) { + setTimeout(next, 0); + } else { + deferred.resolve(); + } + } + + next(); + + return deferred.promise; + } + it('transitions to the LOADING state immediately if this tile is available', function() { - GlobeSurfaceTile.processStateMachine(rootTile, frameState, terrainProvider, imageryLayerCollection); - expect(rootTile.state).toBe(QuadtreeTileLoadState.LOADING); - expect(rootTile.data.terrainState).toBe(TerrainState.UNLOADED); + mockTerrain + .willBeAvailable(rootTile.southwestChild); + + return processTiles([rootTile.southwestChild]).then(function() { + expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING); + expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.UNLOADED); + }); }); it('transitions to the LOADING tile state and FAILED terrain state immediately if this tile is NOT available', function() { - makeTerrainReady(rootTile); - spyOn(terrainProvider, 'getTileDataAvailable').and.returnValue(false); - GlobeSurfaceTile.processStateMachine(rootTile.southwestChild, frameState, terrainProvider, imageryLayerCollection); - expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING); - expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.FAILED); + mockTerrain + .willBeUnavailable(rootTile.southwestChild); + + return processTiles([rootTile.southwestChild]).then(function() { + expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING); + expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.FAILED); + }); }); it('pushes parent along if waiting on it to be able to upsample', function() { - makeTerrainUnloaded(rootTile); - spyOn(terrainProvider, 'getTileDataAvailable').and.returnValue(false); - spyOn(terrainProvider, 'requestTileGeometry'); - GlobeSurfaceTile.processStateMachine(rootTile.southwestChild, frameState, terrainProvider, imageryLayerCollection); - expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); + mockTerrain + .willBeAvailable(rootTile) + .requestTileGeometryWillSucceed(rootTile) + .willBeUnavailable(rootTile.southwestChild); + + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + + return processTiles([rootTile.southwestChild]).then(function() { + expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); + }); }); - it('does nothing when attempting to upsample a failed root tile', function() { - makeTerrainFailed(rootTile); - GlobeSurfaceTile.processStateMachine(rootTile, frameState, terrainProvider, imageryLayerCollection); - expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); - expect(rootTile.data.terrainState).toBe(TerrainState.FAILED); + it('does nothing when a root tile is unavailable', function() { + mockTerrain + .willBeUnavailable(rootTile); + + return processTiles([rootTile]).then(function() { + expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); + expect(rootTile.data.terrainState).toBe(TerrainState.FAILED); + }); + }); + + it('does nothing when a root tile fails to load', function() { + mockTerrain + .requestTileGeometryWillFail(rootTile); + + return processTiles([rootTile]).then(function() { + expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); + expect(rootTile.data.terrainState).toBe(TerrainState.FAILED); + }); }); it('upsamples failed tiles from parent TerrainData', function() { - makeTerrainReceived(rootTile); - spyOn(terrainProvider, 'getTileDataAvailable').and.returnValue(false); - var sw = rootTile.southwestChild; - GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); - expect(rootTile.data.terrainData.upsample).toHaveBeenCalledWith(tilingScheme, rootTile.x, rootTile.y, rootTile.level, sw.x, sw.y, sw.level); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willBeUnavailable(rootTile.southwestChild) + .upsampleWillSucceed(rootTile.southwestChild); + + return processTiles([rootTile, rootTile.southwestChild]).then(function() { + expect(rootTile.data.terrainState).toBe(TerrainState.RECEIVED); + expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.RECEIVED); + expect(rootTile.data.terrainData.wasCreatedByUpsampling()).toBe(false); + expect(rootTile.southwestChild.data.terrainData.wasCreatedByUpsampling()).toBe(true); + }); }); it('loads available tiles', function() { - var sw = rootTile.southwestChild; - terrainProvider.availability.addAvailableTileRange(sw.level, sw.x, sw.y, sw.x, sw.y); - spyOn(terrainProvider, 'requestTileGeometry'); - GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); - expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); + mockTerrain + .willBeAvailable(rootTile.southwestChild) + .requestTileGeometryWillSucceed(rootTile.southwestChild); + + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + + return processTiles([rootTile.southwestChild]).then(function() { + expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); + }); }); it('loads BVH nodes instead when the tile\'s bounding volume is unreliable', function() { - var sw = rootTile.southwestChild; - makeTerrainUnloaded(rootTile); - makeTerrainUnloaded(sw); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveNearestBvhLevel(0, rootTile.southwestChild); - // Indicate that the SW tile's bounding volume comes from the root. - sw.data.boundingVolumeSourceTile = rootTile; + return processTiles([rootTile.southwestChild]).then(function() { + // Indicate that the SW tile's bounding volume comes from the root. + rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; - // Indicate that level 0 has BVH data for the SW tile. - spyOn(terrainProvider, 'getNearestBvhLevel').and.returnValue(0); - spyOn(terrainProvider, 'requestTileGeometry'); + // Monitor calls to requestTileGeometry - we should only see one for the root tile now. + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); - - // Expect that the rootTile is loaded, not the SW tile. - expect(terrainProvider.getNearestBvhLevel).toHaveBeenCalledWith(sw.x, sw.y, sw.level); - expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); + return processTiles([rootTile.southwestChild]); + }).then(function() { + expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); + }); }); it('loads this tile if the nearest BVH level is unknown', function() { - var sw = rootTile.southwestChild; - makeTerrainUnloaded(rootTile); - makeTerrainUnloaded(sw); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveNearestBvhLevel(-1, rootTile.southwestChild); - // Indicate that the SW tile's bounding volume comes from the root. - sw.data.boundingVolumeSourceTile = rootTile; + return processTiles([rootTile.southwestChild]).then(function() { + // Indicate that the SW tile's bounding volume comes from the root. + rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; - // Indicate that no BVH data is available for the SW tile - spyOn(terrainProvider, 'getNearestBvhLevel').and.returnValue(-1); - spyOn(terrainProvider, 'requestTileGeometry'); + // Monitor calls to requestTileGeometry - we should only see one for the southwest tile now. + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); - - // Expect that the SW tile is loaded. - expect(terrainProvider.getNearestBvhLevel).toHaveBeenCalledWith(sw.x, sw.y, sw.level); - expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); + return processTiles([rootTile.southwestChild]); + }).then(function() { + expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); + }); }); it('loads this tile if the terrain provider does not implement getNearestBvhLevel', function() { - var sw = rootTile.southwestChild; - makeTerrainUnloaded(rootTile); - makeTerrainUnloaded(sw); + mockTerrain.getNearestBvhLevel = undefined; - // Indicate that the SW tile's bounding volume comes from the root. - sw.data.boundingVolumeSourceTile = rootTile; + mockTerrain + .requestTileGeometryWillSucceed(rootTile); - // Indicate that no BVH data is available for the SW tile - terrainProvider.getNearestBvhLevel = undefined; - spyOn(terrainProvider, 'requestTileGeometry'); + return processTiles([rootTile.southwestChild]).then(function() { + // Indicate that the SW tile's bounding volume comes from the root. + rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; - GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + // Monitor calls to requestTileGeometry - we should only see one for the southwest tile now. + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - // Expect that the SW tile is loaded. - expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); + return processTiles([rootTile.southwestChild]); + }).then(function() { + expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); + }); }); it('loads only terrain (not imagery) when loading BVH nodes', function() { - var sw = rootTile.southwestChild; - makeTerrainUnloaded(rootTile); - makeTerrainUnloaded(sw); + var mockImagery = new MockImageryProvider(); + imageryLayerCollection.addImageryProvider(mockImagery); - // Indicate that the SW tile's bounding volume comes from the root. - sw.data.boundingVolumeSourceTile = rootTile; + mockImagery + .requestImageWillSucceed(rootTile) - // Indicate that level 0 has BVH data for the SW tile. - spyOn(terrainProvider, 'getNearestBvhLevel').and.returnValue(0); - spyOn(terrainProvider, 'requestTileGeometry'); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveNearestBvhLevel(0, rootTile.southwestChild); - var _createTileImagerySkeletons = jasmine.createSpy('_createTileImagerySkeletons'); - sw.data.imagery.push({ - loadingImagery : { - state : ImageryState.PLACEHOLDER, - imageryLayer : { - imageryProvider : { - ready : true - }, - _createTileImagerySkeletons : _createTileImagerySkeletons - } - }, - freeResources: function() {} + return processTiles([rootTile.southwestChild]).then(function() { + // Indicate that the SW tile's bounding volume comes from the root. + rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; + + // Monitor calls to requestTileGeometry and requestImage. + // We should see a terrain request but not an imagery request. + spyOn(mockImagery, 'requestImage').and.callThrough(); + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + + return processTiles([rootTile.southwestChild]); + }).then(function() { + expect(mockImagery.requestImage.calls.count()).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); + expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); }); + }); + + it('marks an upsampled tile as such', function() { + mockTerrain + .willBeAvailable(rootTile) + .requestTileGeometryWillSucceed(rootTile) + .createMeshWillSucceed(rootTile) + .willBeUnavailable(rootTile.southwestChild) + .upsampleWillSucceed(rootTile.southwestChild) + .createMeshWillSucceed(rootTile.southwestChild); - GlobeSurfaceTile.processStateMachine(sw, frameState, terrainProvider, imageryLayerCollection); + var mockImagery = new MockImageryProvider(); + imageryLayerCollection.addImageryProvider(mockImagery); - // Expect that the rootTile is loaded, not the SW tile. - expect(terrainProvider.getNearestBvhLevel).toHaveBeenCalledWith(sw.x, sw.y, sw.level); - expect(terrainProvider.requestTileGeometry.calls.count()).toBe(1); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); - expect(terrainProvider.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); + mockImagery + .requestImageWillSucceed(rootTile) + .requestImageWillFail(rootTile.southwestChild); - // Expect that _createTileImagerySkeletons was _not_ called - expect(_createTileImagerySkeletons).not.toHaveBeenCalled(); + return processTiles([rootTile, rootTile.southwestChild]).then(function() { + expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE); + expect(rootTile.upsampledFromParent).toBe(false); + expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE); + expect(rootTile.southwestChild.upsampledFromParent).toBe(true); + }); }); - /*it('entirely upsampled tile is marked as such', function() { - var childTile = rootTile.children[0]; + it('does not mark a tile as upsampled if it has fresh imagery', function() { + mockTerrain + .willBeAvailable(rootTile) + .requestTileGeometryWillSucceed(rootTile) + .createMeshWillSucceed(rootTile) + .willBeUnavailable(rootTile.southwestChild) + .upsampleWillSucceed(rootTile.southwestChild) + .createMeshWillSucceed(rootTile.southwestChild); - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(rootTile, frameState, realTerrainProvider, imageryLayerCollection); - GlobeSurfaceTile.processStateMachine(childTile, frameState, alwaysFailTerrainProvider, imageryLayerCollection); - RequestScheduler.update(); - return rootTile.state >= QuadtreeTileLoadState.DONE && - childTile.state >= QuadtreeTileLoadState.DONE; - }).then(function() { + var mockImagery = new MockImageryProvider(); + imageryLayerCollection.addImageryProvider(mockImagery); + + mockImagery + .requestImageWillSucceed(rootTile) + .requestImageWillSucceed(rootTile.southwestChild); + + return processTiles([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE); - expect(childTile.upsampledFromParent).toBe(true); + expect(rootTile.upsampledFromParent).toBe(false); + expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE); + expect(rootTile.southwestChild.upsampledFromParent).toBe(false); }); }); - it('uses shared water mask texture for tiles that are entirely water', function() { - var allWaterTerrainProvider = { - requestTileGeometry : function(x, y, level) { - var real = realTerrainProvider.requestTileGeometry(x, y, level); - if (!defined(real)) { - return real; - } + it('does not mark a tile as upsampled if it has fresh terrain', function() { + mockTerrain + .willBeAvailable(rootTile) + .requestTileGeometryWillSucceed(rootTile) + .createMeshWillSucceed(rootTile) + .willBeAvailable(rootTile.southwestChild) + .requestTileGeometryWillSucceed(rootTile.southwestChild) + .createMeshWillSucceed(rootTile.southwestChild); - return when(real, function(terrainData) { - terrainData._waterMask = new Uint8Array([255]); - return terrainData; - }); - }, - tilingScheme : realTerrainProvider.tilingScheme, - hasWaterMask : function() { - return realTerrainProvider.hasWaterMask(); - }, - getTileDataAvailable : function(x, y, level) { - return undefined; - } - }; + var mockImagery = new MockImageryProvider(); + imageryLayerCollection.addImageryProvider(mockImagery); - var childTile = rootTile.children[0]; + mockImagery + .requestImageWillSucceed(rootTile) + .requestImageWillFail(rootTile.southwestChild); - return pollToPromise(function() { - if (rootTile.state !== QuadtreeTileLoadState.DONE) { - GlobeSurfaceTile.processStateMachine(rootTile, frameState, allWaterTerrainProvider, imageryLayerCollection); - return false; - } - GlobeSurfaceTile.processStateMachine(childTile, frameState, allWaterTerrainProvider, imageryLayerCollection); - return childTile.state === QuadtreeTileLoadState.DONE; - }).then(function() { - expect(childTile.data.waterMaskTexture).toBeDefined(); - expect(childTile.data.waterMaskTexture).toBe(rootTile.data.waterMaskTexture); + return processTiles([rootTile, rootTile.southwestChild]).then(function() { + expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE); + expect(rootTile.upsampledFromParent).toBe(false); + expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE); + expect(rootTile.southwestChild.upsampledFromParent).toBe(false); }); }); + }); - it('uses undefined water mask texture for tiles that are entirely land', function() { - var allLandTerrainProvider = { - requestTileGeometry : function(x, y, level) { - var real = realTerrainProvider.requestTileGeometry(x, y, level); - if (!defined(real)) { - return real; - } + describe('water mask', function() { + var scene; - return when(real, function(terrainData) { - terrainData._waterMask = new Uint8Array([0]); - return terrainData; - }); - }, - tilingScheme : realTerrainProvider.tilingScheme, - hasWaterMask : function() { - return realTerrainProvider.hasWaterMask(); - }, - getTileDataAvailable : function(x, y, level) { - return undefined; - } + beforeAll(function() { + scene = createScene(); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + it('is created from one-byte water mask data, if it exists', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() + }); + + var terrainProvider = { + hasWaterMask: true }; - var childTile = rootTile.children[0]; + makeTerrainReceived(tile); + tile.data.terrainData.waterMask = new Uint8Array(1); + tile.data.terrainData.waterMask[0] = 1; + GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); - return pollToPromise(function() { - if (rootTile.state !== QuadtreeTileLoadState.DONE) { - GlobeSurfaceTile.processStateMachine(rootTile, frameState, allLandTerrainProvider, imageryLayerCollection); - return false; - } - GlobeSurfaceTile.processStateMachine(childTile, frameState, allLandTerrainProvider, imageryLayerCollection); - return childTile.state === QuadtreeTileLoadState.DONE; - }).then(function() { - expect(childTile.data.waterMaskTexture).toBeUndefined(); + expect(tile.data.waterMaskTexture).toBeDefined(); + }); + + it('uses undefined water mask texture for tiles that are entirely land', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() }); + + var terrainProvider = { + hasWaterMask: true + }; + + makeTerrainReceived(tile); + tile.data.terrainData.waterMask = new Uint8Array(1); + tile.data.terrainData.waterMask[0] = 0; + GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); + + expect(tile.data.waterMaskTexture).toBeUndefined(); }); - it('loads parent imagery tile even for root terrain tiles', function() { + it('uses shared water mask texture for tiles that are entirely water', function() { var tile = new QuadtreeTile({ - tilingScheme : new GeographicTilingScheme(), - level : 0, - x : 1, - y : 0 + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() }); - var imageryLayerCollection = new ImageryLayerCollection(); + var terrainProvider = { + hasWaterMask: true + }; - GlobeSurfaceTile.processStateMachine(tile, frameState, alwaysDeferTerrainProvider, imageryLayerCollection); + makeTerrainReceived(tile); + tile.data.terrainData.waterMask = new Uint8Array(1); + tile.data.terrainData.waterMask[0] = 1; + GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); - var layer = new ImageryLayer({ - requestImage : function() { - return when.reject(); - } + var sw = tile.southwestChild; + makeTerrainReceived(sw); + sw.data.terrainData.waterMask = new Uint8Array(1); + sw.data.terrainData.waterMask[0] = 1; + GlobeSurfaceTile.processStateMachine(sw, scene.frameState, terrainProvider, new ImageryLayerCollection()); + + expect(tile.data.waterMaskTexture).toBeDefined(); + expect(tile.data.waterMaskTexture).toBe(sw.data.waterMaskTexture); + }); + + it('is created from multi-byte water mask data, if it exists', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() }); - var imagery = new Imagery(layer, 0, 0, 1, Rectangle.MAX_VALUE); - tile.data.imagery.push(new TileImagery(imagery, new Cartesian4())); - expect(imagery.parent.state).toBe(ImageryState.UNLOADED); + var terrainProvider = { + hasWaterMask: true + }; - return pollToPromise(function() { - GlobeSurfaceTile.processStateMachine(tile, frameState, alwaysDeferTerrainProvider, imageryLayerCollection); - return imagery.parent.state !== ImageryState.UNLOADED; + makeTerrainReceived(tile); + tile.data.terrainData.waterMask = new Uint8Array(4); + tile.data.terrainData.waterMask[0] = 1; + tile.data.terrainData.waterMask[1] = 1; + tile.data.terrainData.waterMask[2] = 0; + tile.data.terrainData.waterMask[3] = 0; + GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); + + expect(tile.data.waterMaskTexture).toBeDefined(); + }); + + it('is created by upsampling if data is not available', function() { + var tile = new QuadtreeTile({ + level: 0, + x: 0, + y: 0, + tilingScheme: new GeographicTilingScheme() }); - });*/ - }); + + var terrainProvider = { + hasWaterMask: true + }; + + makeTerrainReceived(tile); + tile.data.terrainData.waterMask = new Uint8Array(4); + tile.data.terrainData.waterMask[0] = 1; + tile.data.terrainData.waterMask[1] = 1; + tile.data.terrainData.waterMask[2] = 0; + tile.data.terrainData.waterMask[3] = 0; + + var sw = tile.southwestChild; + makeTerrainReceived(sw); + + GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); + + // Parent doesn't have a water mask texture yet, so neither will the child + expect(sw.data.waterMaskTexture).not.toBeDefined(); + + GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); + GlobeSurfaceTile.processStateMachine(sw, scene.frameState, terrainProvider, new ImageryLayerCollection()); + + // But once the parent's water mask texture is created, the child will upsample from it + expect(sw.data.waterMaskTexture).toBeDefined(); + }); + }, 'WebGL'); describe('getBoundingVolumeHierarchy', function() { it('gets the BVH from the TerrainData if available', function() { @@ -468,7 +680,6 @@ defineSuite([ var imageryLayerCollection = new ImageryLayerCollection(); makeLoading(tile); - tile.data.boundingVolumeSourceTile = tile; return pollToPromise(function() { if (!terrainProvider.ready) { @@ -549,8 +760,8 @@ defineSuite([ tile.data.imagery.push({ loadingImagery : { - state : undefined, - }, + state : undefined + } }); Object.keys(ImageryState).forEach(function(state) { diff --git a/Specs/createTileKey.js b/Specs/createTileKey.js new file mode 100644 index 00000000000..c5207d56a20 --- /dev/null +++ b/Specs/createTileKey.js @@ -0,0 +1,15 @@ +define([], function() { + 'use strict'; + + function createTileKey(xOrTile, y, level) { + if (typeof xOrTile === 'object') { + var tile = xOrTile; + xOrTile = tile.x; + y = tile.y; + level = tile.level; + } + return [xOrTile, y, level].join(','); + } + + return createTileKey; +}); diff --git a/Specs/runLater.js b/Specs/runLater.js new file mode 100644 index 00000000000..54aa36ec05a --- /dev/null +++ b/Specs/runLater.js @@ -0,0 +1,24 @@ +define([ + 'Core/defaultValue', + 'ThirdParty/when' +], function( + defaultValue, + when) { + 'use strict'; + + function runLater(functionToRunLater, milliseconds) { + milliseconds = defaultValue(milliseconds, 0); + + var deferred = when.defer(); + setTimeout(function() { + try { + deferred.resolve(functionToRunLater()); + } catch (e) { + deferred.reject(e); + } + }, milliseconds); + return deferred.promise; + } + + return runLater; + }); From ac2c7922073db6542400e87acce6ccf40da46b80 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 17 Dec 2018 10:38:49 +1100 Subject: [PATCH 068/131] Water mask testing. --- Source/Renderer/Texture.js | 4 + Source/Scene/GlobeSurfaceTile.js | 6 +- Specs/MockTerrainProvider.js | 33 +++- Specs/Scene/GlobeSurfaceTileSpec.js | 243 ++++++++-------------------- 4 files changed, 106 insertions(+), 180 deletions(-) diff --git a/Source/Renderer/Texture.js b/Source/Renderer/Texture.js index 609798d7a9f..76840327af3 100644 --- a/Source/Renderer/Texture.js +++ b/Source/Renderer/Texture.js @@ -264,6 +264,10 @@ define([ this.sampler = defined(options.sampler) ? options.sampler : new Sampler(); } + Texture.create = function(options) { + return new Texture(options); + }; + /** * Creates a texture, and copies a subimage of the framebuffer to it. When called without arguments, * the texture is the same width and height as the framebuffer and contains its contents. diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index c5023667234..b0e70293584 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -581,7 +581,7 @@ define([ var data = context.cache.tile_waterMaskData; if (!defined(data)) { - var allWaterTexture = new Texture({ + var allWaterTexture = Texture.create({ context : context, pixelFormat : PixelFormat.LUMINANCE, pixelDatatype : PixelDatatype.UNSIGNED_BYTE, @@ -631,7 +631,7 @@ define([ } } else { var textureSize = Math.sqrt(waterMaskLength); - texture = new Texture({ + texture = Texture.create({ context : context, pixelFormat : PixelFormat.LUMINANCE, pixelDatatype : PixelDatatype.UNSIGNED_BYTE, @@ -658,7 +658,7 @@ define([ // Find the nearest ancestor with loaded terrain. var sourceTile = tile.parent; - while (defined(sourceTile) && !defined(sourceTile.data.terrainData) || sourceTile.data.terrainData.wasCreatedByUpsampling()) { + while (defined(sourceTile) && (!defined(sourceTile.data) || !defined(sourceTile.data.terrainData) || sourceTile.data.terrainData.wasCreatedByUpsampling())) { sourceTile = sourceTile.parent; } diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js index 895a5cdfaa1..15c017accd5 100644 --- a/Specs/MockTerrainProvider.js +++ b/Specs/MockTerrainProvider.js @@ -1,12 +1,12 @@ define([ - 'Core/defaultValue', + 'Core/defined', 'Core/GeographicTilingScheme', 'Core/RuntimeError', 'Core/TerrainProvider', './createTileKey', './runLater' ], function( - defaultValue, + defined, GeographicTilingScheme, RuntimeError, TerrainProvider, @@ -19,9 +19,11 @@ define([ this.heightmapWidth = 65; this.levelZeroMaximumGeometricError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(this.tilingScheme.ellipsoid, this.heightmapWidth, this.tilingScheme.getNumberOfXTilesAtLevel(0)); this.ready = true; + this.hasWaterMask = true; this._tileDataAvailable = {}; this._requestTileGeometryWillSucceed = {}; + this._willHaveWaterMask = {}; this._createMeshWillSucceed = {}; this._upsampleWillSucceed = {}; this._nearestBvhLevel = {}; @@ -36,7 +38,24 @@ define([ var that = this; return runLater(function() { if (willSucceed) { - return createTerrainData(that, x, y, level, false); + var terrainData = createTerrainData(that, x, y, level, false); + var willHaveWaterMask = that._willHaveWaterMask[createTileKey(x, y, level)]; + if (defined(willHaveWaterMask)) { + if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { + terrainData.waterMask = new Uint8Array(4); + terrainData.waterMask[0] = 1; + terrainData.waterMask[1] = 1; + terrainData.waterMask[2] = 0; + terrainData.waterMask[3] = 0; + } else if (willHaveWaterMask.includeLand) { + terrainData.waterMask = new Uint8Array(1); + terrainData.waterMask[0] = 0; + } else if (willHaveWaterMask.includeWater) { + terrainData.waterMask = new Uint8Array(1); + terrainData.waterMask[0] = 1; + } + } + return terrainData; } throw new RuntimeError('requestTileGeometry failed as requested.'); }); @@ -69,6 +88,14 @@ define([ return this; }; + MockTerrainProvider.prototype.willHaveWaterMask = function(includeLand, includeWater, xOrTile, y, level) { + this._willHaveWaterMask[createTileKey(xOrTile, y, level)] = includeLand || includeWater ? { + includeLand: includeLand, + includeWater: includeWater + } : undefined; + return this; + }; + MockTerrainProvider.prototype.createMeshWillSucceed = function(xOrTile, y, level) { this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = true; return this; diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index e16216caec5..0dc53db565c 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -3,6 +3,7 @@ defineSuite([ 'Core/Cartesian3', 'Core/Cartesian4', 'Core/CesiumTerrainProvider', + 'Core/clone', 'Core/createWorldTerrain', 'Core/defined', 'Core/Ellipsoid', @@ -15,6 +16,7 @@ defineSuite([ 'Core/TerrainMesh', 'Core/TileAvailability', 'Renderer/Buffer', + 'Renderer/Texture', 'Scene/Imagery', 'Scene/ImageryLayer', 'Scene/ImageryLayerCollection', @@ -33,6 +35,7 @@ defineSuite([ Cartesian3, Cartesian4, CesiumTerrainProvider, + clone, createWorldTerrain, defined, Ellipsoid, @@ -45,6 +48,7 @@ defineSuite([ TerrainMesh, TileAvailability, Buffer, + Texture, Imagery, ImageryLayer, ImageryLayerCollection, @@ -60,6 +64,27 @@ defineSuite([ MockTerrainProvider) { 'use strict'; + var tilingScheme; + var rootTiles; + var rootTile; + var imageryLayerCollection; + var mockTerrain; + + beforeEach(function() { + tilingScheme = new GeographicTilingScheme(); + rootTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); + rootTile = rootTiles[0]; + imageryLayerCollection = new ImageryLayerCollection(); + + mockTerrain = new MockTerrainProvider(); + }); + + afterEach(function() { + for (var i = 0; i < rootTiles.length; ++i) { + rootTiles[i].freeResources(); + } + }); + describe('processStateMachine', function() { var frameState = { context: { @@ -67,39 +92,8 @@ defineSuite([ _gl: {} } }; - var terrainProvider; - - var tilingScheme; - var rootTiles; - var rootTile; - var imageryLayerCollection; - var mockTerrain; beforeEach(function() { - tilingScheme = new GeographicTilingScheme(); - rootTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); - rootTile = rootTiles[0]; - imageryLayerCollection = new ImageryLayerCollection(); - - terrainProvider = { - requestTileGeometry : function(x, y, level) { - return undefined; - }, - tilingScheme : tilingScheme, - availability : new TileAvailability(tilingScheme, 10), - hasWaterMask : function() { - return true; - }, - getTileDataAvailable : function(x, y, level) { - return undefined; - }, - getNearestBvhLevel : function(x, y, level) { - return -1; - } - }; - - mockTerrain = new MockTerrainProvider(); - // Skip the WebGL bits spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() { var vertexArray = jasmine.createSpyObj('VertexArray', ['destroy']); @@ -114,12 +108,12 @@ defineSuite([ }); spyOn(ImageryLayer.prototype, '_finalizeReprojectTexture'); - }); - afterEach(function() { - for (var i = 0; i < rootTiles.length; ++i) { - rootTiles[i].freeResources(); - } + spyOn(Texture, 'create').and.callFake(function(options) { + var result = clone(options); + result.destroy = function() {}; + return result; + }); }); it('starts in the START state', function() { @@ -346,7 +340,7 @@ defineSuite([ imageryLayerCollection.addImageryProvider(mockImagery); mockImagery - .requestImageWillSucceed(rootTile) + .requestImageWillSucceed(rootTile); mockTerrain .requestTileGeometryWillSucceed(rootTile) @@ -442,143 +436,62 @@ defineSuite([ expect(rootTile.southwestChild.upsampledFromParent).toBe(false); }); }); - }); - - describe('water mask', function() { - var scene; - - beforeAll(function() { - scene = createScene(); - }); - afterAll(function() { - scene.destroyForSpecs(); - }); + it('creates water mask texture from one-byte water mask data, if it exists', function() { + mockTerrain + .willBeAvailable(rootTile) + .requestTileGeometryWillSucceed(rootTile) + .willHaveWaterMask(false, true, rootTile); - it('is created from one-byte water mask data, if it exists', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() + return processTiles([rootTile]).then(function() { + expect(rootTile.data.waterMaskTexture).toBeDefined(); }); - - var terrainProvider = { - hasWaterMask: true - }; - - makeTerrainReceived(tile); - tile.data.terrainData.waterMask = new Uint8Array(1); - tile.data.terrainData.waterMask[0] = 1; - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); - - expect(tile.data.waterMaskTexture).toBeDefined(); }); it('uses undefined water mask texture for tiles that are entirely land', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - - var terrainProvider = { - hasWaterMask: true - }; - - makeTerrainReceived(tile); - tile.data.terrainData.waterMask = new Uint8Array(1); - tile.data.terrainData.waterMask[0] = 0; - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveWaterMask(true, false, rootTile); - expect(tile.data.waterMaskTexture).toBeUndefined(); + return processTiles([rootTile]).then(function() { + expect(rootTile.data.waterMaskTexture).toBeUndefined(); + }); }); it('uses shared water mask texture for tiles that are entirely water', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - - var terrainProvider = { - hasWaterMask: true - }; - - makeTerrainReceived(tile); - tile.data.terrainData.waterMask = new Uint8Array(1); - tile.data.terrainData.waterMask[0] = 1; - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); - - var sw = tile.southwestChild; - makeTerrainReceived(sw); - sw.data.terrainData.waterMask = new Uint8Array(1); - sw.data.terrainData.waterMask[0] = 1; - GlobeSurfaceTile.processStateMachine(sw, scene.frameState, terrainProvider, new ImageryLayerCollection()); - - expect(tile.data.waterMaskTexture).toBeDefined(); - expect(tile.data.waterMaskTexture).toBe(sw.data.waterMaskTexture); - }); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveWaterMask(false, true, rootTile) + .requestTileGeometryWillSucceed(rootTile.southwestChild) + .willHaveWaterMask(false, true, rootTile.southwestChild); - it('is created from multi-byte water mask data, if it exists', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() + return processTiles([rootTile, rootTile.southwestChild]).then(function() { + expect(rootTile.data.waterMaskTexture).toBe(rootTile.southwestChild.data.waterMaskTexture); }); - - var terrainProvider = { - hasWaterMask: true - }; - - makeTerrainReceived(tile); - tile.data.terrainData.waterMask = new Uint8Array(4); - tile.data.terrainData.waterMask[0] = 1; - tile.data.terrainData.waterMask[1] = 1; - tile.data.terrainData.waterMask[2] = 0; - tile.data.terrainData.waterMask[3] = 0; - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); - - expect(tile.data.waterMaskTexture).toBeDefined(); }); - it('is created by upsampling if data is not available', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - - var terrainProvider = { - hasWaterMask: true - }; - - makeTerrainReceived(tile); - tile.data.terrainData.waterMask = new Uint8Array(4); - tile.data.terrainData.waterMask[0] = 1; - tile.data.terrainData.waterMask[1] = 1; - tile.data.terrainData.waterMask[2] = 0; - tile.data.terrainData.waterMask[3] = 0; - - var sw = tile.southwestChild; - makeTerrainReceived(sw); - - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); + it('creates water mask texture from multi-byte water mask data, if it exists', function() { + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveWaterMask(true, true, rootTile); - // Parent doesn't have a water mask texture yet, so neither will the child - expect(sw.data.waterMaskTexture).not.toBeDefined(); + return processTiles([rootTile]).then(function() { + expect(rootTile.data.waterMaskTexture).toBeDefined(); + }); + }); - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, new ImageryLayerCollection()); - GlobeSurfaceTile.processStateMachine(sw, scene.frameState, terrainProvider, new ImageryLayerCollection()); + it('upsamples water mask if data is not available', function() { + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveWaterMask(false, true, rootTile) + .requestTileGeometryWillSucceed(rootTile.southwestChild); - // But once the parent's water mask texture is created, the child will upsample from it - expect(sw.data.waterMaskTexture).toBeDefined(); + return processTiles([rootTile, rootTile.southwestChild]).then(function() { + expect(rootTile.southwestChild.data.waterMaskTexture).toBeDefined(); + expect(rootTile.southwestChild.data.waterMaskTranslationAndScale).toEqual(new Cartesian4(0.0, 0.0, 0.5, 0.5)); + }); }); - }, 'WebGL'); + }); describe('getBoundingVolumeHierarchy', function() { it('gets the BVH from the TerrainData if available', function() { @@ -782,14 +695,6 @@ defineSuite([ tile.state = QuadtreeTileLoadState.LOADING; } - function makeTerrainFailed(tile) { - makeLoading(tile); - tile.data.terrainState = TerrainState.FAILED; - tile.data.terrainData = undefined; - tile.data.vertexArray = undefined; - tile.data.mesh = undefined; - } - function makeTerrainUnloaded(tile) { makeLoading(tile); tile.data.terrainState = TerrainState.UNLOADED; @@ -808,16 +713,6 @@ defineSuite([ tile.data.vertexArray = undefined; } - function makeTerrainUpsampled(tile) { - makeLoading(tile); - tile.data.terrainState = TerrainState.RECEIVED; - tile.data.terrainData = jasmine.createSpyObj('TerrainData', ['createMesh', 'upsample', 'wasCreatedByUpsampling']); - tile.data.terrainData.wasCreatedByUpsampling.and.returnValue(true); - - tile.data.mesh = undefined; - tile.data.vertexArray = undefined; - } - function makeTerrainTransformed(tile) { makeLoading(tile); From 024dd6f5de8b4d24f32bd006407e155f66145fa8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 17 Dec 2018 18:40:59 +1100 Subject: [PATCH 069/131] Start moving getBoundingVolumeHierarchy specs to new system. --- Specs/MockTerrainProvider.js | 13 ++ Specs/Scene/GlobeSurfaceTileSpec.js | 244 ++++++++++++++-------------- Specs/createTileKey.js | 11 +- 3 files changed, 144 insertions(+), 124 deletions(-) diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js index 15c017accd5..70879bf13ff 100644 --- a/Specs/MockTerrainProvider.js +++ b/Specs/MockTerrainProvider.js @@ -24,6 +24,7 @@ define([ this._tileDataAvailable = {}; this._requestTileGeometryWillSucceed = {}; this._willHaveWaterMask = {}; + this._willHaveBvh = {}; this._createMeshWillSucceed = {}; this._upsampleWillSucceed = {}; this._nearestBvhLevel = {}; @@ -39,6 +40,7 @@ define([ return runLater(function() { if (willSucceed) { var terrainData = createTerrainData(that, x, y, level, false); + var willHaveWaterMask = that._willHaveWaterMask[createTileKey(x, y, level)]; if (defined(willHaveWaterMask)) { if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { @@ -55,6 +57,12 @@ define([ terrainData.waterMask[0] = 1; } } + + var willHaveBvh = that._willHaveBvh[createTileKey(x, y, level)]; + if (defined(willHaveBvh)) { + terrainData.bvh = willHaveBvh; + } + return terrainData; } throw new RuntimeError('requestTileGeometry failed as requested.'); @@ -96,6 +104,11 @@ define([ return this; }; + MockTerrainProvider.prototype.willHaveBvh = function(bvh, xOrTile, y, level) { + this._willHaveBvh[createTileKey(xOrTile, y, level)] = bvh; + return this; + }; + MockTerrainProvider.prototype.createMeshWillSucceed = function(xOrTile, y, level) { this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = true; return this; diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 0dc53db565c..7c19a25445e 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -64,13 +64,105 @@ defineSuite([ MockTerrainProvider) { 'use strict'; + var frameState; var tilingScheme; var rootTiles; var rootTile; var imageryLayerCollection; var mockTerrain; + function mockWebGL() { + spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() { + var vertexArray = jasmine.createSpyObj('VertexArray', ['destroy']); + return vertexArray; + }); + + spyOn(ImageryLayer.prototype, '_createTextureWebGL').and.callFake(function(context, imagery) { + var texture = jasmine.createSpyObj('Texture', ['destroy']); + texture.width = imagery.image.width; + texture.height = imagery.image.height; + return texture; + }); + + spyOn(ImageryLayer.prototype, '_finalizeReprojectTexture'); + + spyOn(Texture, 'create').and.callFake(function(options) { + var result = clone(options); + result.destroy = function() {}; + return result; + }); + } + + // Processes the given list of tiles until all terrain and imagery states stop changing. + function processTiles(tiles) { + var deferred = when.defer(); + + function getState(tile) { + return [ + tile.state, + tile.data ? tile.data.terrainState : undefined, + tile.data && tile.data.imagery ? tile.data.imagery.map(function(imagery) { + return [ + imagery.readyImagery ? imagery.readyImagery.state : undefined, + imagery.loadingImagery ? imagery.loadingImagery.state : undefined + ]; + }) : [] + ]; + } + + function statesAreSame(a, b) { + if (a.length !== b.length) { + return false; + } + + var same = true; + for (var i = 0; i < a.length; ++i) { + if (Array.isArray(a[i]) && Array.isArray(b[i])) { + same = same && statesAreSame(a[i], b[i]); + } else if (Array.isArray(a[i]) || Array.isArray(b[i])) { + same = false; + } else { + same = same && a[i] === b[i]; + } + } + + return same; + } + + function next() { + // Keep going until all terrain and imagery provider are ready and states are no longer changing. + var changed = !mockTerrain.ready; + + for (var i = 0; i < imageryLayerCollection.length; ++i) { + changed = changed || !imageryLayerCollection.get(i).imageryProvider.ready; + } + + tiles.forEach(function(tile) { + var beforeState = getState(tile); + GlobeSurfaceTile.processStateMachine(tile, frameState, mockTerrain, imageryLayerCollection); + var afterState = getState(tile); + changed = changed || !statesAreSame(beforeState, afterState); + }); + + if (changed) { + setTimeout(next, 0); + } else { + deferred.resolve(); + } + } + + next(); + + return deferred.promise; + } + beforeEach(function() { + frameState = { + context: { + cache: {} + } + }; + tilingScheme = new GeographicTilingScheme(); rootTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme); rootTile = rootTiles[0]; @@ -86,34 +178,8 @@ defineSuite([ }); describe('processStateMachine', function() { - var frameState = { - context: { - cache: {}, - _gl: {} - } - }; - beforeEach(function() { - // Skip the WebGL bits - spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() { - var vertexArray = jasmine.createSpyObj('VertexArray', ['destroy']); - return vertexArray; - }); - - spyOn(ImageryLayer.prototype, '_createTextureWebGL').and.callFake(function(context, imagery) { - var texture = jasmine.createSpyObj('Texture', ['destroy']); - texture.width = imagery.image.width; - texture.height = imagery.image.height; - return texture; - }); - - spyOn(ImageryLayer.prototype, '_finalizeReprojectTexture'); - - spyOn(Texture, 'create').and.callFake(function(options) { - var result = clone(options); - result.destroy = function() {}; - return result; - }); + mockWebGL(); }); it('starts in the START state', function() { @@ -123,69 +189,6 @@ defineSuite([ } }); - // Processes the given list of tiles until all terrain and imagery states stop changing. - function processTiles(tiles) { - var deferred = when.defer(); - - function getState(tile) { - return [ - tile.state, - tile.data ? tile.data.terrainState : undefined, - tile.data && tile.data.imagery ? tile.data.imagery.map(function(imagery) { - return [ - imagery.readyImagery ? imagery.readyImagery.state : undefined, - imagery.loadingImagery ? imagery.loadingImagery.state : undefined - ]; - }) : [] - ]; - } - - function statesAreSame(a, b) { - if (a.length !== b.length) { - return false; - } - - var same = true; - for (var i = 0; i < a.length; ++i) { - if (Array.isArray(a[i]) && Array.isArray(b[i])) { - same = same && statesAreSame(a[i], b[i]); - } else if (Array.isArray(a[i]) || Array.isArray(b[i])) { - same = false; - } else { - same = same && a[i] === b[i]; - } - } - - return same; - } - - function next() { - // Keep going until all terrain and imagery provider are ready and states are no longer changing. - var changed = !mockTerrain.ready; - - for (var i = 0; i < imageryLayerCollection.length; ++i) { - changed = changed || !imageryLayerCollection.get(i).imageryProvider.ready; - } - - tiles.forEach(function(tile) { - var beforeState = getState(tile); - GlobeSurfaceTile.processStateMachine(tile, frameState, mockTerrain, imageryLayerCollection); - var afterState = getState(tile); - changed = changed || !statesAreSame(beforeState, afterState); - }); - - if (changed) { - setTimeout(next, 0); - } else { - deferred.resolve(); - } - } - - next(); - - return deferred.promise; - } - it('transitions to the LOADING state immediately if this tile is available', function() { mockTerrain .willBeAvailable(rootTile.southwestChild); @@ -494,45 +497,40 @@ defineSuite([ }); describe('getBoundingVolumeHierarchy', function() { + beforeEach(function() { + mockWebGL(); + }); + it('gets the BVH from the TerrainData if available', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - makeTerrainReceived(tile); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveBvh(new Float32Array([1.0, 2.0]), rootTile); - var bvh = [1.0, 2.0]; - tile.data.terrainData.bvh = bvh; - expect(tile.data.getBoundingVolumeHierarchy(tile)).toBe(bvh); + return processTiles([rootTile]).then(function() { + expect(rootTile.data.getBoundingVolumeHierarchy(rootTile)).toEqual([1.0, 2.0]); + }); }); it('gets the BVH from the parent tile if available', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - makeTerrainReceived(tile); - tile.data.terrainData.bvh = new Float32Array([1.0, 10.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]); - - var sw = tile.southwestChild; - makeLoading(sw); - expect(sw.data.getBoundingVolumeHierarchy(sw)).toEqual([3.0, 4.0]); + var sw = rootTile.southwestChild; + var nw = rootTile.northwestChild; + var se = rootTile.southeastChild; + var ne = rootTile.northeastChild; - var se = tile.southeastChild; - makeLoading(se); - expect(se.data.getBoundingVolumeHierarchy(se)).toEqual([5.0, 6.0]); - - var nw = tile.northwestChild; - makeLoading(nw); - expect(nw.data.getBoundingVolumeHierarchy(nw)).toEqual([7.0, 8.0]); - - var ne = tile.northeastChild; - makeLoading(ne); - expect(ne.data.getBoundingVolumeHierarchy(ne)).toEqual([9.0, 10.0]); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveBvh(new Float32Array([1.0, 10.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]), rootTile) + .requestTileGeometryWillSucceed(sw) + .requestTileGeometryWillFail(nw) + .requestTileGeometryWillSucceed(ne) + .requestTileGeometryWillFail(se); + + return processTiles([rootTile, sw, nw, se, ne]).then(function() { + expect(sw.data.getBoundingVolumeHierarchy(sw)).toEqual([3.0, 4.0]); + expect(se.data.getBoundingVolumeHierarchy(se)).toEqual([5.0, 6.0]); + expect(nw.data.getBoundingVolumeHierarchy(nw)).toEqual([7.0, 8.0]); + expect(ne.data.getBoundingVolumeHierarchy(ne)).toEqual([9.0, 10.0]); + }); }); it('returns undefined if the parent BVH does not extend to this tile', function() { diff --git a/Specs/createTileKey.js b/Specs/createTileKey.js index c5207d56a20..f20dfaa944a 100644 --- a/Specs/createTileKey.js +++ b/Specs/createTileKey.js @@ -1,7 +1,16 @@ -define([], function() { +define([ + 'Core/defined', + 'Core/DeveloperError' +], function( + defined, + DeveloperError) { 'use strict'; function createTileKey(xOrTile, y, level) { + if (!defined(xOrTile)) { + throw new DeveloperError('xOrTile is required'); + } + if (typeof xOrTile === 'object') { var tile = xOrTile; xOrTile = tile.x; From 9e07b73e0f6d6f0bef7573bb63619f7b8840c16d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 17 Dec 2018 18:44:36 +1100 Subject: [PATCH 070/131] Update the rest of the BVH tests. --- Specs/Scene/GlobeSurfaceTileSpec.js | 35 +++++++++++------------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 7c19a25445e..8a557e80603 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -534,33 +534,24 @@ defineSuite([ }); it('returns undefined if the parent BVH does not extend to this tile', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - makeTerrainReceived(tile); - tile.data.terrainData.bvh = new Float32Array([1.0, 10.0]); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .willHaveBvh(new Float32Array([1.0, 10.0]), rootTile) + .requestTileGeometryWillSucceed(rootTile.southwestChild); - var sw = tile.southwestChild; - makeLoading(sw); - expect(sw.data.getBoundingVolumeHierarchy(sw)).toBeUndefined(); + return processTiles([rootTile, rootTile.southwestChild]).then(function() { + expect(rootTile.southwestChild.data.getBoundingVolumeHierarchy(rootTile.southwestChild)).toBeUndefined(); + }); }); it('returns undefined if the parent does not have a BVH', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - makeTerrainReceived(tile); - tile.data.terrainData.bvh = undefined; + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .requestTileGeometryWillSucceed(rootTile.southwestChild); - var sw = tile.southwestChild; - makeLoading(sw); - expect(sw.data.getBoundingVolumeHierarchy(sw)).toBeUndefined(); + return processTiles([rootTile, rootTile.southwestChild]).then(function() { + expect(rootTile.southwestChild.data.getBoundingVolumeHierarchy(rootTile.southwestChild)).toBeUndefined(); + }); }); }); From 57cb725b294ebaf9b14cb64f6f0e2adabd211000 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 18 Dec 2018 14:33:04 +1100 Subject: [PATCH 071/131] Last of the GlobeSurfaceTile specs. --- Specs/MockImageryProvider.js | 14 +- Specs/MockTerrainProvider.js | 80 +++++++---- Specs/Scene/GlobeSurfaceTileSpec.js | 214 +++++++++++----------------- 3 files changed, 146 insertions(+), 162 deletions(-) diff --git a/Specs/MockImageryProvider.js b/Specs/MockImageryProvider.js index 45e1bc6b837..731c8d17edc 100644 --- a/Specs/MockImageryProvider.js +++ b/Specs/MockImageryProvider.js @@ -39,10 +39,15 @@ define([ var that = this; return runLater(function() { - if (willSucceed) { + if (willSucceed === true) { return that._image; + } else if (willSucceed === false) { + throw new RuntimeError('requestImage failed as request.'); } - throw new RuntimeError('requestImage failed as request.'); + + return when(willSucceed).then(function() { + return that._image; + }); }); }; @@ -61,5 +66,10 @@ define([ return this; }; + MockImageryProvider.prototype.requestImageWillWaitOn = function(promise, xOrTile, y, level) { + this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = promise; + return this; + }; + return MockImageryProvider; }); diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js index 70879bf13ff..d371fe37df3 100644 --- a/Specs/MockTerrainProvider.js +++ b/Specs/MockTerrainProvider.js @@ -3,6 +3,7 @@ define([ 'Core/GeographicTilingScheme', 'Core/RuntimeError', 'Core/TerrainProvider', + 'ThirdParty/when', './createTileKey', './runLater' ], function( @@ -10,6 +11,7 @@ define([ GeographicTilingScheme, RuntimeError, TerrainProvider, + when, createTileKey, runLater) { 'use strict'; @@ -38,34 +40,15 @@ define([ var that = this; return runLater(function() { - if (willSucceed) { - var terrainData = createTerrainData(that, x, y, level, false); - - var willHaveWaterMask = that._willHaveWaterMask[createTileKey(x, y, level)]; - if (defined(willHaveWaterMask)) { - if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { - terrainData.waterMask = new Uint8Array(4); - terrainData.waterMask[0] = 1; - terrainData.waterMask[1] = 1; - terrainData.waterMask[2] = 0; - terrainData.waterMask[3] = 0; - } else if (willHaveWaterMask.includeLand) { - terrainData.waterMask = new Uint8Array(1); - terrainData.waterMask[0] = 0; - } else if (willHaveWaterMask.includeWater) { - terrainData.waterMask = new Uint8Array(1); - terrainData.waterMask[0] = 1; - } - } - - var willHaveBvh = that._willHaveBvh[createTileKey(x, y, level)]; - if (defined(willHaveBvh)) { - terrainData.bvh = willHaveBvh; - } - - return terrainData; + if (willSucceed === true) { + return createTerrainData(that, x, y, level, false); + } else if (willSucceed === false) { + throw new RuntimeError('requestTileGeometry failed as requested.'); } - throw new RuntimeError('requestTileGeometry failed as requested.'); + + return when(willSucceed).then(function() { + return createTerrainData(that, x, y, level, false); + }); }); }; @@ -96,6 +79,11 @@ define([ return this; }; + MockTerrainProvider.prototype.requestTileGeometryWillWaitOn = function(promise, xOrTile, y, level) { + this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = promise; + return this; + }; + MockTerrainProvider.prototype.willHaveWaterMask = function(includeLand, includeWater, xOrTile, y, level) { this._willHaveWaterMask[createTileKey(xOrTile, y, level)] = includeLand || includeWater ? { includeLand: includeLand, @@ -124,6 +112,11 @@ define([ return this; }; + MockTerrainProvider.prototype.createMeshWillWaitOn = function(promise, xOrTile, y, level) { + this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = promise; + return this; + }; + MockTerrainProvider.prototype.upsampleWillSucceed = function(xOrTile, y, level) { this._upsampleWillSucceed[createTileKey(xOrTile, y, level)] = true; return this; @@ -163,6 +156,30 @@ define([ var terrainData = jasmine.createSpyObj('MockTerrainData', ['createMesh', 'upsample', 'wasCreatedByUpsampling']); terrainData.wasCreatedByUpsampling.and.returnValue(upsampled); + if (!upsampled) { + var willHaveWaterMask = terrainProvider._willHaveWaterMask[createTileKey(x, y, level)]; + if (defined(willHaveWaterMask)) { + if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { + terrainData.waterMask = new Uint8Array(4); + terrainData.waterMask[0] = 1; + terrainData.waterMask[1] = 1; + terrainData.waterMask[2] = 0; + terrainData.waterMask[3] = 0; + } else if (willHaveWaterMask.includeLand) { + terrainData.waterMask = new Uint8Array(1); + terrainData.waterMask[0] = 0; + } else if (willHaveWaterMask.includeWater) { + terrainData.waterMask = new Uint8Array(1); + terrainData.waterMask[0] = 1; + } + } + + var willHaveBvh = terrainProvider._willHaveBvh[createTileKey(x, y, level)]; + if (defined(willHaveBvh)) { + terrainData.bvh = willHaveBvh; + } + } + terrainData.createMesh.and.callFake(function(tilingScheme, x, y, level) { var willSucceed = terrainProvider._createMeshWillSucceed[createTileKey(x, y, level)]; if (willSucceed === undefined) { @@ -170,10 +187,15 @@ define([ } return runLater(function() { - if (willSucceed) { + if (willSucceed === true) { return {}; + } else if (willSucceed === false) { + throw new RuntimeError('createMesh failed as requested.'); } - throw new RuntimeError('createMesh failed as requested.'); + + return when(willSucceed).then(function() { + return {}; + }); }); }); diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 8a557e80603..bade869e2c7 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -5,6 +5,7 @@ defineSuite([ 'Core/CesiumTerrainProvider', 'Core/clone', 'Core/createWorldTerrain', + 'Core/defaultValue', 'Core/defined', 'Core/Ellipsoid', 'Core/GeographicTilingScheme', @@ -29,7 +30,8 @@ defineSuite([ 'Specs/pollToPromise', 'ThirdParty/when', '../MockImageryProvider', - '../MockTerrainProvider' + '../MockTerrainProvider', + '../runLater' ], function( GlobeSurfaceTile, Cartesian3, @@ -37,6 +39,7 @@ defineSuite([ CesiumTerrainProvider, clone, createWorldTerrain, + defaultValue, defined, Ellipsoid, GeographicTilingScheme, @@ -61,7 +64,8 @@ defineSuite([ pollToPromise, when, MockImageryProvider, - MockTerrainProvider) { + MockTerrainProvider, + runLater) { 'use strict'; var frameState; @@ -94,7 +98,11 @@ defineSuite([ } // Processes the given list of tiles until all terrain and imagery states stop changing. - function processTiles(tiles) { + function processTiles(tiles, maxIterations, overrideFrameState, overrideTerrainProvider, overrideImageryLayerCollection) { + overrideFrameState = defaultValue(overrideFrameState, frameState); + overrideTerrainProvider = defaultValue(overrideTerrainProvider, mockTerrain); + overrideImageryLayerCollection = defaultValue(overrideImageryLayerCollection, imageryLayerCollection); + var deferred = when.defer(); function getState(tile) { @@ -129,25 +137,35 @@ defineSuite([ return same; } + var iterations = 0; + function next() { + ++iterations; + // Keep going until all terrain and imagery provider are ready and states are no longer changing. - var changed = !mockTerrain.ready; + var changed = !overrideTerrainProvider.ready; - for (var i = 0; i < imageryLayerCollection.length; ++i) { - changed = changed || !imageryLayerCollection.get(i).imageryProvider.ready; + for (var i = 0; i < overrideImageryLayerCollection.length; ++i) { + changed = changed || !overrideImageryLayerCollection.get(i).imageryProvider.ready; } - tiles.forEach(function(tile) { - var beforeState = getState(tile); - GlobeSurfaceTile.processStateMachine(tile, frameState, mockTerrain, imageryLayerCollection); - var afterState = getState(tile); - changed = changed || !statesAreSame(beforeState, afterState); - }); + if (overrideTerrainProvider.ready) { + tiles.forEach(function(tile) { + var beforeState = getState(tile); + GlobeSurfaceTile.processStateMachine(tile, overrideFrameState, overrideTerrainProvider, overrideImageryLayerCollection); + var afterState = getState(tile); + changed = + changed || + tile.data.terrainState === TerrainState.RECEIVING || + tile.data.terrainState === TerrainState.TRANSFORMING || + !statesAreSame(beforeState, afterState); + }); + } - if (changed) { - setTimeout(next, 0); + if (!changed || iterations >= maxIterations) { + deferred.resolve(iterations); } else { - deferred.resolve(); + setTimeout(next, 0); } } @@ -579,23 +597,11 @@ defineSuite([ y : 1336 }); - var imageryLayerCollection = new ImageryLayerCollection(); - - makeLoading(tile); - - return pollToPromise(function() { - if (!terrainProvider.ready) { - return false; - } - - GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, imageryLayerCollection); - RequestScheduler.update(); - return tile.state === QuadtreeTileLoadState.DONE; - }).then(function() { + return processTiles([tile], undefined, scene.frameState, terrainProvider, new ImageryLayerCollection()).then(function() { var ray = new Ray( new Cartesian3(-5052039.459789615, 2561172.040315167, -2936276.999965875), new Cartesian3(0.5036332963145244, 0.6648033332898124, 0.5517155343926082)); - var pickResult = tile.data.pick(ray, undefined, true); + var pickResult = tile.data.pick(ray, undefined, undefined, true); var cartographic = Ellipsoid.WGS84.cartesianToCartographic(pickResult); expect(cartographic.height).toBeGreaterThan(-500.0); }); @@ -603,126 +609,72 @@ defineSuite([ }, 'WebGL'); describe('eligibleForUnloading', function() { + beforeEach(function() { + mockWebGL(); + }); + it('returns true when no loading has been done', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - makeLoading(tile); - expect(tile.data.eligibleForUnloading).toBe(true); + rootTile.data = new GlobeSurfaceTile(); + expect(rootTile.data.eligibleForUnloading).toBe(true); }); it('returns true when some loading has been done', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - - makeTerrainReceived(tile); - expect(tile.data.eligibleForUnloading).toBe(true); - - makeTerrainTransformed(tile); - expect(tile.data.eligibleForUnloading).toBe(true); - - makeTerrainReady(tile); - expect(tile.data.eligibleForUnloading).toBe(true); - }); + mockTerrain + .requestTileGeometryWillSucceed(rootTile); - it('returns false when RECEIVING or TRANSITIONING', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() + return processTiles([rootTile]).then(function() { + expect(rootTile.data.eligibleForUnloading).toBe(true); + mockTerrain + .createMeshWillSucceed(rootTile); + return processTiles([rootTile]); + }).then(function() { + expect(rootTile.data.eligibleForUnloading).toBe(true); }); - - makeTerrainUnloaded(tile); - expect(tile.data.eligibleForUnloading).toBe(true); - - tile.data.terrainState = TerrainState.RECEIVING; - expect(tile.data.eligibleForUnloading).toBe(false); - - tile.data.terrainState = TerrainState.TRANSFORMING; - expect(tile.data.eligibleForUnloading).toBe(false); }); - it('returns false when imagery is TRANSITIONING, true otherwise', function() { - var tile = new QuadtreeTile({ - level: 0, - x: 0, - y: 0, - tilingScheme: new GeographicTilingScheme() - }); - makeTerrainReady(tile); - expect(tile.data.eligibleForUnloading).toBe(true); + it('returns false when RECEIVING', function() { + var deferred = when.defer(); - tile.data.imagery.push({ - loadingImagery : { - state : undefined - } - }); + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .requestTileGeometryWillWaitOn(deferred.promise, rootTile); - Object.keys(ImageryState).forEach(function(state) { - tile.data.imagery[0].loadingImagery.state = state; - if (state === ImageryState.TRANSITIONING) { - expect(tile.data.eligibleForUnloading).toBe(false); - } else { - expect(tile.data.eligibleForUnloading).toBe(true); - } + return processTiles([rootTile], 5).then(function() { + expect(rootTile.data.eligibleForUnloading).toBe(false); + deferred.resolve(); }); }); - }); - function makeLoading(tile) { - if (!defined(tile.data)) { - tile.data = new GlobeSurfaceTile(); - } - tile.state = QuadtreeTileLoadState.LOADING; - } - - function makeTerrainUnloaded(tile) { - makeLoading(tile); - tile.data.terrainState = TerrainState.UNLOADED; - tile.data.terrainData = undefined; - tile.data.vertexArray = undefined; - tile.data.mesh = undefined; - } + it ('returns false when TRANSFORMING', function() { + var deferred = when.defer(); - function makeTerrainReceived(tile) { - makeLoading(tile); - tile.data.terrainState = TerrainState.RECEIVED; - tile.data.terrainData = jasmine.createSpyObj('TerrainData', ['createMesh', 'upsample', 'wasCreatedByUpsampling']); - tile.data.terrainData.wasCreatedByUpsampling.and.returnValue(false); - - tile.data.mesh = undefined; - tile.data.vertexArray = undefined; - } + mockTerrain + .requestTileGeometryWillSucceed(rootTile) + .createMeshWillSucceed(rootTile) + .createMeshWillWaitOn(deferred.promise, rootTile); - function makeTerrainTransformed(tile) { - makeLoading(tile); + return processTiles([rootTile], 5).then(function() { + expect(rootTile.data.eligibleForUnloading).toBe(false); + deferred.resolve(); + }); + }); - if (!defined(tile.data.terrainData)) { - makeTerrainReceived(tile); - } + it('returns false when imagery is TRANSITIONING', function() { + var deferred = when.defer(); - tile.data.terrainState = TerrainState.TRANSFORMED; - tile.data.mesh = {}; - } + var mockImagery = new MockImageryProvider(); + imageryLayerCollection.addImageryProvider(mockImagery); - function makeTerrainReady(tile) { - makeLoading(tile); + mockImagery + .requestImageWillWaitOn(deferred.promise, rootTile); - if (!defined(tile.data.mesh)) { - makeTerrainTransformed(tile); - } + mockTerrain + .requestTileGeometryWillSucceed(rootTile); - tile.data.terrainState = TerrainState.DONE; - tile.data.vertexArray = jasmine.createSpyObj('VertexArray', ['destroy', 'isDestroyed']); - tile.data.vertexArray.isDestroyed.and.returnValue(false); - tile.data.fill = tile.data.fill && tile.data.fill.destroy(); - } + return processTiles([rootTile], 5).then(function() { + expect(rootTile.data.eligibleForUnloading).toBe(false); + deferred.resolve(); + }); + }); + }); }); From 03da694ce01af2c77a0be463d24ae9812aaf3f4b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 18 Dec 2018 14:46:07 +1100 Subject: [PATCH 072/131] Remove unused requires. --- Specs/MockImageryProvider.js | 2 -- Specs/Scene/GlobeSurfaceTileSpec.js | 32 ++--------------------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/Specs/MockImageryProvider.js b/Specs/MockImageryProvider.js index 731c8d17edc..6afa3e142aa 100644 --- a/Specs/MockImageryProvider.js +++ b/Specs/MockImageryProvider.js @@ -1,5 +1,4 @@ define([ - 'Core/defaultValue', 'Core/GeographicTilingScheme', 'Core/Resource', 'Core/RuntimeError', @@ -7,7 +6,6 @@ define([ './createTileKey', './runLater' ], function( - defaultValue, GeographicTilingScheme, Resource, RuntimeError, diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index bade869e2c7..dcba9412759 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -2,70 +2,42 @@ defineSuite([ 'Scene/GlobeSurfaceTile', 'Core/Cartesian3', 'Core/Cartesian4', - 'Core/CesiumTerrainProvider', 'Core/clone', 'Core/createWorldTerrain', 'Core/defaultValue', - 'Core/defined', 'Core/Ellipsoid', 'Core/GeographicTilingScheme', - 'Core/HeightmapTerrainData', 'Core/Ray', - 'Core/Rectangle', - 'Core/RequestScheduler', - 'Core/TerrainEncoding', - 'Core/TerrainMesh', - 'Core/TileAvailability', - 'Renderer/Buffer', 'Renderer/Texture', - 'Scene/Imagery', 'Scene/ImageryLayer', 'Scene/ImageryLayerCollection', - 'Scene/ImageryState', 'Scene/QuadtreeTile', 'Scene/QuadtreeTileLoadState', 'Scene/TerrainState', - 'Scene/TileImagery', 'Specs/createScene', - 'Specs/pollToPromise', 'ThirdParty/when', '../MockImageryProvider', - '../MockTerrainProvider', - '../runLater' + '../MockTerrainProvider' ], function( GlobeSurfaceTile, Cartesian3, Cartesian4, - CesiumTerrainProvider, clone, createWorldTerrain, defaultValue, - defined, Ellipsoid, GeographicTilingScheme, - HeightmapTerrainData, Ray, - Rectangle, - RequestScheduler, - TerrainEncoding, - TerrainMesh, - TileAvailability, - Buffer, Texture, - Imagery, ImageryLayer, ImageryLayerCollection, - ImageryState, QuadtreeTile, QuadtreeTileLoadState, TerrainState, - TileImagery, createScene, - pollToPromise, when, MockImageryProvider, - MockTerrainProvider, - runLater) { + MockTerrainProvider) { 'use strict'; var frameState; From a816f65c5eac392bad4bb87a509a3e30fe45520f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 18 Dec 2018 17:39:04 +1100 Subject: [PATCH 073/131] Break out processTiles. --- Specs/Scene/GlobeSurfaceTileSpec.js | 157 ++++++++-------------------- Specs/TerrainTileProcessor.js | 94 +++++++++++++++++ 2 files changed, 137 insertions(+), 114 deletions(-) create mode 100644 Specs/TerrainTileProcessor.js diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index dcba9412759..8de54ea3652 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -4,7 +4,6 @@ defineSuite([ 'Core/Cartesian4', 'Core/clone', 'Core/createWorldTerrain', - 'Core/defaultValue', 'Core/Ellipsoid', 'Core/GeographicTilingScheme', 'Core/Ray', @@ -17,14 +16,14 @@ defineSuite([ 'Specs/createScene', 'ThirdParty/when', '../MockImageryProvider', - '../MockTerrainProvider' + '../MockTerrainProvider', + '../TerrainTileProcessor' ], function( GlobeSurfaceTile, Cartesian3, Cartesian4, clone, createWorldTerrain, - defaultValue, Ellipsoid, GeographicTilingScheme, Ray, @@ -37,7 +36,8 @@ defineSuite([ createScene, when, MockImageryProvider, - MockTerrainProvider) { + MockTerrainProvider, + TerrainTileProcessor) { 'use strict'; var frameState; @@ -46,6 +46,7 @@ defineSuite([ var rootTile; var imageryLayerCollection; var mockTerrain; + var processor; function mockWebGL() { spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() { @@ -69,83 +70,6 @@ defineSuite([ }); } - // Processes the given list of tiles until all terrain and imagery states stop changing. - function processTiles(tiles, maxIterations, overrideFrameState, overrideTerrainProvider, overrideImageryLayerCollection) { - overrideFrameState = defaultValue(overrideFrameState, frameState); - overrideTerrainProvider = defaultValue(overrideTerrainProvider, mockTerrain); - overrideImageryLayerCollection = defaultValue(overrideImageryLayerCollection, imageryLayerCollection); - - var deferred = when.defer(); - - function getState(tile) { - return [ - tile.state, - tile.data ? tile.data.terrainState : undefined, - tile.data && tile.data.imagery ? tile.data.imagery.map(function(imagery) { - return [ - imagery.readyImagery ? imagery.readyImagery.state : undefined, - imagery.loadingImagery ? imagery.loadingImagery.state : undefined - ]; - }) : [] - ]; - } - - function statesAreSame(a, b) { - if (a.length !== b.length) { - return false; - } - - var same = true; - for (var i = 0; i < a.length; ++i) { - if (Array.isArray(a[i]) && Array.isArray(b[i])) { - same = same && statesAreSame(a[i], b[i]); - } else if (Array.isArray(a[i]) || Array.isArray(b[i])) { - same = false; - } else { - same = same && a[i] === b[i]; - } - } - - return same; - } - - var iterations = 0; - - function next() { - ++iterations; - - // Keep going until all terrain and imagery provider are ready and states are no longer changing. - var changed = !overrideTerrainProvider.ready; - - for (var i = 0; i < overrideImageryLayerCollection.length; ++i) { - changed = changed || !overrideImageryLayerCollection.get(i).imageryProvider.ready; - } - - if (overrideTerrainProvider.ready) { - tiles.forEach(function(tile) { - var beforeState = getState(tile); - GlobeSurfaceTile.processStateMachine(tile, overrideFrameState, overrideTerrainProvider, overrideImageryLayerCollection); - var afterState = getState(tile); - changed = - changed || - tile.data.terrainState === TerrainState.RECEIVING || - tile.data.terrainState === TerrainState.TRANSFORMING || - !statesAreSame(beforeState, afterState); - }); - } - - if (!changed || iterations >= maxIterations) { - deferred.resolve(iterations); - } else { - setTimeout(next, 0); - } - } - - next(); - - return deferred.promise; - } - beforeEach(function() { frameState = { context: { @@ -159,6 +83,8 @@ defineSuite([ imageryLayerCollection = new ImageryLayerCollection(); mockTerrain = new MockTerrainProvider(); + + processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection); }); afterEach(function() { @@ -183,7 +109,7 @@ defineSuite([ mockTerrain .willBeAvailable(rootTile.southwestChild); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING); expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.UNLOADED); }); @@ -193,7 +119,7 @@ defineSuite([ mockTerrain .willBeUnavailable(rootTile.southwestChild); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING); expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.FAILED); }); @@ -207,7 +133,7 @@ defineSuite([ spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); @@ -219,7 +145,7 @@ defineSuite([ mockTerrain .willBeUnavailable(rootTile); - return processTiles([rootTile]).then(function() { + return processor.process([rootTile]).then(function() { expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); expect(rootTile.data.terrainState).toBe(TerrainState.FAILED); }); @@ -229,7 +155,7 @@ defineSuite([ mockTerrain .requestTileGeometryWillFail(rootTile); - return processTiles([rootTile]).then(function() { + return processor.process([rootTile]).then(function() { expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED); expect(rootTile.data.terrainState).toBe(TerrainState.FAILED); }); @@ -241,7 +167,7 @@ defineSuite([ .willBeUnavailable(rootTile.southwestChild) .upsampleWillSucceed(rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.data.terrainState).toBe(TerrainState.RECEIVED); expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.RECEIVED); expect(rootTile.data.terrainData.wasCreatedByUpsampling()).toBe(false); @@ -256,7 +182,7 @@ defineSuite([ spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); @@ -269,14 +195,14 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .willHaveNearestBvhLevel(0, rootTile.southwestChild); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { // Indicate that the SW tile's bounding volume comes from the root. rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; // Monitor calls to requestTileGeometry - we should only see one for the root tile now. spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - return processTiles([rootTile.southwestChild]); + return processor.process([rootTile.southwestChild]); }).then(function() { expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); @@ -290,14 +216,14 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .willHaveNearestBvhLevel(-1, rootTile.southwestChild); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { // Indicate that the SW tile's bounding volume comes from the root. rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; // Monitor calls to requestTileGeometry - we should only see one for the southwest tile now. spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - return processTiles([rootTile.southwestChild]); + return processor.process([rootTile.southwestChild]); }).then(function() { expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); @@ -312,14 +238,14 @@ defineSuite([ mockTerrain .requestTileGeometryWillSucceed(rootTile); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { // Indicate that the SW tile's bounding volume comes from the root. rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; // Monitor calls to requestTileGeometry - we should only see one for the southwest tile now. spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - return processTiles([rootTile.southwestChild]); + return processor.process([rootTile.southwestChild]); }).then(function() { expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); @@ -339,7 +265,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .willHaveNearestBvhLevel(0, rootTile.southwestChild); - return processTiles([rootTile.southwestChild]).then(function() { + return processor.process([rootTile.southwestChild]).then(function() { // Indicate that the SW tile's bounding volume comes from the root. rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; @@ -348,7 +274,7 @@ defineSuite([ spyOn(mockImagery, 'requestImage').and.callThrough(); spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - return processTiles([rootTile.southwestChild]); + return processor.process([rootTile.southwestChild]); }).then(function() { expect(mockImagery.requestImage.calls.count()).toBe(0); expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); @@ -374,7 +300,7 @@ defineSuite([ .requestImageWillSucceed(rootTile) .requestImageWillFail(rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE); expect(rootTile.upsampledFromParent).toBe(false); expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE); @@ -398,7 +324,7 @@ defineSuite([ .requestImageWillSucceed(rootTile) .requestImageWillSucceed(rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE); expect(rootTile.upsampledFromParent).toBe(false); expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE); @@ -422,7 +348,7 @@ defineSuite([ .requestImageWillSucceed(rootTile) .requestImageWillFail(rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE); expect(rootTile.upsampledFromParent).toBe(false); expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE); @@ -436,7 +362,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .willHaveWaterMask(false, true, rootTile); - return processTiles([rootTile]).then(function() { + return processor.process([rootTile]).then(function() { expect(rootTile.data.waterMaskTexture).toBeDefined(); }); }); @@ -446,7 +372,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .willHaveWaterMask(true, false, rootTile); - return processTiles([rootTile]).then(function() { + return processor.process([rootTile]).then(function() { expect(rootTile.data.waterMaskTexture).toBeUndefined(); }); }); @@ -458,7 +384,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile.southwestChild) .willHaveWaterMask(false, true, rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.data.waterMaskTexture).toBe(rootTile.southwestChild.data.waterMaskTexture); }); }); @@ -468,7 +394,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .willHaveWaterMask(true, true, rootTile); - return processTiles([rootTile]).then(function() { + return processor.process([rootTile]).then(function() { expect(rootTile.data.waterMaskTexture).toBeDefined(); }); }); @@ -479,7 +405,7 @@ defineSuite([ .willHaveWaterMask(false, true, rootTile) .requestTileGeometryWillSucceed(rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.southwestChild.data.waterMaskTexture).toBeDefined(); expect(rootTile.southwestChild.data.waterMaskTranslationAndScale).toEqual(new Cartesian4(0.0, 0.0, 0.5, 0.5)); }); @@ -496,7 +422,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .willHaveBvh(new Float32Array([1.0, 2.0]), rootTile); - return processTiles([rootTile]).then(function() { + return processor.process([rootTile]).then(function() { expect(rootTile.data.getBoundingVolumeHierarchy(rootTile)).toEqual([1.0, 2.0]); }); }); @@ -515,7 +441,7 @@ defineSuite([ .requestTileGeometryWillSucceed(ne) .requestTileGeometryWillFail(se); - return processTiles([rootTile, sw, nw, se, ne]).then(function() { + return processor.process([rootTile, sw, nw, se, ne]).then(function() { expect(sw.data.getBoundingVolumeHierarchy(sw)).toEqual([3.0, 4.0]); expect(se.data.getBoundingVolumeHierarchy(se)).toEqual([5.0, 6.0]); expect(nw.data.getBoundingVolumeHierarchy(nw)).toEqual([7.0, 8.0]); @@ -529,7 +455,7 @@ defineSuite([ .willHaveBvh(new Float32Array([1.0, 10.0]), rootTile) .requestTileGeometryWillSucceed(rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.southwestChild.data.getBoundingVolumeHierarchy(rootTile.southwestChild)).toBeUndefined(); }); }); @@ -539,7 +465,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .requestTileGeometryWillSucceed(rootTile.southwestChild); - return processTiles([rootTile, rootTile.southwestChild]).then(function() { + return processor.process([rootTile, rootTile.southwestChild]).then(function() { expect(rootTile.southwestChild.data.getBoundingVolumeHierarchy(rootTile.southwestChild)).toBeUndefined(); }); }); @@ -569,7 +495,10 @@ defineSuite([ y : 1336 }); - return processTiles([tile], undefined, scene.frameState, terrainProvider, new ImageryLayerCollection()).then(function() { + processor.frameState = scene.frameState; + processor.terrainProvider = terrainProvider; + + return processor.process([tile]).then(function() { var ray = new Ray( new Cartesian3(-5052039.459789615, 2561172.040315167, -2936276.999965875), new Cartesian3(0.5036332963145244, 0.6648033332898124, 0.5517155343926082)); @@ -594,11 +523,11 @@ defineSuite([ mockTerrain .requestTileGeometryWillSucceed(rootTile); - return processTiles([rootTile]).then(function() { + return processor.process([rootTile]).then(function() { expect(rootTile.data.eligibleForUnloading).toBe(true); mockTerrain .createMeshWillSucceed(rootTile); - return processTiles([rootTile]); + return processor.process([rootTile]); }).then(function() { expect(rootTile.data.eligibleForUnloading).toBe(true); }); @@ -611,7 +540,7 @@ defineSuite([ .requestTileGeometryWillSucceed(rootTile) .requestTileGeometryWillWaitOn(deferred.promise, rootTile); - return processTiles([rootTile], 5).then(function() { + return processor.process([rootTile], 5).then(function() { expect(rootTile.data.eligibleForUnloading).toBe(false); deferred.resolve(); }); @@ -625,7 +554,7 @@ defineSuite([ .createMeshWillSucceed(rootTile) .createMeshWillWaitOn(deferred.promise, rootTile); - return processTiles([rootTile], 5).then(function() { + return processor.process([rootTile], 5).then(function() { expect(rootTile.data.eligibleForUnloading).toBe(false); deferred.resolve(); }); @@ -643,7 +572,7 @@ defineSuite([ mockTerrain .requestTileGeometryWillSucceed(rootTile); - return processTiles([rootTile], 5).then(function() { + return processor.process([rootTile], 5).then(function() { expect(rootTile.data.eligibleForUnloading).toBe(false); deferred.resolve(); }); diff --git a/Specs/TerrainTileProcessor.js b/Specs/TerrainTileProcessor.js new file mode 100644 index 00000000000..157a6a0d434 --- /dev/null +++ b/Specs/TerrainTileProcessor.js @@ -0,0 +1,94 @@ +define([ + 'Scene/GlobeSurfaceTile', + 'Scene/TerrainState', + 'ThirdParty/when' +], function( + GlobeSurfaceTile, + TerrainState, + when) { + 'use strict'; + + function TerrainTileProcessor(frameState, terrainProvider, imageryLayerCollection) { + this.frameState = frameState; + this.terrainProvider = terrainProvider; + this.imageryLayerCollection = imageryLayerCollection; + } + + // Processes the given list of tiles until all terrain and imagery states stop changing. + TerrainTileProcessor.prototype.process = function(tiles, maxIterations) { + var that = this; + + var deferred = when.defer(); + + function getState(tile) { + return [ + tile.state, + tile.data ? tile.data.terrainState : undefined, + tile.data && tile.data.imagery ? tile.data.imagery.map(function(imagery) { + return [ + imagery.readyImagery ? imagery.readyImagery.state : undefined, + imagery.loadingImagery ? imagery.loadingImagery.state : undefined + ]; + }) : [] + ]; + } + + function statesAreSame(a, b) { + if (a.length !== b.length) { + return false; + } + + var same = true; + for (var i = 0; i < a.length; ++i) { + if (Array.isArray(a[i]) && Array.isArray(b[i])) { + same = same && statesAreSame(a[i], b[i]); + } else if (Array.isArray(a[i]) || Array.isArray(b[i])) { + same = false; + } else { + same = same && a[i] === b[i]; + } + } + + return same; + } + + var iterations = 0; + + function next() { + ++iterations; + + // Keep going until all terrain and imagery provider are ready and states are no longer changing. + var changed = !that.terrainProvider.ready; + + for (var i = 0; i < that.imageryLayerCollection.length; ++i) { + changed = changed || !that.imageryLayerCollection.get(i).imageryProvider.ready; + } + + if (that.terrainProvider.ready) { + tiles.forEach(function(tile) { + var beforeState = getState(tile); + GlobeSurfaceTile.processStateMachine(tile, that.frameState, that.terrainProvider, that.imageryLayerCollection); + var afterState = getState(tile); + changed = + changed || + tile.data.terrainState === TerrainState.RECEIVING || + tile.data.terrainState === TerrainState.TRANSFORMING || + !statesAreSame(beforeState, afterState); + }); + } + + if (!changed || iterations >= maxIterations) { + deferred.resolve(iterations); + } else { + setTimeout(next, 0); + } + } + + next(); + + return deferred.promise; + }; + + return TerrainTileProcessor; +}); + From 686a9f5e1f20dbf539679e8635536546e07f8f50 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Jan 2019 16:51:40 +1100 Subject: [PATCH 074/131] selectTilesForRendering tests, etc. --- Source/Core/HeightmapTerrainData.js | 18 +- Source/Scene/GlobeSurfaceTile.js | 10 +- Source/Scene/GlobeSurfaceTileProvider.js | 51 +- Source/Scene/QuadtreePrimitive.js | 16 +- Specs/MockTerrainProvider.js | 108 ++- Specs/Scene/GlobeSurfaceTileSpec.js | 31 +- Specs/Scene/QuadtreePrimitiveSpec.js | 1048 +++++++++++++--------- Specs/TerrainTileProcessor.js | 28 + Specs/createTileKey.js | 2 +- 9 files changed, 805 insertions(+), 507 deletions(-) diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index 572ae3609af..2456a05c733 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -50,6 +50,10 @@ define([ * 24Northwest * 38Northeast * + * @param {Uint8Array} [options.waterMask] The water mask included in this terrain data, if any. A water mask is a square + * Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land. + * Values in between 0 and 255 are allowed as well to smoothly blend between land and water. + * @param {Float32Array} [options.bvh] The bounding-volume hierarchy for this tile and its descendents. TODO: describe its structure * @param {Object} [options.structure] An object describing the structure of the height data. * @param {Number} [options.structure.heightScale=1.0] The factor by which to multiply height samples in order to obtain * the height above the heightOffset, in meters. The heightOffset is added to the resulting @@ -117,6 +121,7 @@ define([ this._width = options.width; this._height = options.height; this._childTileMask = defaultValue(options.childTileMask, 15); + this._bvh = options.bvh; var defaultStructure = HeightmapTessellator.DEFAULT_STRUCTURE; var structure = options.structure; @@ -152,7 +157,7 @@ define([ } }, /** - * The water mask included in this terrain data, if any. A water mask is a rectangular + * The water mask included in this terrain data, if any. A water mask is a square * Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land. * Values in between 0 and 255 are allowed as well to smoothly blend between land and water. * @memberof HeightmapTerrainData.prototype @@ -168,6 +173,17 @@ define([ get : function() { return this._childTileMask; } + }, + + /** + * Gets the bounding-volume hierarchy (BVH) starting with this tile. + * @memberof HeightmapTerrainData.prototype + * @type {Float32Array} + */ + bvh : { + get : function() { + return this._bvh; + } } }); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index b0e70293584..311b8a63900 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -7,6 +7,7 @@ define([ '../Core/DeveloperError', '../Core/IndexDatatype', '../Core/IntersectionTests', + '../Core/OrientedBoundingBox', '../Core/PixelFormat', '../Core/Request', '../Core/RequestState', @@ -36,6 +37,7 @@ define([ DeveloperError, IndexDatatype, IntersectionTests, + OrientedBoundingBox, PixelFormat, Request, RequestState, @@ -297,12 +299,12 @@ define([ } // From here down we're loading imagery, not terrain. We don't want to load imagery until - // we're certain that the terrain tiles are actually visible, though. So if our bounding - // volume isn't accurate, stop here. Also stop here if we're explicitly loading terrain - // only, which happens in these two scenarios: + // we're certain that the terrain tiles are actually visible, though. We'll load terrainOnly + // in these scenarios: + // * our bounding volume isn't accurate so we're not certain this tile is really visible (see GlobeSurfaceTileProvider#loadTile). // * we want ancestor BVH data from this tile but don't plan to render it (see code above). // * we want to upsample from this tile but don't plan to render it (see processTerrainStateMachine). - if (terrainOnly || (tile.level !== 0 && defined(surfaceTile.boundingVolumeSourceTile) && surfaceTile.boundingVolumeSourceTile !== tile)) { + if (terrainOnly) { return; } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 5af77ef96e1..e6e20fb41af 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -153,9 +153,9 @@ define([ */ this.fillHighlightColor = undefined; - this.hueShift = undefined; - this.saturationShift = undefined; - this.brightnessShift = undefined; + this.hueShift = 0.0; + this.saturationShift = 0.0; + this.brightnessShift = 0.0; this._quadtree = undefined; this._terrainProvider = options.terrainProvider; @@ -362,18 +362,6 @@ define([ this._imageryLayers._update(); }; - function freeVertexArray(vertexArray) { - var indexBuffer = vertexArray.indexBuffer; - vertexArray.destroy(); - - if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { - --indexBuffer.referenceCount; - if (indexBuffer.referenceCount === 0) { - indexBuffer.destroy(); - } - } - } - function updateCredits(surface, frameState) { var creditDisplay = frameState.creditDisplay; if (surface._terrainProvider.ready && defined(surface._terrainProvider.credit)) { @@ -516,8 +504,6 @@ define([ return this._terrainProvider.getLevelMaximumGeometricError(level); }; - var stopLoad = false; - /** * Loads, or continues loading, a given tile. This function will continue to be called * until {@link QuadtreeTile#state} is no longer {@link QuadtreeTileLoadState#LOADING}. This function should @@ -529,10 +515,33 @@ define([ * @exception {DeveloperError} loadTile must not be called before the tile provider is ready. */ GlobeSurfaceTileProvider.prototype.loadTile = function(frameState, tile) { - if (stopLoad) { - return; + // We don't want to load imagery until we're certain that the terrain tiles are actually visible. + // So if our bounding volume isn't accurate because it came from another tile, load terrain only + // initially. If we load some terrain and suddenly have a more accurate bounding volume and the + // tile is _still_ visible, give the tile a chance to load imagery immediately rather than + // waiting for next frame. + + var surfaceTile = tile.data; + var terrainOnly = true; + var terrainStateBefore; + if (defined(surfaceTile)) { + terrainOnly = surfaceTile.boundingVolumeSourceTile !== tile; + terrainStateBefore = surfaceTile.terrainState; + } + + GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, terrainOnly); + + surfaceTile = tile.data; + if (terrainOnly && terrainStateBefore !== tile.data.terrainState) { + // Terrain state changed. If: + // a) The tile is visible, and + // b) The bounding volume is accurate (updated as a side effect of computing visibility) + // Then we'll load imagery, too. + if (this.computeTileVisibility(tile, frameState, this.quadtree.occluders) && surfaceTile.boundingVolumeSourceTile === tile) { + terrainOnly = false; + GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, terrainOnly); + } } - GlobeSurfaceTile.processStateMachine(tile, frameState, this._terrainProvider, this._imageryLayers); }; var boundingSphereScratch = new BoundingSphere(); @@ -750,7 +759,7 @@ define([ Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]); return ellipsoidalOccluder.computeHorizonCullingPoint(center, cornerPositions, result); -} + } /** * Gets the distance from the camera to the closest point on the tile. This is used for level-of-detail selection. diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index e054c659b2f..df5a27bc8ae 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -195,6 +195,12 @@ define([ get : function() { return this._tileLoadProgressEvent; } + }, + + occluders : { + get : function() { + return this._occluders; + } } }); @@ -796,6 +802,7 @@ define([ } if (primitive.preloadAncestors) { + // TODO: don't queue here if this tile was queued at medium above. queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } } @@ -963,6 +970,8 @@ define([ tileProvider.loadTile(frameState, tile); didSomething = true; } + + return didSomething; } var scratchRay = new Ray(); @@ -971,6 +980,10 @@ define([ var scratchArray = []; function updateHeights(primitive, frameState) { + if (!primitive.tileProvider.ready) { + return; + } + var tryNextFrame = scratchArray; tryNextFrame.length = 0; var tilesToUpdateHeights = primitive._tileToUpdateHeights; @@ -982,7 +995,7 @@ define([ var mode = frameState.mode; var projection = frameState.mapProjection; - var ellipsoid = projection.ellipsoid; + var ellipsoid = primitive.tileProvider.tilingScheme.ellipsoid; var i; while (tilesToUpdateHeights.length > 0) { @@ -1089,7 +1102,6 @@ define([ var tileProvider = primitive._tileProvider; var tilesToRender = primitive._tilesToRender; var nearestRenderableTiles = primitive._nearestRenderableTiles; - var tilesToUpdateHeights = primitive._tileToUpdateHeights; for (var i = 0, len = tilesToRender.length; i < len; ++i) { var tile = tilesToRender[i]; diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js index d371fe37df3..236f87202ac 100644 --- a/Specs/MockTerrainProvider.js +++ b/Specs/MockTerrainProvider.js @@ -1,6 +1,7 @@ define([ 'Core/defined', 'Core/GeographicTilingScheme', + 'Core/HeightmapTerrainData', 'Core/RuntimeError', 'Core/TerrainProvider', 'ThirdParty/when', @@ -9,6 +10,7 @@ define([ ], function( defined, GeographicTilingScheme, + HeightmapTerrainData, RuntimeError, TerrainProvider, when, @@ -33,6 +35,7 @@ define([ } MockTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { + console.log('request ' + createTileKey(x, y, level)); var willSucceed = this._requestTileGeometryWillSucceed[createTileKey(x, y, level)]; if (willSucceed === undefined) { return undefined; // defer by default @@ -57,7 +60,11 @@ define([ }; MockTerrainProvider.prototype.getNearestBvhLevel = function(x, y, level) { - return this._nearestBvhLevel[createTileKey(x, y, level)]; + var bvhLevel = this._nearestBvhLevel[createTileKey(x, y, level)]; + if (!defined(bvhLevel)) { + bvhLevel = -1; + } + return bvhLevel; }; MockTerrainProvider.prototype.getLevelMaximumGeometricError = function(level) { @@ -153,65 +160,80 @@ define([ }; function createTerrainData(terrainProvider, x, y, level, upsampled) { - var terrainData = jasmine.createSpyObj('MockTerrainData', ['createMesh', 'upsample', 'wasCreatedByUpsampling']); - terrainData.wasCreatedByUpsampling.and.returnValue(upsampled); - - if (!upsampled) { - var willHaveWaterMask = terrainProvider._willHaveWaterMask[createTileKey(x, y, level)]; - if (defined(willHaveWaterMask)) { - if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { - terrainData.waterMask = new Uint8Array(4); - terrainData.waterMask[0] = 1; - terrainData.waterMask[1] = 1; - terrainData.waterMask[2] = 0; - terrainData.waterMask[3] = 0; - } else if (willHaveWaterMask.includeLand) { - terrainData.waterMask = new Uint8Array(1); - terrainData.waterMask[0] = 0; - } else if (willHaveWaterMask.includeWater) { - terrainData.waterMask = new Uint8Array(1); - terrainData.waterMask[0] = 1; - } + var options = { + width: 5, + height: 5, + buffer: new Float32Array(25), + createdByUpsampling: upsampled + }; + + var willHaveWaterMask = terrainProvider._willHaveWaterMask[createTileKey(x, y, level)]; + if (defined(willHaveWaterMask)) { + if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { + options.waterMask = new Uint8Array(4); + options.waterMask[0] = 1; + options.waterMask[1] = 1; + options.waterMask[2] = 0; + options.waterMask[3] = 0; + } else if (willHaveWaterMask.includeLand) { + options.waterMask = new Uint8Array(1); + options.waterMask[0] = 0; + } else if (willHaveWaterMask.includeWater) { + options.waterMask = new Uint8Array(1); + options.waterMask[0] = 1; } + } - var willHaveBvh = terrainProvider._willHaveBvh[createTileKey(x, y, level)]; - if (defined(willHaveBvh)) { - terrainData.bvh = willHaveBvh; - } + var willHaveBvh = terrainProvider._willHaveBvh[createTileKey(x, y, level)]; + if (defined(willHaveBvh)) { + options.bvh = willHaveBvh; } - terrainData.createMesh.and.callFake(function(tilingScheme, x, y, level) { - var willSucceed = terrainProvider._createMeshWillSucceed[createTileKey(x, y, level)]; + var terrainData = new HeightmapTerrainData(options); + + var originalUpsample = terrainData.upsample; + terrainData.upsample = function(tilingScheme, thisX, thisY, thisLevel, descendantX, descendantY) { + console.log('upsample ' + createTileKey(descendantX, descendantY, thisLevel + 1)); + + var willSucceed = terrainProvider._upsampleWillSucceed[createTileKey(descendantX, descendantY, thisLevel + 1)]; if (willSucceed === undefined) { return undefined; // defer by default } - return runLater(function() { - if (willSucceed === true) { - return {}; - } else if (willSucceed === false) { - throw new RuntimeError('createMesh failed as requested.'); - } + if (willSucceed) { + return originalUpsample.apply(terrainData, arguments); + } - return when(willSucceed).then(function() { - return {}; - }); + return runLater(function() { + throw new RuntimeError('upsample failed as requested.'); }); - }); + }; - terrainData.upsample.and.callFake(function(tilingScheme, thisX, thisY, thisLevel, descendantX, descendantY) { - var willSucceed = terrainProvider._upsampleWillSucceed[createTileKey(descendantX, descendantY, thisLevel + 1)]; + var originalCreateMesh = terrainData.createMesh; + terrainData.createMesh = function(tilingScheme, x, y, level) { + console.log('createMesh ' + createTileKey(x, y, level)); + + var willSucceed = terrainProvider._createMeshWillSucceed[createTileKey(x, y, level)]; if (willSucceed === undefined) { return undefined; // defer by default } + if (willSucceed === true) { + return originalCreateMesh.apply(terrainData, arguments); + } else if (willSucceed === false) { + return runLater(function() { + throw new RuntimeError('createMesh failed as requested.'); + }); + } + + var args = arguments; + return runLater(function() { - if (willSucceed) { - return createTerrainData(terrainProvider, descendantX, descendantY, thisLevel + 1, true); - } - throw new RuntimeError('upsample failed as requested.'); + return when(willSucceed).then(function() { + return originalCreateMesh.apply(terrainData, args); + }); }); - }); + }; return terrainData; } diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 8de54ea3652..dcfb49feabd 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -48,28 +48,6 @@ defineSuite([ var mockTerrain; var processor; - function mockWebGL() { - spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() { - var vertexArray = jasmine.createSpyObj('VertexArray', ['destroy']); - return vertexArray; - }); - - spyOn(ImageryLayer.prototype, '_createTextureWebGL').and.callFake(function(context, imagery) { - var texture = jasmine.createSpyObj('Texture', ['destroy']); - texture.width = imagery.image.width; - texture.height = imagery.image.height; - return texture; - }); - - spyOn(ImageryLayer.prototype, '_finalizeReprojectTexture'); - - spyOn(Texture, 'create').and.callFake(function(options) { - var result = clone(options); - result.destroy = function() {}; - return result; - }); - } - beforeEach(function() { frameState = { context: { @@ -95,7 +73,7 @@ defineSuite([ describe('processStateMachine', function() { beforeEach(function() { - mockWebGL(); + processor.mockWebGL(); }); it('starts in the START state', function() { @@ -164,12 +142,11 @@ defineSuite([ it('upsamples failed tiles from parent TerrainData', function() { mockTerrain .requestTileGeometryWillSucceed(rootTile) + .createMeshWillSucceed(rootTile) .willBeUnavailable(rootTile.southwestChild) .upsampleWillSucceed(rootTile.southwestChild); return processor.process([rootTile, rootTile.southwestChild]).then(function() { - expect(rootTile.data.terrainState).toBe(TerrainState.RECEIVED); - expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.RECEIVED); expect(rootTile.data.terrainData.wasCreatedByUpsampling()).toBe(false); expect(rootTile.southwestChild.data.terrainData.wasCreatedByUpsampling()).toBe(true); }); @@ -414,7 +391,7 @@ defineSuite([ describe('getBoundingVolumeHierarchy', function() { beforeEach(function() { - mockWebGL(); + processor.mockWebGL(); }); it('gets the BVH from the TerrainData if available', function() { @@ -511,7 +488,7 @@ defineSuite([ describe('eligibleForUnloading', function() { beforeEach(function() { - mockWebGL(); + processor.mockWebGL(); }); it('returns true when no loading has been done', function() { diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index b609f1bb792..7d2d069b703 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -3,532 +3,764 @@ defineSuite([ 'Core/Cartesian3', 'Core/Cartographic', 'Core/defineProperties', + 'Core/Ellipsoid', 'Core/EventHelper', + 'Core/GeographicProjection', 'Core/GeographicTilingScheme', + 'Core/Intersect', + 'Core/Rectangle', 'Core/Visibility', + 'Scene/Camera', + 'Scene/GlobeSurfaceTileProvider', + 'Scene/ImageryLayerCollection', 'Scene/QuadtreeTileLoadState', + 'Scene/SceneMode', 'Specs/createScene', - 'Specs/pollToPromise' + 'Specs/pollToPromise', + 'ThirdParty/when', + '../MockTerrainProvider', + '../TerrainTileProcessor' ], function( QuadtreePrimitive, Cartesian3, Cartographic, defineProperties, + Ellipsoid, EventHelper, + GeographicProjection, GeographicTilingScheme, + Intersect, + Rectangle, Visibility, + Camera, + GlobeSurfaceTileProvider, + ImageryLayerCollection, QuadtreeTileLoadState, + SceneMode, createScene, - pollToPromise) { + pollToPromise, + when, + MockTerrainProvider, + TerrainTileProcessor) { 'use strict'; - var scene; + describe('selectTilesForRendering', function() { + var scene; + var camera; + var frameState; + var quadtree; + var mockTerrain; + var tileProvider; + var imageryLayerCollection; + var surfaceShaderSet; + var processor; + var rootTiles; + + beforeEach(function() { + console.log('beforeEach - start'); + scene = { + mapProjection: new GeographicProjection(), + drawingBufferWidth: 1000, + drawingBufferHeight: 1000 + }; - beforeAll(function() { - scene = createScene(); - scene.render(); - }); + camera = new Camera(scene); + + frameState = { + frameNumber: 0, + passes: { + render: true + }, + camera: camera, + fog: { + enabled: false + }, + context: { + drawingBufferWidth: scene.drawingBufferWidth, + drawingBufferHeight: scene.drawingBufferHeight + }, + mode: SceneMode.SCENE3D, + commandList: [], + cullingVolume: jasmine.createSpyObj('CullingVolume', ['computeVisibility']), + afterRender: [] + }; - afterAll(function() { - scene.destroyForSpecs(); - }); + frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING); - it('must be constructed with a tileProvider', function() { - expect(function() { - return new QuadtreePrimitive(); - }).toThrowDeveloperError(); + imageryLayerCollection = new ImageryLayerCollection(); + surfaceShaderSet = jasmine.createSpyObj('SurfaceShaderSet', ['getShaderProgram']); + mockTerrain = new MockTerrainProvider(); + tileProvider = new GlobeSurfaceTileProvider({ + terrainProvider: mockTerrain, + imageryLayers: imageryLayerCollection, + surfaceShaderSet: surfaceShaderSet + }); + quadtree = new QuadtreePrimitive({ + tileProvider: tileProvider + }); - expect(function() { - return new QuadtreePrimitive({}); - }).toThrowDeveloperError(); - }); + processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection); - function createSpyTileProvider() { - var result = jasmine.createSpyObj('tileProvider', [ - 'getQuadtree', 'setQuadtree', 'getReady', 'getTilingScheme', 'getErrorEvent', - 'initialize', 'updateImagery', 'beginUpdate', 'endUpdate', 'getLevelMaximumGeometricError', 'loadTile', - 'computeTileVisibility', 'showTileThisFrame', 'computeDistanceToTile', 'canRefine', 'isDestroyed', 'destroy']); - - defineProperties(result, { - quadtree : { - get : result.getQuadtree, - set : result.setQuadtree - }, - ready : { - get : result.getReady - }, - tilingScheme : { - get : result.getTilingScheme - }, - errorEvent : { - get : result.getErrorEvent - } + quadtree.render(frameState); + rootTiles = quadtree._levelZeroTiles; + + processor.mockWebGL(); + console.log('beforeEach - end'); }); - var tilingScheme = new GeographicTilingScheme(); - result.getTilingScheme.and.returnValue(tilingScheme); + function process(quadtreePrimitive, callback) { + var deferred = when.defer(); + + function next() { + ++frameState.frameNumber; + console.log('Frame ' + frameState.frameNumber); + quadtree.beginFrame(frameState); + quadtree.render(frameState); + quadtree.endFrame(frameState); + + if (callback()) { + setTimeout(next, 0); + } else { + deferred.resolve(); + } + } + + next(); + + return deferred.promise; + } - result.canRefine.and.callFake(function(tile) { - return tile.renderable; + it('selects nothing when the root tiles are not yet ready', function() { + quadtree.render(frameState); + expect(quadtree._tilesToRender.length).toBe(0); }); - return result; - } + it('selects root tiles once they\'re ready', function() { + mockTerrain + .requestTileGeometryWillSucceed(rootTiles[0]) + .requestTileGeometryWillSucceed(rootTiles[1]) + .createMeshWillSucceed(rootTiles[0]) + .createMeshWillSucceed(rootTiles[1]); + + return processor.process(rootTiles).then(function() { + quadtree.render(frameState); - it('calls initialize, beginUpdate, loadTile, and endUpdate', function() { - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); + // There should be at least one selected tile. + expect(quadtree._tilesToRender.length).toBeGreaterThan(0); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider + // All selected tiles should be root tiles. + expect(quadtree._tilesToRender.filter(function(tile) { return tile.level === 0; }).length).toBe(quadtree._tilesToRender.length); + }); }); - // determine what tiles to load - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - // load tiles - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - expect(tileProvider.initialize).toHaveBeenCalled(); - expect(tileProvider.beginUpdate).toHaveBeenCalled(); - expect(tileProvider.loadTile).toHaveBeenCalled(); - expect(tileProvider.endUpdate).toHaveBeenCalled(); + it('selects deeper tiles once they\'re renderable', function() { + mockTerrain + .requestTileGeometryWillSucceed(rootTiles[0]) + .requestTileGeometryWillSucceed(rootTiles[1]) + .createMeshWillSucceed(rootTiles[0]) + .createMeshWillSucceed(rootTiles[1]); + + rootTiles[0].children.forEach(function(tile) { + mockTerrain + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + expect(tile.renderable).toBe(false); + }); + + return processor.process(rootTiles).then(function() { + quadtree.render(frameState); + + // All selected tiles should be root tiles. + expect(quadtree._tilesToRender.length).toBeGreaterThan(0); + expect(quadtree._tilesToRender.filter(function(tile) { return tile.level === 0; }).length).toBe(quadtree._tilesToRender.length); + + // Allow the child tiles to load. + return processor.process(rootTiles[0].children); + }).then(function() { + quadtree.render(frameState); + + // Now child tiles should be rendered too. + expect(quadtree._tilesToRender).toContain(rootTiles[0].southwestChild); + expect(quadtree._tilesToRender).toContain(rootTiles[0].southeastChild); + expect(quadtree._tilesToRender).toContain(rootTiles[0].northwestChild); + expect(quadtree._tilesToRender).toContain(rootTiles[0].northeastChild); + }); + }); + + it('skips levels when tiles are known to be available', function() { + console.log('start'); + + // Mark all tiles through level 2 as available. + var tiles = []; + rootTiles.forEach(function(tile) { + tiles.push(tile); + + // level 0 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + tiles.push(tile); + + // level 1 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + tiles.push(tile); + + // level 2 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + }); + }); + }); + + quadtree.preloadAncestors = false; + + // Look down at the center of a level 2 tile. + var lookAtTile = rootTiles[0].southwestChild.northeastChild; + expect(lookAtTile.level).toBe(2); + expect(lookAtTile.parent.level).toBe(1); + + var lookAtCenter = Ellipsoid.WGS84.cartographicToCartesian(Rectangle.center(lookAtTile.rectangle)); + camera.lookAt(lookAtCenter, new Cartesian3(0.0, 0.0, 100.0)); + + var originalComputeDistanceToTile = quadtree.tileProvider.computeDistanceToTile; + spyOn(quadtree.tileProvider, 'computeDistanceToTile').and.callFake(function(tile, frameState) { + var result = originalComputeDistanceToTile.apply(this, arguments); + if (tile.level === 2) { + // Level 2 is far away so it will definitely meet SSE requirement. + return 9999999999999.0; + } + return result; + }); + + return process(quadtree, function() { + // Process until the lookAtTile is rendered. That tile's parent (level 1) + // should never be rendered along the way. + expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent); + return quadtree._tilesToRender.indexOf(lookAtTile) >= 0; + }); + }); }); - it('shows the root tiles when they are ready and visible', function() { - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); - tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); - tileProvider.loadTile.and.callFake(function(frameState, tile) { - tile.renderable = true; + describe('old', function() { + var scene; + + beforeAll(function() { + scene = createScene(); + scene.render(); }); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider + afterAll(function() { + scene.destroyForSpecs(); }); - // determine what tiles to load - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + it('must be constructed with a tileProvider', function() { + expect(function() { + return new QuadtreePrimitive(); + }).toThrowDeveloperError(); - // load tiles - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + expect(function() { + return new QuadtreePrimitive({}); + }).toThrowDeveloperError(); + }); - expect(tileProvider.showTileThisFrame).toHaveBeenCalled(); - }); + function createSpyTileProvider() { + var result = jasmine.createSpyObj('tileProvider', [ + 'getQuadtree', 'setQuadtree', 'getReady', 'getTilingScheme', 'getErrorEvent', + 'initialize', 'updateImagery', 'beginUpdate', 'endUpdate', 'getLevelMaximumGeometricError', 'loadTile', + 'computeTileVisibility', 'showTileThisFrame', 'computeDistanceToTile', 'canRefine', 'isDestroyed', 'destroy']); + + defineProperties(result, { + quadtree : { + get : result.getQuadtree, + set : result.setQuadtree + }, + ready : { + get : result.getReady + }, + tilingScheme : { + get : result.getTilingScheme + }, + errorEvent : { + get : result.getErrorEvent + } + }); - it('stops loading a tile that moves to the DONE state', function() { - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); - tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + var tilingScheme = new GeographicTilingScheme(); + result.getTilingScheme.and.returnValue(tilingScheme); - var calls = 0; - tileProvider.loadTile.and.callFake(function(frameState, tile) { - ++calls; - tile.state = QuadtreeTileLoadState.DONE; + result.canRefine.and.callFake(function(tile) { + return tile.renderable; + }); + + return result; + } + + it('calls initialize, beginUpdate, loadTile, and endUpdate', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); + + // determine what tiles to load + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + // load tiles + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + expect(tileProvider.initialize).toHaveBeenCalled(); + expect(tileProvider.beginUpdate).toHaveBeenCalled(); + expect(tileProvider.loadTile).toHaveBeenCalled(); + expect(tileProvider.endUpdate).toHaveBeenCalled(); }); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider + it('shows the root tiles when they are ready and visible', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + tileProvider.loadTile.and.callFake(function(frameState, tile) { + tile.renderable = true; + }); + + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); + + // determine what tiles to load + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + // load tiles + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + expect(tileProvider.showTileThisFrame).toHaveBeenCalled(); }); - // determine what tiles to load - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + it('stops loading a tile that moves to the DONE state', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); - // load tiles - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + var calls = 0; + tileProvider.loadTile.and.callFake(function(frameState, tile) { + ++calls; + tile.state = QuadtreeTileLoadState.DONE; + }); - expect(calls).toBe(2); + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + // determine what tiles to load + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - expect(calls).toBe(2); - }); + // load tiles + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - it('tileLoadProgressEvent is raised when tile loaded and when new children discovered', function() { - var eventHelper = new EventHelper(); + expect(calls).toBe(2); - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); - tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider + expect(calls).toBe(2); }); - var progressEventSpy = jasmine.createSpy('progressEventSpy'); - eventHelper.add(quadtree.tileLoadProgressEvent, progressEventSpy); + it('tileLoadProgressEvent is raised when tile loaded and when new children discovered', function() { + var eventHelper = new EventHelper(); + + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); - // Initial update to get the zero-level tiles set up. - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); - // load zero-level tiles - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + var progressEventSpy = jasmine.createSpy('progressEventSpy'); + eventHelper.add(quadtree.tileLoadProgressEvent, progressEventSpy); - quadtree.update(scene.frameState); + // Initial update to get the zero-level tiles set up. + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - scene.renderForSpecs(); + // load zero-level tiles + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - // There will now be two zero-level tiles in the load queue. - expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(2); + quadtree.update(scene.frameState); - // Change one to loaded and update again - quadtree._levelZeroTiles[0].state = QuadtreeTileLoadState.DONE; - quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.LOADING; + scene.renderForSpecs(); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + // There will now be two zero-level tiles in the load queue. + expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(2); - quadtree.update(scene.frameState); + // Change one to loaded and update again + quadtree._levelZeroTiles[0].state = QuadtreeTileLoadState.DONE; + quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.LOADING; - scene.renderForSpecs(); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - // Now there should only be one left in the update queue - expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(1); + quadtree.update(scene.frameState); - // Simulate the second zero-level child having loaded with two children. - quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.DONE; - quadtree._levelZeroTiles[1].renderable = true; + scene.renderForSpecs(); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + // Now there should only be one left in the update queue + expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(1); - quadtree.update(scene.frameState); + // Simulate the second zero-level child having loaded with two children. + quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.DONE; + quadtree._levelZeroTiles[1].renderable = true; - scene.renderForSpecs(); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - // Now that tile's four children should be in the load queue. - expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(4); - }); + quadtree.update(scene.frameState); - it('forEachLoadedTile does not enumerate tiles in the START state', function() { - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); - tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); - tileProvider.computeDistanceToTile.and.returnValue(1e-15); + scene.renderForSpecs(); - // Load the root tiles. - tileProvider.loadTile.and.callFake(function(frameState, tile) { - tile.state = QuadtreeTileLoadState.DONE; - tile.renderable = true; + // Now that tile's four children should be in the load queue. + expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(4); }); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider - }); + it('forEachLoadedTile does not enumerate tiles in the START state', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + tileProvider.computeDistanceToTile.and.returnValue(1e-15); - // determine what tiles to load - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - // load tiles - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - // Don't load further tiles. - tileProvider.loadTile.and.callFake(function(frameState, tile) { - tile.state = QuadtreeTileLoadState.START; - }); + // Load the root tiles. + tileProvider.loadTile.and.callFake(function(frameState, tile) { + tile.state = QuadtreeTileLoadState.DONE; + tile.renderable = true; + }); - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); - quadtree.forEachLoadedTile(function(tile) { - expect(tile.state).not.toBe(QuadtreeTileLoadState.START); - }); - }); + // determine what tiles to load + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - 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(frameState, tile) { - tile.state = QuadtreeTileLoadState.DONE; - tile.renderable = true; - tile.data = { - pick : function() { - return undefined; - } - }; - }); + // load tiles + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider - }); + // Don't load further tiles. + tileProvider.loadTile.and.callFake(function(frameState, tile) { + tile.state = QuadtreeTileLoadState.START; + }); - var removeFunc = quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(position) { + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + quadtree.forEachLoadedTile(function(tile) { + expect(tile.state).not.toBe(QuadtreeTileLoadState.START); + }); }); - // determine what tiles to load - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + 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); - ++scene.frameState.frameNumber; + // Load the root tiles. + tileProvider.loadTile.and.callFake(function(frameState, tile) { + tile.state = QuadtreeTileLoadState.DONE; + tile.renderable = true; + tile.data = { + pick : function() { + return undefined; + } + }; + }); - // load tiles - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); - var addedCallback = false; - quadtree.forEachLoadedTile(function(tile) { - addedCallback = addedCallback || tile.customData.length > 0; - }); + var removeFunc = quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(position) { + }); - expect(addedCallback).toEqual(true); + // determine what tiles to load + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - removeFunc(); + ++scene.frameState.frameNumber; - ++scene.frameState.frameNumber; + // load tiles + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + var addedCallback = false; + quadtree.forEachLoadedTile(function(tile) { + addedCallback = addedCallback || tile.customData.length > 0; + }); - var removedCallback = true; - quadtree.forEachLoadedTile(function(tile) { - removedCallback = removedCallback && tile.customData.length === 0; - }); + expect(addedCallback).toEqual(true); - expect(removedCallback).toEqual(true); - }); + removeFunc(); - it('updates heights', function() { - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); - tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); - tileProvider.computeDistanceToTile.and.returnValue(1e-15); + ++scene.frameState.frameNumber; - tileProvider.terrainProvider = { - getTileDataAvailable : function() { - return true; - } - }; - - var position = Cartesian3.clone(Cartesian3.ZERO); - var updatedPosition = Cartesian3.clone(Cartesian3.UNIT_X); - var currentPosition = position; - - // Load the root tiles. - tileProvider.loadTile.and.callFake(function(frameState, tile) { - tile.state = QuadtreeTileLoadState.DONE; - tile.renderable = true; - tile.data = { - pick : function() { - return currentPosition; - } - }; - }); + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider - }); + var removedCallback = true; + quadtree.forEachLoadedTile(function(tile) { + removedCallback = removedCallback && tile.customData.length === 0; + }); - quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(p) { - Cartesian3.clone(p, position); + expect(removedCallback).toEqual(true); }); - // determine what tiles to load - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + it('updates heights', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + tileProvider.computeDistanceToTile.and.returnValue(1e-15); - // load tiles - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + tileProvider.terrainProvider = { + getTileDataAvailable : function() { + return true; + } + }; - expect(position).toEqual(Cartesian3.ZERO); + var position = Cartesian3.clone(Cartesian3.ZERO); + var updatedPosition = Cartesian3.clone(Cartesian3.UNIT_X); + var currentPosition = position; - currentPosition = updatedPosition; + // Load the root tiles. + tileProvider.loadTile.and.callFake(function(frameState, tile) { + tile.state = QuadtreeTileLoadState.DONE; + tile.renderable = true; + tile.data = { + pick : function() { + return currentPosition; + } + }; + }); + + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); + + quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(p) { + Cartesian3.clone(p, position); + }); + + // determine what tiles to load + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); + // load tiles + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - expect(position).toEqual(updatedPosition); - }); + expect(position).toEqual(Cartesian3.ZERO); + + currentPosition = updatedPosition; - it('gives correct priority to tile loads', function() { - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); - tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider + expect(position).toEqual(updatedPosition); }); - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - // The root tiles should be in the high priority load queue - expect(quadtree._tileLoadQueueHigh.length).toBe(2); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); - expect(quadtree._tileLoadQueueMedium.length).toBe(0); - expect(quadtree._tileLoadQueueLow.length).toBe(0); - - // Mark the first root tile renderable (but not done loading) - quadtree._levelZeroTiles[0].renderable = true; - - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - // That root tile should now load with low priority while its children should load with high. - expect(quadtree._tileLoadQueueHigh.length).toBe(5); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[1]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[2]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[3]); - expect(quadtree._tileLoadQueueMedium.length).toBe(0); - expect(quadtree._tileLoadQueueLow.length).toBe(1); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]); - - // Mark the children of that root tile renderable too, so we can refine it - quadtree._levelZeroTiles[0].children[0].renderable = true; - quadtree._levelZeroTiles[0].children[1].renderable = true; - quadtree._levelZeroTiles[0].children[2].renderable = true; - quadtree._levelZeroTiles[0].children[3].renderable = true; - - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - expect(quadtree._tileLoadQueueHigh.length).toBe(17); // levelZeroTiles[1] plus levelZeroTiles[0]'s 16 grandchildren - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[0]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[1]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[2]); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[3]); - expect(quadtree._tileLoadQueueMedium.length).toBe(0); - expect(quadtree._tileLoadQueueLow.length).toBe(5); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]); - expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]); - - // Mark the children of levelZeroTiles[0] upsampled - quadtree._levelZeroTiles[0].children[0].upsampledFromParent = true; - quadtree._levelZeroTiles[0].children[1].upsampledFromParent = true; - quadtree._levelZeroTiles[0].children[2].upsampledFromParent = true; - quadtree._levelZeroTiles[0].children[3].upsampledFromParent = true; - - quadtree.update(scene.frameState); - quadtree.beginFrame(scene.frameState); - quadtree.render(scene.frameState); - quadtree.endFrame(scene.frameState); - - // levelZeroTiles[0] should move to medium priority. - expect(quadtree._tileLoadQueueHigh.length).toBe(1); - expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); - expect(quadtree._tileLoadQueueMedium.length).toBe(1); - expect(quadtree._tileLoadQueueMedium).toContain(quadtree._levelZeroTiles[0]); - expect(quadtree._tileLoadQueueLow.length).toBe(0); - }); + it('gives correct priority to tile loads', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); - it('renders tiles in approximate near-to-far order', function() { - var tileProvider = createSpyTileProvider(); - tileProvider.getReady.and.returnValue(true); - tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); - var quadtree = new QuadtreePrimitive({ - tileProvider : tileProvider - }); + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); - tileProvider.loadTile.and.callFake(function(frameState, tile) { - if (tile.level <= 1) { - tile.state = QuadtreeTileLoadState.DONE; - tile.renderable = true; - } - }); + // The root tiles should be in the high priority load queue + expect(quadtree._tileLoadQueueHigh.length).toBe(2); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); + expect(quadtree._tileLoadQueueMedium.length).toBe(0); + expect(quadtree._tileLoadQueueLow.length).toBe(0); - scene.camera.setView({ - destination : Cartesian3.fromDegrees(1.0, 1.0, 15000.0) - }); - scene.camera.update(scene.mode); + // Mark the first root tile renderable (but not done loading) + quadtree._levelZeroTiles[0].renderable = true; - return pollToPromise(function() { quadtree.update(scene.frameState); quadtree.beginFrame(scene.frameState); quadtree.render(scene.frameState); quadtree.endFrame(scene.frameState); - return quadtree._tilesToRender.filter(function(tile) { return tile.level === 1; }).length === 8; - }).then(function() { + // That root tile should now load with low priority while its children should load with high. + expect(quadtree._tileLoadQueueHigh.length).toBe(5); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[1]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[2]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[3]); + expect(quadtree._tileLoadQueueMedium.length).toBe(0); + expect(quadtree._tileLoadQueueLow.length).toBe(1); + expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]); + + // Mark the children of that root tile renderable too, so we can refine it + quadtree._levelZeroTiles[0].children[0].renderable = true; + quadtree._levelZeroTiles[0].children[1].renderable = true; + quadtree._levelZeroTiles[0].children[2].renderable = true; + quadtree._levelZeroTiles[0].children[3].renderable = true; + quadtree.update(scene.frameState); quadtree.beginFrame(scene.frameState); quadtree.render(scene.frameState); quadtree.endFrame(scene.frameState); - // Rendered tiles: - // +----+----+----+----+ - // |w.nw|w.ne|e.nw|e.ne| - // +----+----+----+----+ - // |w.sw|w.se|e.sw|e.se| - // +----+----+----+----+ - // camera is located in e.nw (east.northwestChild) - - var west = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 0; })[0]; - var east = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 1; })[0]; - expect(quadtree._tilesToRender[0]).toBe(east.northwestChild); - expect(quadtree._tilesToRender[1] === east.southwestChild || quadtree._tilesToRender[1] === east.northeastChild).toBe(true); - expect(quadtree._tilesToRender[2] === east.southwestChild || quadtree._tilesToRender[2] === east.northeastChild).toBe(true); - expect(quadtree._tilesToRender[3]).toBe(east.southeastChild); - expect(quadtree._tilesToRender[4]).toBe(west.northeastChild); - expect(quadtree._tilesToRender[5] === west.northwestChild || quadtree._tilesToRender[5] === west.southeastChild).toBe(true); - expect(quadtree._tilesToRender[6] === west.northwestChild || quadtree._tilesToRender[6] === west.southeastChild).toBe(true); - expect(quadtree._tilesToRender[7]).toBe(west.southwestChild); + expect(quadtree._tileLoadQueueHigh.length).toBe(17); // levelZeroTiles[1] plus levelZeroTiles[0]'s 16 grandchildren + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[0]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[1]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[2]); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[3]); + expect(quadtree._tileLoadQueueMedium.length).toBe(0); + expect(quadtree._tileLoadQueueLow.length).toBe(5); + expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]); + expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]); + expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]); + expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]); + expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]); + + // Mark the children of levelZeroTiles[0] upsampled + quadtree._levelZeroTiles[0].children[0].upsampledFromParent = true; + quadtree._levelZeroTiles[0].children[1].upsampledFromParent = true; + quadtree._levelZeroTiles[0].children[2].upsampledFromParent = true; + quadtree._levelZeroTiles[0].children[3].upsampledFromParent = true; + + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + // levelZeroTiles[0] should move to medium priority. + expect(quadtree._tileLoadQueueHigh.length).toBe(1); + expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]); + expect(quadtree._tileLoadQueueMedium.length).toBe(1); + expect(quadtree._tileLoadQueueMedium).toContain(quadtree._levelZeroTiles[0]); + expect(quadtree._tileLoadQueueLow.length).toBe(0); }); - }); -}, 'WebGL'); + + it('renders tiles in approximate near-to-far order', function() { + var tileProvider = createSpyTileProvider(); + tileProvider.getReady.and.returnValue(true); + tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL); + + var quadtree = new QuadtreePrimitive({ + tileProvider : tileProvider + }); + + tileProvider.loadTile.and.callFake(function(frameState, tile) { + if (tile.level <= 1) { + tile.state = QuadtreeTileLoadState.DONE; + tile.renderable = true; + } + }); + + scene.camera.setView({ + destination : Cartesian3.fromDegrees(1.0, 1.0, 15000.0) + }); + scene.camera.update(scene.mode); + + return pollToPromise(function() { + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + return quadtree._tilesToRender.filter(function(tile) { return tile.level === 1; }).length === 8; + }).then(function() { + quadtree.update(scene.frameState); + quadtree.beginFrame(scene.frameState); + quadtree.render(scene.frameState); + quadtree.endFrame(scene.frameState); + + // Rendered tiles: + // +----+----+----+----+ + // |w.nw|w.ne|e.nw|e.ne| + // +----+----+----+----+ + // |w.sw|w.se|e.sw|e.se| + // +----+----+----+----+ + // camera is located in e.nw (east.northwestChild) + + var west = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 0; })[0]; + var east = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 1; })[0]; + expect(quadtree._tilesToRender[0]).toBe(east.northwestChild); + expect(quadtree._tilesToRender[1] === east.southwestChild || quadtree._tilesToRender[1] === east.northeastChild).toBe(true); + expect(quadtree._tilesToRender[2] === east.southwestChild || quadtree._tilesToRender[2] === east.northeastChild).toBe(true); + expect(quadtree._tilesToRender[3]).toBe(east.southeastChild); + expect(quadtree._tilesToRender[4]).toBe(west.northeastChild); + expect(quadtree._tilesToRender[5] === west.northwestChild || quadtree._tilesToRender[5] === west.southeastChild).toBe(true); + expect(quadtree._tilesToRender[6] === west.northwestChild || quadtree._tilesToRender[6] === west.southeastChild).toBe(true); + expect(quadtree._tilesToRender[7]).toBe(west.southwestChild); + }); + }); + }, 'WebGL'); + +}); diff --git a/Specs/TerrainTileProcessor.js b/Specs/TerrainTileProcessor.js index 157a6a0d434..0270d17504d 100644 --- a/Specs/TerrainTileProcessor.js +++ b/Specs/TerrainTileProcessor.js @@ -1,9 +1,15 @@ define([ + 'Core/clone', + 'Renderer/Texture', 'Scene/GlobeSurfaceTile', + 'Scene/ImageryLayer', 'Scene/TerrainState', 'ThirdParty/when' ], function( + clone, + Texture, GlobeSurfaceTile, + ImageryLayer, TerrainState, when) { 'use strict'; @@ -89,6 +95,28 @@ define([ return deferred.promise; }; + TerrainTileProcessor.prototype.mockWebGL = function() { + spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() { + var vertexArray = jasmine.createSpyObj('VertexArray', ['destroy']); + return vertexArray; + }); + + spyOn(ImageryLayer.prototype, '_createTextureWebGL').and.callFake(function(context, imagery) { + var texture = jasmine.createSpyObj('Texture', ['destroy']); + texture.width = imagery.image.width; + texture.height = imagery.image.height; + return texture; + }); + + spyOn(ImageryLayer.prototype, '_finalizeReprojectTexture'); + + spyOn(Texture, 'create').and.callFake(function(options) { + var result = clone(options); + result.destroy = function() {}; + return result; + }); + }; + return TerrainTileProcessor; }); diff --git a/Specs/createTileKey.js b/Specs/createTileKey.js index f20dfaa944a..a8892f96b40 100644 --- a/Specs/createTileKey.js +++ b/Specs/createTileKey.js @@ -17,7 +17,7 @@ define([ y = tile.y; level = tile.level; } - return [xOrTile, y, level].join(','); + return 'L' + level + 'X' + xOrTile + 'Y' + y; } return createTileKey; From 4e3fc5f391157ba532772cc54ae8b497e156717c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Jan 2019 18:40:09 +1100 Subject: [PATCH 075/131] Fix continue processing logic. --- Specs/Scene/QuadtreePrimitiveSpec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index 7d2d069b703..1bdee5e932d 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -188,8 +188,6 @@ defineSuite([ }); it('skips levels when tiles are known to be available', function() { - console.log('start'); - // Mark all tiles through level 2 as available. var tiles = []; rootTiles.forEach(function(tile) { @@ -246,7 +244,9 @@ defineSuite([ // Process until the lookAtTile is rendered. That tile's parent (level 1) // should never be rendered along the way. expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent); - return quadtree._tilesToRender.indexOf(lookAtTile) >= 0; + var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0; + var continueProcessing = !lookAtTileRendered; + return continueProcessing; }); }); }); From d6d65a847895a9232d85927b383ee79ab407c4d7 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Jan 2019 19:19:42 +1100 Subject: [PATCH 076/131] Simplify test. --- Specs/Scene/QuadtreePrimitiveSpec.js | 42 +++++++++++++++------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index 1bdee5e932d..25bf4649b52 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -220,33 +220,22 @@ defineSuite([ }); }); - quadtree.preloadAncestors = false; - - // Look down at the center of a level 2 tile. + // Look down at the center of a level 2 tile from a distance that will refine to it. var lookAtTile = rootTiles[0].southwestChild.northeastChild; - expect(lookAtTile.level).toBe(2); - expect(lookAtTile.parent.level).toBe(1); - - var lookAtCenter = Ellipsoid.WGS84.cartographicToCartesian(Rectangle.center(lookAtTile.rectangle)); - camera.lookAt(lookAtCenter, new Cartesian3(0.0, 0.0, 100.0)); - - var originalComputeDistanceToTile = quadtree.tileProvider.computeDistanceToTile; - spyOn(quadtree.tileProvider, 'computeDistanceToTile').and.callFake(function(tile, frameState) { - var result = originalComputeDistanceToTile.apply(this, arguments); - if (tile.level === 2) { - // Level 2 is far away so it will definitely meet SSE requirement. - return 9999999999999.0; - } - return result; - }); + setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level); return process(quadtree, function() { // Process until the lookAtTile is rendered. That tile's parent (level 1) - // should never be rendered along the way. + // should not be rendered along the way. expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent); var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0; var continueProcessing = !lookAtTileRendered; return continueProcessing; + }).then(function() { + // The lookAtTile should be a real tile, not a fill. + expect(quadtree._tilesToRender).toContain(lookAtTile); + expect(lookAtTile.data.fill).toBeUndefined(); + expect(lookAtTile.data.vertexArray).toBeDefined(); }); }); }); @@ -763,4 +752,19 @@ defineSuite([ }); }, 'WebGL'); + // Sets the camera to look at a given cartographic position from a distance + // that will produce a screen-space error at that position that will refine to + // a given tile level and no further. + function setCameraPosition(quadtree, frameState, position, level) { + var camera = frameState.camera; + var geometricError = quadtree.tileProvider.getLevelMaximumGeometricError(level); + var sse = quadtree.maximumScreenSpaceError * 0.8; + var sseDenominator = camera.frustum.sseDenominator; + var height = frameState.context.drawingBufferHeight; + + var distance = (geometricError * height) / (sse * sseDenominator); + var cartesian = Ellipsoid.WGS84.cartographicToCartesian(position); + camera.lookAt(cartesian, new Cartesian3(0.0, 0.0, distance)); + } + }); From 72514d2545b430b2fa7f8f8a3ed7702fc64e79bc Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Jan 2019 23:23:37 +1100 Subject: [PATCH 077/131] New spec, fix failures, etc. --- Source/Scene/GlobeSurfaceTile.js | 47 ++- Source/Scene/TerrainFillMesh.js | 37 +-- .../CesiumInspectorViewModel.js | 2 +- Specs/MockTerrainProvider.js | 5 - Specs/Scene/QuadtreePrimitiveSpec.js | 310 ++++++++++++++++-- 5 files changed, 310 insertions(+), 91 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 311b8a63900..83a9eae19fb 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -234,33 +234,10 @@ define([ }; GlobeSurfaceTile.prototype.freeVertexArray = function() { - var indexBuffer; - - if (defined(this.vertexArray)) { - indexBuffer = this.vertexArray.indexBuffer; - - this.vertexArray = this.vertexArray.destroy(); - - if (defined(indexBuffer) && !indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { - --indexBuffer.referenceCount; - if (indexBuffer.referenceCount === 0) { - indexBuffer.destroy(); - } - } - } - - if (defined(this.wireframeVertexArray)) { - indexBuffer = this.wireframeVertexArray.indexBuffer; - - this.wireframeVertexArray = this.wireframeVertexArray.destroy(); - - if (defined(indexBuffer) && !indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { - --indexBuffer.referenceCount; - if (indexBuffer.referenceCount === 0) { - indexBuffer.destroy(); - } - } - } + GlobeSurfaceTile._freeVertexArray(this.vertexArray); + this.vertexArray = undefined; + GlobeSurfaceTile._freeVertexArray(this.wireframeVertexArray); + this.wireframeVertexArray = undefined; }; GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, terrainOnly) { @@ -573,6 +550,22 @@ define([ }); }; + GlobeSurfaceTile._freeVertexArray = function(vertexArray) { + if (defined(vertexArray)) { + var indexBuffer = vertexArray.indexBuffer; + + vertexArray.destroy(); + + if (defined(indexBuffer) && !indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) { + --indexBuffer.referenceCount; + if (indexBuffer.referenceCount === 0) { + indexBuffer.destroy(); + } + } + } + + }; + function createResources(surfaceTile, context, terrainProvider, x, y, level) { surfaceTile.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, surfaceTile.mesh); surfaceTile.terrainState = TerrainState.READY; diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 7df2f53491b..58b05c034ec 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -8,7 +8,6 @@ define([ '../Core/defined', '../Core/Math', '../Core/DeveloperError', - '../Core/IndexDatatype', '../Core/OrientedBoundingBox', '../Core/Queue', '../Core/Rectangle', @@ -16,9 +15,7 @@ define([ '../Core/TerrainEncoding', '../Core/TerrainMesh', '../Core/WebMercatorProjection', - '../Renderer/Buffer', - '../Renderer/BufferUsage', - '../Renderer/VertexArray', + './GlobeSurfaceTile', './ImageryState', './TileSelectionResult' ], function( @@ -31,7 +28,6 @@ define([ defined, CesiumMath, DeveloperError, - IndexDatatype, OrientedBoundingBox, Queue, Rectangle, @@ -39,9 +35,7 @@ define([ TerrainEncoding, TerrainMesh, WebMercatorProjection, - Buffer, - BufferUsage, - VertexArray, + GlobeSurfaceTile, ImageryState, TileSelectionResult) { 'use strict'; @@ -588,31 +582,8 @@ define([ var context = frameState.context; - if (fill.vertexArray !== undefined) { - fill.vertexArray.destroy(); - fill.vertexArray = undefined; - } - - var buffer = Buffer.createVertexBuffer({ - context : context, - typedArray : typedArray, - usage : BufferUsage.STATIC_DRAW - }); - var attributes = mesh.encoding.getAttributes(buffer); - - var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; - var indexBuffer = Buffer.createIndexBuffer({ - context : context, - typedArray : mesh.indices, - usage : BufferUsage.STATIC_DRAW, - indexDatatype : indexDatatype - }); - - fill.vertexArray = new VertexArray({ - context : context, - attributes : attributes, - indexBuffer : indexBuffer - }); + GlobeSurfaceTile._freeVertexArray(fill.vertexArray); + fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, mesh); var tileImageryCollection = surfaceTile.imagery; diff --git a/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js b/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js index cceb9cb1d66..61b4cf81639 100644 --- a/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js +++ b/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js @@ -471,7 +471,7 @@ define([ this._doFilterTile = createCommand(function() { if (!that.filterTile) { - //that.suspendUpdates = false; + that.suspendUpdates = false; } else { that.suspendUpdates = true; diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js index 236f87202ac..0432dcc168c 100644 --- a/Specs/MockTerrainProvider.js +++ b/Specs/MockTerrainProvider.js @@ -35,7 +35,6 @@ define([ } MockTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { - console.log('request ' + createTileKey(x, y, level)); var willSucceed = this._requestTileGeometryWillSucceed[createTileKey(x, y, level)]; if (willSucceed === undefined) { return undefined; // defer by default @@ -193,8 +192,6 @@ define([ var originalUpsample = terrainData.upsample; terrainData.upsample = function(tilingScheme, thisX, thisY, thisLevel, descendantX, descendantY) { - console.log('upsample ' + createTileKey(descendantX, descendantY, thisLevel + 1)); - var willSucceed = terrainProvider._upsampleWillSucceed[createTileKey(descendantX, descendantY, thisLevel + 1)]; if (willSucceed === undefined) { return undefined; // defer by default @@ -211,8 +208,6 @@ define([ var originalCreateMesh = terrainData.createMesh; terrainData.createMesh = function(tilingScheme, x, y, level) { - console.log('createMesh ' + createTileKey(x, y, level)); - var willSucceed = terrainProvider._createMeshWillSucceed[createTileKey(x, y, level)]; if (willSucceed === undefined) { return undefined; // defer by default diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index 25bf4649b52..33712132ee1 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -2,6 +2,7 @@ defineSuite([ 'Scene/QuadtreePrimitive', 'Core/Cartesian3', 'Core/Cartographic', + 'Core/defined', 'Core/defineProperties', 'Core/Ellipsoid', 'Core/EventHelper', @@ -24,6 +25,7 @@ defineSuite([ QuadtreePrimitive, Cartesian3, Cartographic, + defined, defineProperties, Ellipsoid, EventHelper, @@ -57,7 +59,6 @@ defineSuite([ var rootTiles; beforeEach(function() { - console.log('beforeEach - start'); scene = { mapProjection: new GeographicProjection(), drawingBufferWidth: 1000, @@ -105,7 +106,6 @@ defineSuite([ rootTiles = quadtree._levelZeroTiles; processor.mockWebGL(); - console.log('beforeEach - end'); }); function process(quadtreePrimitive, callback) { @@ -113,7 +113,6 @@ defineSuite([ function next() { ++frameState.frameNumber; - console.log('Frame ' + frameState.frameNumber); quadtree.beginFrame(frameState); quadtree.render(frameState); quadtree.endFrame(frameState); @@ -130,12 +129,22 @@ defineSuite([ return deferred.promise; } + it('must be constructed with a tileProvider', function() { + expect(function() { + return new QuadtreePrimitive(); + }).toThrowDeveloperError(); + + expect(function() { + return new QuadtreePrimitive({}); + }).toThrowDeveloperError(); + }); + it('selects nothing when the root tiles are not yet ready', function() { quadtree.render(frameState); expect(quadtree._tilesToRender.length).toBe(0); }); - it('selects root tiles once they\'re ready', function() { + it('selects root tiles once they are ready', function() { mockTerrain .requestTileGeometryWillSucceed(rootTiles[0]) .requestTileGeometryWillSucceed(rootTiles[1]) @@ -153,7 +162,7 @@ defineSuite([ }); }); - it('selects deeper tiles once they\'re renderable', function() { + it('selects deeper tiles once they are renderable', function() { mockTerrain .requestTileGeometryWillSucceed(rootTiles[0]) .requestTileGeometryWillSucceed(rootTiles[1]) @@ -187,12 +196,9 @@ defineSuite([ }); }); - it('skips levels when tiles are known to be available', function() { + it('skips loading levels when tiles are known to be available', function() { // Mark all tiles through level 2 as available. - var tiles = []; rootTiles.forEach(function(tile) { - tiles.push(tile); - // level 0 tile mockTerrain .willBeAvailable(tile) @@ -200,8 +206,6 @@ defineSuite([ .createMeshWillSucceed(tile); tile.children.forEach(function(tile) { - tiles.push(tile); - // level 1 tile mockTerrain .willBeAvailable(tile) @@ -209,8 +213,6 @@ defineSuite([ .createMeshWillSucceed(tile); tile.children.forEach(function(tile) { - tiles.push(tile); - // level 2 tile mockTerrain .willBeAvailable(tile) @@ -220,10 +222,14 @@ defineSuite([ }); }); + quadtree.preloadAncestors = false; + // Look down at the center of a level 2 tile from a distance that will refine to it. var lookAtTile = rootTiles[0].southwestChild.northeastChild; setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level); + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + return process(quadtree, function() { // Process until the lookAtTile is rendered. That tile's parent (level 1) // should not be rendered along the way. @@ -236,11 +242,276 @@ defineSuite([ expect(quadtree._tilesToRender).toContain(lookAtTile); expect(lookAtTile.data.fill).toBeUndefined(); expect(lookAtTile.data.vertexArray).toBeDefined(); + + // The parent of the lookAtTile should not have been requested. + var parent = lookAtTile.parent; + mockTerrain.requestTileGeometry.calls.allArgs().forEach(function(call) { + expect(call.slice(0, 3)).not.toEqual([parent.x, parent.y, parent.level]); + }); + }); + }); + + it('does not skip loading levels if availability is unknown', function() { + // Mark all tiles through level 2 as available. + rootTiles.forEach(function(tile) { + // level 0 tile + mockTerrain + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 1 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 2 tile + mockTerrain + .willBeUnknownAvailability(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + }); + }); + }); + + quadtree.preloadAncestors = false; + + // Look down at the center of a level 2 tile from a distance that will refine to it. + var lookAtTile = rootTiles[0].southwestChild.northeastChild; + setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level); + + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + + return process(quadtree, function() { + // Process until the lookAtTile is rendered. That tile's parent (level 1) + // should not be rendered along the way, but it will be loaded. + expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent); + var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0; + var continueProcessing = !lookAtTileRendered; + return continueProcessing; + }).then(function() { + // The lookAtTile should be a real tile, not a fill. + expect(quadtree._tilesToRender).toContain(lookAtTile); + expect(lookAtTile.data.fill).toBeUndefined(); + expect(lookAtTile.data.vertexArray).toBeDefined(); + + // The parent of the lookAtTile should have been requested before the lookAtTile itself. + var parent = lookAtTile.parent; + var allArgs = mockTerrain.requestTileGeometry.calls.allArgs(); + var parentArgsIndex = allArgs.indexOf(allArgs.filter(function(call) { + return call[0] === parent.x && call[1] === parent.y && call[2] === parent.level; + })[0]); + var lookAtArgsIndex = allArgs.indexOf(allArgs.filter(function(call) { + return call[0] === lookAtTile.x && call[1] === lookAtTile.y && call[2] === lookAtTile.level; + })[0]); + expect(parentArgsIndex).toBeLessThan(lookAtArgsIndex); + }); + }); + + it('loads and renders intermediate tiles according to loadingDescendantLimit', function() { + // Mark all tiles through level 2 as available. + rootTiles.forEach(function(tile) { + // level 0 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 1 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 2 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + }); + }); + }); + + quadtree.preloadAncestors = false; + quadtree.loadingDescendantLimit = 1; + + // Look down at the center of a level 2 tile from a distance that will refine to it. + var lookAtTile = rootTiles[0].southwestChild.northeastChild; + setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level); + + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + + return process(quadtree, function() { + // First the lookAtTile's parent should be rendered. + var lookAtTileParentRendered = quadtree._tilesToRender.indexOf(lookAtTile.parent) >= 0; + var continueProcessing = !lookAtTileParentRendered; + return continueProcessing; + }).then(function() { + // The lookAtTile's parent should be a real tile, not a fill. + expect(quadtree._tilesToRender).toContain(lookAtTile.parent); + expect(lookAtTile.parent.data.fill).toBeUndefined(); + expect(lookAtTile.parent.data.vertexArray).toBeDefined(); + + return process(quadtree, function() { + // Then the lookAtTile should be rendered. + var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0; + var continueProcessing = !lookAtTileRendered; + return continueProcessing; + }); + }).then(function() { + // The lookAtTile should be a real tile, not a fill. + expect(quadtree._tilesToRender).toContain(lookAtTile); + expect(lookAtTile.data.fill).toBeUndefined(); + expect(lookAtTile.data.vertexArray).toBeDefined(); + }); + }); + + it('continues rendering more detailed tiles when camera zooms out and an appropriate ancestor is not yet renderable', function() { + // Mark all tiles through level 2 as available. + rootTiles.forEach(function(tile) { + // level 0 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 1 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 2 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + }); + }); + }); + + quadtree.preloadAncestors = false; + + // Look down at the center of a level 2 tile from a distance that will refine to it. + var lookAtTile = rootTiles[0].southwestChild.northeastChild; + setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level); + + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + + return process(quadtree, function() { + // Process until the lookAtTile is rendered. That tile's parent (level 1) + // should not be rendered along the way. + expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent); + var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0; + var continueProcessing = !lookAtTileRendered; + return continueProcessing; + }).then(function() { + // Zoom out so the parent tile no longer needs to refine to meet SSE. + setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.parent.level); + + // Select new tiles + quadtree.beginFrame(frameState); + quadtree.render(frameState); + quadtree.endFrame(frameState); + + // The lookAtTile should still be rendered, not it's parent. + expect(quadtree._tilesToRender).toContain(lookAtTile); + expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent); + + return process(quadtree, function() { + // Eventually the parent should be rendered instead. + var parentRendered = quadtree._tilesToRender.indexOf(lookAtTile.parent) >= 0; + var continueProcessing = !parentRendered; + return continueProcessing; + }); + }).then(function() { + expect(quadtree._tilesToRender).not.toContain(lookAtTile); + expect(quadtree._tilesToRender).toContain(lookAtTile.parent); + }); + }); + + it('renders a fill for a newly-visible tile', function() { + // Mark all tiles through level 2 as available. + rootTiles.forEach(function(tile) { + // level 0 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 1 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + + tile.children.forEach(function(tile) { + // level 2 tile + mockTerrain + .willBeAvailable(tile) + .requestTileGeometryWillSucceed(tile) + .createMeshWillSucceed(tile); + }); + }); + }); + + quadtree.preloadAncestors = false; + + var visibleTile = rootTiles[0].southwestChild.northeastChild; + var notVisibleTile = rootTiles[0].southwestChild.northwestChild; + + frameState.cullingVolume.computeVisibility.and.callFake(function(boundingVolume) { + if (!defined(visibleTile.data)) { + return Intersect.INTERSECTING; + } + + if (boundingVolume === visibleTile.data.orientedBoundingBox) { + return Intersect.INTERSECTING; + } else if (boundingVolume === notVisibleTile.data.orientedBoundingBox) { + return Intersect.OUTSIDE; + } + return Intersect.INTERSECTING; + }); + + // Look down at the center of the edge between the visible and non-visible tiles. + var middle = new Cartographic(visibleTile.rectangle.west, (visibleTile.rectangle.south + visibleTile.rectangle.north) * 0.5, 0.0); + setCameraPosition(quadtree, frameState, middle, visibleTile.level); + + spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); + + return process(quadtree, function() { + // Process until the visibleTile is rendered. + var visibleTileRendered = quadtree._tilesToRender.indexOf(visibleTile) >= 0; + var continueProcessing = !visibleTileRendered; + return continueProcessing; + }).then(function() { + expect(quadtree._tilesToRender).not.toContain(notVisibleTile); + + // Now treat the not-visible-tile as visible. + frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING); + + // Select new tiles + quadtree.beginFrame(frameState); + quadtree.render(frameState); + quadtree.endFrame(frameState); + + // The notVisibleTile should be rendered as a fill. + expect(quadtree._tilesToRender).toContain(visibleTile); + expect(quadtree._tilesToRender).toContain(notVisibleTile); + expect(notVisibleTile.data.fill).toBeDefined(); + expect(notVisibleTile.data.vertexArray).toBeUndefined(); }); }); }); - describe('old', function() { + describe('with mock tile provider', function() { var scene; beforeAll(function() { @@ -252,16 +523,6 @@ defineSuite([ scene.destroyForSpecs(); }); - it('must be constructed with a tileProvider', function() { - expect(function() { - return new QuadtreePrimitive(); - }).toThrowDeveloperError(); - - expect(function() { - return new QuadtreePrimitive({}); - }).toThrowDeveloperError(); - }); - function createSpyTileProvider() { var result = jasmine.createSpyObj('tileProvider', [ 'getQuadtree', 'setQuadtree', 'getReady', 'getTilingScheme', 'getErrorEvent', @@ -766,5 +1027,4 @@ defineSuite([ var cartesian = Ellipsoid.WGS84.cartographicToCartesian(position); camera.lookAt(cartesian, new Cartesian3(0.0, 0.0, distance)); } - }); From 8cfda704c1484b54bbd15688f6614bb85ef69520 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Jan 2019 23:30:12 +1100 Subject: [PATCH 078/131] Fix test failure. --- Specs/Scene/GlobeSurfaceTileProviderSpec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Specs/Scene/GlobeSurfaceTileProviderSpec.js b/Specs/Scene/GlobeSurfaceTileProviderSpec.js index f398050cfb1..0bf5746fbb1 100644 --- a/Specs/Scene/GlobeSurfaceTileProviderSpec.js +++ b/Specs/Scene/GlobeSurfaceTileProviderSpec.js @@ -304,7 +304,7 @@ defineSuite([ // Verify that each tile has 2 imagery objects and no loaded callbacks forEachRenderedTile(scene.globe._surface, 1, undefined, function(tile) { expect(tile.data.imagery.length).toBe(2); - expect(Object.keys(tile._loadedCallbacks).length).toBe(1); + expect(Object.keys(tile._loadedCallbacks).length).toBe(0); }); // Reload each layer @@ -319,14 +319,14 @@ defineSuite([ // and also has 2 callbacks so the old imagery will be removed once loaded. forEachRenderedTile(scene.globe._surface, 1, undefined, function(tile) { expect(tile.data.imagery.length).toBe(4); - expect(Object.keys(tile._loadedCallbacks).length).toBe(3); + expect(Object.keys(tile._loadedCallbacks).length).toBe(2); }); return updateUntilDone(scene.globe).then(function() { // Verify the old imagery was removed and the callbacks are no longer there forEachRenderedTile(scene.globe._surface, 1, undefined, function(tile) { expect(tile.data.imagery.length).toBe(2); - expect(Object.keys(tile._loadedCallbacks).length).toBe(1); + expect(Object.keys(tile._loadedCallbacks).length).toBe(0); }); }); }); From 6456e37341d675b30fca4c0a68b7ecb8c7f315bd Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Jan 2019 21:02:01 +1100 Subject: [PATCH 079/131] Start adding specs for TerrainFillMesh. --- Source/Renderer/Texture.js | 5 + Source/Scene/GlobeSurfaceTileProvider.js | 4 +- Source/Scene/TerrainFillMesh.js | 20 ++-- Specs/Scene/TerrainFillMeshSpec.js | 129 +++++++++++++++++++++++ 4 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 Specs/Scene/TerrainFillMeshSpec.js diff --git a/Source/Renderer/Texture.js b/Source/Renderer/Texture.js index 76840327af3..9673494db3b 100644 --- a/Source/Renderer/Texture.js +++ b/Source/Renderer/Texture.js @@ -264,6 +264,11 @@ define([ this.sampler = defined(options.sampler) ? options.sampler : new Sampler(); } + /** + * This function is identical to using the Texture constructor except that it can be + * replaced with a mock/spy in tests. + * @private + */ Texture.create = function(options) { return new Texture(options); }; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index e6e20fb41af..72dbea4e9e0 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1441,9 +1441,7 @@ define([ // No fill was created for this tile, probably because this tile is not connected to // any renderable tiles. So create a simple tile in the middle of the tile's possible // height range. - surfaceTile.fill = new TerrainFillMesh(); - surfaceTile.fill.tile = tile; - surfaceTile.fill.changedThisFrame = true; + surfaceTile.fill = new TerrainFillMesh(tile); } surfaceTile.fill.update(tileProvider, frameState); } diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 58b05c034ec..9b2e23f82b0 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -40,8 +40,8 @@ define([ TileSelectionResult) { 'use strict'; - function TerrainFillMesh() { - this.tile = undefined; + function TerrainFillMesh(tile) { + this.tile = tile; this.frameLastUpdated = undefined; this.westMeshes = []; // north to south (CCW) this.westTiles = []; @@ -59,7 +59,7 @@ define([ this.northwestTile = undefined; this.northeastMesh = undefined; this.northeastTile = undefined; - this.changedThisFrame = false; + this.changedThisFrame = true; this.visitedFrame = undefined; this.mesh = undefined; this.vertexArray = undefined; @@ -73,7 +73,8 @@ define([ }; TerrainFillMesh.prototype.destroy = function() { - this.vertexArray = this.vertexArray && this.vertexArray.destroy(); + GlobeSurfaceTile._freeVertexArray(this.vertexArray); + this.vertexArray = undefined; return undefined; }; @@ -226,8 +227,7 @@ define([ var destinationSurfaceTile = destinationTile.data; if (destinationSurfaceTile.fill === undefined) { - destinationSurfaceTile.fill = new TerrainFillMesh(); - destinationSurfaceTile.fill.tile = destinationTile; + destinationSurfaceTile.fill = new TerrainFillMesh(destinationTile); } if (destinationSurfaceTile.fill.visitedFrame !== frameNumber) { @@ -828,8 +828,12 @@ define([ // No heights available whatsoever, so use the average of this tile's minimum and maximum height. var surfaceTile = terrainFillMesh.tile.data; var tileBoundingRegion = surfaceTile.tileBoundingRegion; - var minimumHeight = tileBoundingRegion.minimumHeight; - var maximumHeight = tileBoundingRegion.maximumHeight; + var minimumHeight = 0.0; + var maximumHeight = 0.0; + if (defined(tileBoundingRegion)) { + minimumHeight = tileBoundingRegion.minimumHeight; + maximumHeight = tileBoundingRegion.maximumHeight; + } height = (minimumHeight + maximumHeight) * 0.5; } diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js new file mode 100644 index 00000000000..70143c84b1e --- /dev/null +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -0,0 +1,129 @@ +defineSuite([ + 'Scene/TerrainFillMesh', + 'Core/GeographicProjection', + 'Core/Intersect', + 'Scene/Camera', + 'Scene/GlobeSurfaceTileProvider', + 'Scene/ImageryLayerCollection', + 'Scene/QuadtreePrimitive', + 'Scene/SceneMode', + 'Scene/TileBoundingRegion', + '../MockTerrainProvider', + '../TerrainTileProcessor' + ], function( + TerrainFillMesh, + GeographicProjection, + Intersect, + Camera, + GlobeSurfaceTileProvider, + ImageryLayerCollection, + QuadtreePrimitive, + SceneMode, + TileBoundingRegion, + MockTerrainProvider, + TerrainTileProcessor) { + 'use strict'; + + describe('update', function() { + var processor; + var scene; + var camera; + var frameState; + var imageryLayerCollection; + var surfaceShaderSet; + var mockTerrain; + var tileProvider; + var quadtree; + var rootTiles; + + beforeEach(function() { + scene = { + mapProjection: new GeographicProjection(), + drawingBufferWidth: 1000, + drawingBufferHeight: 1000 + }; + + camera = new Camera(scene); + + frameState = { + frameNumber: 0, + passes: { + render: true + }, + camera: camera, + fog: { + enabled: false + }, + context: { + drawingBufferWidth: scene.drawingBufferWidth, + drawingBufferHeight: scene.drawingBufferHeight + }, + mode: SceneMode.SCENE3D, + commandList: [], + cullingVolume: jasmine.createSpyObj('CullingVolume', ['computeVisibility']), + afterRender: [] + }; + + frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING); + + imageryLayerCollection = new ImageryLayerCollection(); + surfaceShaderSet = jasmine.createSpyObj('SurfaceShaderSet', ['getShaderProgram']); + mockTerrain = new MockTerrainProvider(); + tileProvider = new GlobeSurfaceTileProvider({ + terrainProvider: mockTerrain, + imageryLayers: imageryLayerCollection, + surfaceShaderSet: surfaceShaderSet + }); + quadtree = new QuadtreePrimitive({ + tileProvider: tileProvider + }); + + processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection); + processor.mockWebGL(); + + quadtree.render(frameState); + rootTiles = quadtree._levelZeroTiles; + }); + + it('puts a middle height at the four corners and center when there are no adjacent tiles', function() { + var tile = rootTiles[0].southwestChild; + return processor.process([tile]).then(function() { + tile.data.tileBoundingRegion = new TileBoundingRegion({ + rectangle: tile.rectangle, + minimumHeight: 1.0, + maximumHeight: 3.0, + computeBoundingVolumes: false + }); + + var fill = tile.data.fill = new TerrainFillMesh(tile); + fill.update(tileProvider, frameState); + + var encoding = fill.mesh.encoding; + var vertices = fill.mesh.vertices; + expect(vertices.length / encoding.getStride()).toBe(5); + expect(encoding.decodeHeight(vertices, 0)).toBe(2.0); + expect(encoding.decodeHeight(vertices, 1)).toBe(2.0); + expect(encoding.decodeHeight(vertices, 2)).toBe(2.0); + expect(encoding.decodeHeight(vertices, 3)).toBe(2.0); + expect(encoding.decodeHeight(vertices, 4)).toBe(2.0); + }); + }); + + it('puts zero height at the four corners and center when there are no adjacent tiles and no bounding region', function() { + var tile = rootTiles[0].southwestChild; + return processor.process([tile]).then(function() { + var fill = tile.data.fill = new TerrainFillMesh(tile); + fill.update(tileProvider, frameState); + + var encoding = fill.mesh.encoding; + var vertices = fill.mesh.vertices; + expect(vertices.length / encoding.getStride()).toBe(5); + expect(encoding.decodeHeight(vertices, 0)).toBe(0.0); + expect(encoding.decodeHeight(vertices, 1)).toBe(0.0); + expect(encoding.decodeHeight(vertices, 2)).toBe(0.0); + expect(encoding.decodeHeight(vertices, 3)).toBe(0.0); + expect(encoding.decodeHeight(vertices, 4)).toBe(0.0); + }); + }); + }); +}); From 0a0426c6b5991a68caa12550bfdda5f49644b7ea Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Jan 2019 22:35:22 +1100 Subject: [PATCH 080/131] More TerrainFillMesh tests. --- Source/Core/HeightmapTerrainData.js | 2 +- Specs/Scene/TerrainFillMeshSpec.js | 213 ++++++++++++++++++++++++---- 2 files changed, 190 insertions(+), 25 deletions(-) diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index 2456a05c733..fd581897f17 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -30,7 +30,7 @@ define([ /** * Terrain data for a single tile where the terrain data is represented as a heightmap. A heightmap - * is a rectangular array of heights in row-major order from south to north and west to east. + * is a rectangular array of heights in row-major order from north to south and west to east. * * @alias HeightmapTerrainData * @constructor diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 70143c84b1e..2bb0a4a27e9 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -1,25 +1,35 @@ defineSuite([ 'Scene/TerrainFillMesh', + 'Core/Cartesian2', + 'Core/Cartesian3', 'Core/GeographicProjection', + 'Core/HeightmapTerrainData', 'Core/Intersect', + 'Core/Math', 'Scene/Camera', 'Scene/GlobeSurfaceTileProvider', 'Scene/ImageryLayerCollection', 'Scene/QuadtreePrimitive', 'Scene/SceneMode', 'Scene/TileBoundingRegion', + 'ThirdParty/when', '../MockTerrainProvider', '../TerrainTileProcessor' ], function( TerrainFillMesh, + Cartesian2, + Cartesian3, GeographicProjection, + HeightmapTerrainData, Intersect, + CesiumMath, Camera, GlobeSurfaceTileProvider, ImageryLayerCollection, QuadtreePrimitive, SceneMode, TileBoundingRegion, + when, MockTerrainProvider, TerrainTileProcessor) { 'use strict'; @@ -36,6 +46,16 @@ defineSuite([ var quadtree; var rootTiles; + var center; + var west; + var south; + var east; + var north; + var southwest; + var southeast; + var northwest; + var northeast; + beforeEach(function() { scene = { mapProjection: new GeographicProjection(), @@ -83,47 +103,192 @@ defineSuite([ quadtree.render(frameState); rootTiles = quadtree._levelZeroTiles; + + center = rootTiles[0].northeastChild.southwestChild; + west = center.findTileToWest(rootTiles); + south = center.findTileToSouth(rootTiles); + east = center.findTileToEast(rootTiles); + north = center.findTileToNorth(rootTiles); + southwest = west.findTileToSouth(rootTiles); + southeast = east.findTileToSouth(rootTiles); + northwest = west.findTileToNorth(rootTiles); + northeast = east.findTileToNorth(rootTiles); + + spyOn(mockTerrain, 'requestTileGeometry').and.callFake(function(x, y, level) { + var buffer = new Float32Array(4); + if (x === center.x && y === center.y) { + return undefined; + } else if (x === west.x && y === west.y) { + buffer[0] = 3.0; + buffer[1] = 4.0; + buffer[2] = 1.0; + buffer[3] = 2.0; + } else if (x === south.x && y === south.y) { + buffer[0] = 2.0; + buffer[1] = 7.0; + buffer[2] = 5.0; + buffer[3] = 6.0; + } else if (x === east.x && y === east.y) { + buffer[0] = 9.0; + buffer[1] = 10.0; + buffer[2] = 7.0; + buffer[3] = 8.0; + } else if (x === north.x && y === north.y) { + buffer[0] = 11.0; + buffer[1] = 12.0; + buffer[2] = 4.0; + buffer[3] = 9.0; + } else if (x === southwest.x && y === southwest.y) { + buffer[0] = 1.0; + buffer[1] = 2.0; + buffer[2] = 13.0; + buffer[3] = 5.0; + } else if (x === southeast.x && y === southeast.y) { + buffer[0] = 7.0; + buffer[1] = 8.0; + buffer[2] = 6.0; + buffer[3] = 14.0; + } else if (x === northwest.x && y === northwest.y) { + buffer[0] = 15.0; + buffer[1] = 11.0; + buffer[2] = 3.0; + buffer[3] = 4.0; + } else if (x === northeast.x && y === northeast.y) { + buffer[0] = 12.0; + buffer[1] = 16.0; + buffer[2] = 9.0; + buffer[3] = 10.0; + } + + var terrainData = new HeightmapTerrainData({ + width: 2, + height: 2, + buffer: buffer, + createdByUpsampling: false + }); + return when(terrainData); + }); }); it('puts a middle height at the four corners and center when there are no adjacent tiles', function() { - var tile = rootTiles[0].southwestChild; - return processor.process([tile]).then(function() { - tile.data.tileBoundingRegion = new TileBoundingRegion({ - rectangle: tile.rectangle, + return processor.process([center]).then(function() { + center.data.tileBoundingRegion = new TileBoundingRegion({ + rectangle: center.rectangle, minimumHeight: 1.0, maximumHeight: 3.0, computeBoundingVolumes: false }); - var fill = tile.data.fill = new TerrainFillMesh(tile); + var fill = center.data.fill = new TerrainFillMesh(center); fill.update(tileProvider, frameState); - var encoding = fill.mesh.encoding; - var vertices = fill.mesh.vertices; - expect(vertices.length / encoding.getStride()).toBe(5); - expect(encoding.decodeHeight(vertices, 0)).toBe(2.0); - expect(encoding.decodeHeight(vertices, 1)).toBe(2.0); - expect(encoding.decodeHeight(vertices, 2)).toBe(2.0); - expect(encoding.decodeHeight(vertices, 3)).toBe(2.0); - expect(encoding.decodeHeight(vertices, 4)).toBe(2.0); + expectVertexCount(fill, 5); + expectVertex(fill, 0.0, 0.0, 2.0); + expectVertex(fill, 0.0, 1.0, 2.0); + expectVertex(fill, 1.0, 0.0, 2.0); + expectVertex(fill, 1.0, 1.0, 2.0); + expectVertex(fill, 0.5, 0.5, 2.0); }); }); it('puts zero height at the four corners and center when there are no adjacent tiles and no bounding region', function() { - var tile = rootTiles[0].southwestChild; - return processor.process([tile]).then(function() { - var fill = tile.data.fill = new TerrainFillMesh(tile); + return processor.process([center]).then(function() { + var fill = center.data.fill = new TerrainFillMesh(center); + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 5); + expectVertex(fill, 0.0, 0.0, 0.0); + expectVertex(fill, 0.0, 1.0, 0.0); + expectVertex(fill, 1.0, 0.0, 0.0); + expectVertex(fill, 1.0, 1.0, 0.0); + expectVertex(fill, 0.5, 0.5, 0.0); + }); + }); + + it('uses adjacent edge heights', function() { + return processor.process([center, west, south, east, north]).then(function() { + var fill = center.data.fill = new TerrainFillMesh(center); + + fill.westTiles.push(west); + fill.westMeshes.push(west.data.mesh); + fill.southTiles.push(south); + fill.southMeshes.push(south.data.mesh); + fill.eastTiles.push(east); + fill.eastMeshes.push(east.data.mesh); + fill.northTiles.push(north); + fill.northMeshes.push(north.data.mesh); + fill.update(tileProvider, frameState); - var encoding = fill.mesh.encoding; - var vertices = fill.mesh.vertices; - expect(vertices.length / encoding.getStride()).toBe(5); - expect(encoding.decodeHeight(vertices, 0)).toBe(0.0); - expect(encoding.decodeHeight(vertices, 1)).toBe(0.0); - expect(encoding.decodeHeight(vertices, 2)).toBe(0.0); - expect(encoding.decodeHeight(vertices, 3)).toBe(0.0); - expect(encoding.decodeHeight(vertices, 4)).toBe(0.0); + expectVertexCount(fill, 5); + expectVertex(fill, 0.0, 0.0, 2.0); + expectVertex(fill, 1.0, 0.0, 7.0); + expectVertex(fill, 0.0, 1.0, 4.0); + expectVertex(fill, 1.0, 1.0, 9.0); + expectVertex(fill, 0.5, 0.5, (9.0 + 2.0) / 2.0); + }); + }); + + it('uses adjacent corner heights if adjacent edges are not available', function() { + return processor.process([center, southwest, southeast, northwest, northeast]).then(function() { + var fill = center.data.fill = new TerrainFillMesh(center); + + fill.southwestTile = southwest; + fill.southwestMesh = southwest.data.mesh; + fill.southeastTile = southeast; + fill.southeastMesh = southeast.data.mesh; + fill.northwestTile = northwest; + fill.northwestMesh = northwest.data.mesh; + fill.northeastTile = northeast; + fill.northeastMesh = northeast.data.mesh; + + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 5); + expectVertex(fill, 0.0, 0.0, 2.0); + expectVertex(fill, 1.0, 0.0, 7.0); + expectVertex(fill, 0.0, 1.0, 4.0); + expectVertex(fill, 1.0, 1.0, 9.0); + expectVertex(fill, 0.5, 0.5, (9.0 + 2.0) / 2.0); }); }); }); + + var textureCoordinateScratch = new Cartesian2(); + var positionScratch = new Cartesian3(); + var expectedPositionScratch = new Cartesian3(); + + function expectVertex(fill, u, v, height) { + var mesh = fill.mesh; + var rectangle = fill.tile.rectangle; + var encoding = mesh.encoding; + var vertices = mesh.vertices; + var stride = encoding.getStride(); + var count = mesh.vertices.length / stride; + + for (var i = 0; i < count; ++i) { + var tc = encoding.decodeTextureCoordinates(vertices, i, textureCoordinateScratch); + var vertexHeight = encoding.decodeHeight(vertices, i); + var vertexPosition = encoding.decodePosition(vertices, i, positionScratch); + if (Math.abs(u - tc.x) < 1e-5 && Math.abs(v - tc.y) < CesiumMath.EPSILON5) { + expect(vertexHeight).toEqualEpsilon(height, CesiumMath.EPSILON5); + + var longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); + var latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); + var expectedPosition = Cartesian3.fromRadians(longitude, latitude, vertexHeight, undefined, expectedPositionScratch); + expect(vertexPosition).toEqualEpsilon(expectedPosition, 1); + return; + } + } + + fail('Vertex with u=' + u + ', v=' + v + ' does not exist.'); + } + + function expectVertexCount(fill, count) { + // A fill tile may have space allocated for extra vertices, but not all will be used. + var actualCount = fill.mesh.indices.reduce(function(high, current) { + return Math.max(high, current); + }, -1) + 1; + expect(actualCount).toBe(count); + } }); From b5ba318e42e55b0c8c99552f09cd6264415b9ccf Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 5 Jan 2019 00:49:25 +1100 Subject: [PATCH 081/131] Improve fill specs. --- Specs/Scene/TerrainFillMeshSpec.js | 134 +++++++++++++++++++---------- 1 file changed, 89 insertions(+), 45 deletions(-) diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 2bb0a4a27e9..0e3545828db 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -115,54 +115,94 @@ defineSuite([ northeast = east.findTileToNorth(rootTiles); spyOn(mockTerrain, 'requestTileGeometry').and.callFake(function(x, y, level) { - var buffer = new Float32Array(4); + var buffer = new Float32Array(9); if (x === center.x && y === center.y) { return undefined; } else if (x === west.x && y === west.y) { - buffer[0] = 3.0; - buffer[1] = 4.0; - buffer[2] = 1.0; - buffer[3] = 2.0; + buffer[0] = 15.0; + buffer[1] = 16.0; + buffer[2] = 17.0; + buffer[3] = 22.0; + buffer[4] = 23.0; + buffer[5] = 24.0; + buffer[6] = 29.0; + buffer[7] = 30.0; + buffer[8] = 31.0; } else if (x === south.x && y === south.y) { - buffer[0] = 2.0; - buffer[1] = 7.0; - buffer[2] = 5.0; - buffer[3] = 6.0; + buffer[0] = 31.0; + buffer[1] = 32.0; + buffer[2] = 33.0; + buffer[3] = 38.0; + buffer[4] = 39.0; + buffer[5] = 40.0; + buffer[6] = 45.0; + buffer[7] = 46.0; + buffer[8] = 47.0; } else if (x === east.x && y === east.y) { - buffer[0] = 9.0; - buffer[1] = 10.0; - buffer[2] = 7.0; - buffer[3] = 8.0; + buffer[0] = 19.0; + buffer[1] = 20.0; + buffer[2] = 21.0; + buffer[3] = 26.0; + buffer[4] = 27.0; + buffer[5] = 28.0; + buffer[6] = 33.0; + buffer[7] = 34.0; + buffer[8] = 35.0; } else if (x === north.x && y === north.y) { - buffer[0] = 11.0; - buffer[1] = 12.0; - buffer[2] = 4.0; - buffer[3] = 9.0; + buffer[0] = 3.0; + buffer[1] = 4.0; + buffer[2] = 5.0; + buffer[3] = 10.0; + buffer[4] = 11.0; + buffer[5] = 12.0; + buffer[6] = 17.0; + buffer[7] = 18.0; + buffer[8] = 19.0; } else if (x === southwest.x && y === southwest.y) { - buffer[0] = 1.0; - buffer[1] = 2.0; - buffer[2] = 13.0; - buffer[3] = 5.0; + buffer[0] = 29.0; + buffer[1] = 30.0; + buffer[2] = 31.0; + buffer[3] = 36.0; + buffer[4] = 37.0; + buffer[5] = 38.0; + buffer[6] = 43.0; + buffer[7] = 44.0; + buffer[8] = 45.0; } else if (x === southeast.x && y === southeast.y) { - buffer[0] = 7.0; - buffer[1] = 8.0; - buffer[2] = 6.0; - buffer[3] = 14.0; + buffer[0] = 33.0; + buffer[1] = 34.0; + buffer[2] = 35.0; + buffer[3] = 40.0; + buffer[4] = 41.0; + buffer[5] = 42.0; + buffer[6] = 47.0; + buffer[7] = 48.0; + buffer[8] = 49.0; } else if (x === northwest.x && y === northwest.y) { - buffer[0] = 15.0; - buffer[1] = 11.0; + buffer[0] = 1.0; + buffer[1] = 2.0; buffer[2] = 3.0; - buffer[3] = 4.0; + buffer[3] = 8.0; + buffer[4] = 9.0; + buffer[5] = 10.0; + buffer[6] = 15.0; + buffer[7] = 16.0; + buffer[8] = 17.0; } else if (x === northeast.x && y === northeast.y) { - buffer[0] = 12.0; - buffer[1] = 16.0; - buffer[2] = 9.0; - buffer[3] = 10.0; + buffer[0] = 5.0; + buffer[1] = 6.0; + buffer[2] = 7.0; + buffer[3] = 12.0; + buffer[4] = 13.0; + buffer[5] = 14.0; + buffer[6] = 19.0; + buffer[7] = 20.0; + buffer[8] = 21.0; } var terrainData = new HeightmapTerrainData({ - width: 2, - height: 2, + width: 3, + height: 3, buffer: buffer, createdByUpsampling: false }); @@ -220,12 +260,16 @@ defineSuite([ fill.update(tileProvider, frameState); - expectVertexCount(fill, 5); - expectVertex(fill, 0.0, 0.0, 2.0); - expectVertex(fill, 1.0, 0.0, 7.0); - expectVertex(fill, 0.0, 1.0, 4.0); - expectVertex(fill, 1.0, 1.0, 9.0); - expectVertex(fill, 0.5, 0.5, (9.0 + 2.0) / 2.0); + expectVertexCount(fill, 9); + expectVertex(fill, 0.0, 0.0, 31.0); + expectVertex(fill, 0.5, 0.0, 32.0); + expectVertex(fill, 1.0, 0.0, 33.0); + expectVertex(fill, 0.0, 0.5, 24.0); + expectVertex(fill, 0.5, 0.5, (33.0 + 17.0) / 2); + expectVertex(fill, 1.0, 0.5, 26.0); + expectVertex(fill, 0.0, 1.0, 17.0); + expectVertex(fill, 0.5, 1.0, 18.0); + expectVertex(fill, 1.0, 1.0, 19.0); }); }); @@ -245,11 +289,11 @@ defineSuite([ fill.update(tileProvider, frameState); expectVertexCount(fill, 5); - expectVertex(fill, 0.0, 0.0, 2.0); - expectVertex(fill, 1.0, 0.0, 7.0); - expectVertex(fill, 0.0, 1.0, 4.0); - expectVertex(fill, 1.0, 1.0, 9.0); - expectVertex(fill, 0.5, 0.5, (9.0 + 2.0) / 2.0); + expectVertex(fill, 0.0, 0.0, 31.0); + expectVertex(fill, 1.0, 0.0, 33.0); + expectVertex(fill, 0.0, 1.0, 17.0); + expectVertex(fill, 1.0, 1.0, 19.0); + expectVertex(fill, 0.5, 0.5, (17.0 + 33.0) / 2.0); }); }); }); From c0bf974b3973c05b2f74106e54e8a91208c75ab1 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 5 Jan 2019 17:10:51 +1100 Subject: [PATCH 082/131] Work around missing slice function in IE11. --- .../createVerticesFromQuantizedTerrainMesh.js | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js index 3270550a005..dd13bf7d3c7 100644 --- a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js +++ b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js @@ -134,16 +134,16 @@ define([ Cartesian3.maximumByComponent(cartesian3Scratch, maximum, maximum); } - var westIndicesSouthToNorth = parameters.westIndices.slice().sort(function(a, b) { + var westIndicesSouthToNorth = copyAndSort(parameters.westIndices, function(a, b) { return uvs[a].y - uvs[b].y; }); - var eastIndicesNorthToSouth = parameters.eastIndices.slice().sort(function(a, b) { + var eastIndicesNorthToSouth = copyAndSort(parameters.eastIndices, function(a, b) { return uvs[b].y - uvs[a].y; }); - var southIndicesEastToWest = parameters.southIndices.slice().sort(function(a, b) { + var southIndicesEastToWest = copyAndSort(parameters.southIndices, function(a, b) { return uvs[b].x - uvs[a].x; }); - var northIndicesWestToEast = parameters.northIndices.slice().sort(function(a, b) { + var northIndicesWestToEast = copyAndSort(parameters.northIndices, function(a, b) { return uvs[a].x - uvs[b].x; }); @@ -358,5 +358,24 @@ define([ return indexBufferIndex; } + function copyAndSort(typedArray, comparator) { + var copy; + if (typeof typedArray.slice === 'function') { + copy = typedArray.slice(); + if (typeof copy.sort !== 'function') { + // Sliced typed array isn't sortable, so we can't use it. + copy = undefined; + } + } + + if (!defined(copy)) { + copy = Array.prototype.slice.call(typedArray) + } + + copy.sort(comparator); + + return copy; + } + return createTaskProcessorWorker(createVerticesFromQuantizedTerrainMesh); }); From f4c2f3553166d458c57906bb64dfd2a8443d3512 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 6 Jan 2019 20:22:01 +1100 Subject: [PATCH 083/131] Add missing semicolon. --- Source/Workers/createVerticesFromQuantizedTerrainMesh.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js index dd13bf7d3c7..8d9f5b0d6f3 100644 --- a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js +++ b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js @@ -369,7 +369,7 @@ define([ } if (!defined(copy)) { - copy = Array.prototype.slice.call(typedArray) + copy = Array.prototype.slice.call(typedArray); } copy.sort(comparator); From 34d14d7363bb4fe667ccb103b64488e485f8b1bc Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 7 Jan 2019 17:45:59 +1100 Subject: [PATCH 084/131] More fill specs. --- Source/Scene/TerrainFillMesh.js | 128 ++++++++---------- Specs/Scene/TerrainFillMeshSpec.js | 203 +++++++++++++++++++++++++++-- 2 files changed, 247 insertions(+), 84 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 9b2e23f82b0..aba98289a1d 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -414,6 +414,37 @@ define([ this.encodedNormal = new Cartesian2(); } + function fillMissingCorner(fill, ellipsoid, u, v, corner, adjacentCorner1, adjacentCorner2, oppositeCorner, vertex) { + if (defined(corner)) { + return corner; + } + + var height; + + if (defined(adjacentCorner1) && defined(adjacentCorner2)) { + height = (adjacentCorner1.height + adjacentCorner2.height) * 0.5; + } else if (defined(adjacentCorner1)) { + height = adjacentCorner1.height; + } else if (defined(adjacentCorner2)) { + height = adjacentCorner2.height; + } else if (defined(oppositeCorner)) { + height = oppositeCorner.height; + } else { + var surfaceTile = fill.tile.data; + var tileBoundingRegion = surfaceTile.tileBoundingRegion; + var minimumHeight = 0.0; + var maximumHeight = 0.0; + if (defined(tileBoundingRegion)) { + minimumHeight = tileBoundingRegion.minimumHeight; + maximumHeight = tileBoundingRegion.maximumHeight; + } + height = (minimumHeight + maximumHeight) * 0.5; + } + + getVertexWithHeightAtCorner(fill, ellipsoid, u, v, height, vertex); + return vertex; + } + var swVertexScratch = new HeightAndNormal(); var seVertexScratch = new HeightAndNormal(); var nwVertexScratch = new HeightAndNormal(); @@ -431,6 +462,11 @@ define([ var seCorner = getCorner(fill, ellipsoid, 1.0, 0.0, fill.southeastTile, fill.southeastMesh, fill.southTiles, fill.southMeshes, fill.eastTiles, fill.eastMeshes, seVertexScratch); var neCorner = getCorner(fill, ellipsoid, 1.0, 1.0, fill.northeastTile, fill.northeastMesh, fill.eastTiles, fill.eastMeshes, fill.northTiles, fill.northMeshes, neVertexScratch); + nwCorner = fillMissingCorner(fill, ellipsoid, 0.0, 1.0, nwCorner, swCorner, neCorner, seCorner, nwVertexScratch); + swCorner = fillMissingCorner(fill, ellipsoid, 0.0, 0.0, swCorner, nwCorner, seCorner, neCorner, swVertexScratch); + seCorner = fillMissingCorner(fill, ellipsoid, 1.0, 1.0, seCorner, swCorner, neCorner, nwCorner, seVertexScratch); + neCorner = fillMissingCorner(fill, ellipsoid, 1.0, 1.0, neCorner, seCorner, nwCorner, swCorner, neVertexScratch); + var southwestHeight = swCorner.height; var southeastHeight = seCorner.height; var northwestHeight = nwCorner.height; @@ -774,7 +810,10 @@ define([ } // There is no precise vertex available from the corner or from either adjacent edge. - // So use the height from the closest vertex anywhere on the perimeter of this tile. + // This is either because there are no tiles at all at the edges and corner, or + // because the tiles at the edge are higher-level-number and don't extend all the way + // to the corner. + // Try to grab a height from the adjacent edges. var height; if (u === 0.0) { if (v === 0.0) { @@ -782,22 +821,12 @@ define([ height = getClosestHeightToCorner( terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, - terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, - terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, - terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, - terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, - terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, u, v); } else { // northwest height = getClosestHeightToCorner( terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, - terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, - terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, - terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, - terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, - terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, u, v); } } else if (v === 0.0) { @@ -805,88 +834,39 @@ define([ height = getClosestHeightToCorner( terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, - terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, - terrainFillMesh.northeastMesh, terrainFillMesh.northeastTile, TileEdge.SOUTHWEST, - terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, - terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, - terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, u, v); } else { // northeast height = getClosestHeightToCorner( terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, - terrainFillMesh.southeastMesh, terrainFillMesh.southeastTile, TileEdge.NORTHWEST, - terrainFillMesh.northwestMesh, terrainFillMesh.northwestTile, TileEdge.SOUTHEAST, - terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, - terrainFillMesh.southMeshes, terrainFillMesh.southTiles,TileEdge.NORTH, - terrainFillMesh.southwestMesh, terrainFillMesh.southwestTile, TileEdge.NORTHEAST, u, v); } - if (!defined(height)) { - // No heights available whatsoever, so use the average of this tile's minimum and maximum height. - var surfaceTile = terrainFillMesh.tile.data; - var tileBoundingRegion = surfaceTile.tileBoundingRegion; - var minimumHeight = 0.0; - var maximumHeight = 0.0; - if (defined(tileBoundingRegion)) { - minimumHeight = tileBoundingRegion.minimumHeight; - maximumHeight = tileBoundingRegion.maximumHeight; - } - height = (minimumHeight + maximumHeight) * 0.5; + if (defined(height)) { + getVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, vertex); + return vertex; } - getVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, vertex); - return vertex; + // No heights available that are closer than the adjacent corners. + return undefined; } function getClosestHeightToCorner( adjacentEdge1Meshes, adjacentEdge1Tiles, adjacentEdge1, adjacentEdge2Meshes, adjacentEdge2Tiles, adjacentEdge2, - adjacentCorner1Mesh, adjacentCorner1Tile, adjacentCorner1, - adjacentCorner2Mesh, adjacentCorner2Tile, adjacentCorner2, - oppositeEdge1Meshes, oppositeEdge1Tiles, oppositeEdge1, - oppositeEdge2Meshes, oppositeEdge2Tiles, oppositeEdge2, - oppositeCornerMesh, oppositeCornerTile, oppositeCorner, u, v ) { - // To find a height to use for this corner, we'll first look at the two adjacent edges, - // then the two adjacent corners, then the two opposite edges, then the two opposite - // corners. When e.g. both adjacent edges have a height, it would be better to choose - // the closest height rather than always choosing adjacentEdge1's height, as we're - // doing here, but it probably doesn't matter too much. - var height = getNearestHeightOnEdge(adjacentEdge1Meshes, adjacentEdge1Tiles, false, adjacentEdge1, u, v); - if (defined(height)) { - return height; - } - - height = getNearestHeightOnEdge(adjacentEdge2Meshes, adjacentEdge2Tiles, true, adjacentEdge2, u, v); - if (defined(height)) { - return height; + var height1 = getNearestHeightOnEdge(adjacentEdge1Meshes, adjacentEdge1Tiles, false, adjacentEdge1, u, v); + var height2 = getNearestHeightOnEdge(adjacentEdge2Meshes, adjacentEdge2Tiles, true, adjacentEdge2, u, v); + if (defined(height1) && defined(height2)) { + // It would be slightly better to do a weighted average of the two heights + // based on their distance from the corner, but it shouldn't matter much in practice. + return (height1 + height2) * 0.5; + } else if (defined(height1)) { + return height1; } - - height = getHeightAtCorner(adjacentCorner1Mesh, adjacentCorner1Tile, adjacentCorner1, u, v); - if (defined(height)) { - return height; - } - - height = getHeightAtCorner(adjacentCorner2Mesh, adjacentCorner2Tile, adjacentCorner2, u, v); - if (defined(height)) { - return height; - } - - height = getNearestHeightOnEdge(oppositeEdge1Meshes, oppositeEdge1Tiles, false, oppositeEdge1, u, v); - if (defined(height)) { - return height; - } - - height = getNearestHeightOnEdge(oppositeEdge2Meshes, oppositeEdge2Tiles, true, oppositeEdge2, u, v); - if (defined(height)) { - return height; - } - - return getHeightAtCorner(oppositeCornerMesh, oppositeCornerTile, oppositeCorner, u, v); + return height2; } function addEdge(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles, edgeMeshes, tileEdge) { diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 0e3545828db..439a9cd10a9 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -116,9 +116,7 @@ defineSuite([ spyOn(mockTerrain, 'requestTileGeometry').and.callFake(function(x, y, level) { var buffer = new Float32Array(9); - if (x === center.x && y === center.y) { - return undefined; - } else if (x === west.x && y === west.y) { + if (level === west.level && x === west.x && y === west.y) { buffer[0] = 15.0; buffer[1] = 16.0; buffer[2] = 17.0; @@ -128,7 +126,7 @@ defineSuite([ buffer[6] = 29.0; buffer[7] = 30.0; buffer[8] = 31.0; - } else if (x === south.x && y === south.y) { + } else if (level === south.level && x === south.x && y === south.y) { buffer[0] = 31.0; buffer[1] = 32.0; buffer[2] = 33.0; @@ -138,7 +136,7 @@ defineSuite([ buffer[6] = 45.0; buffer[7] = 46.0; buffer[8] = 47.0; - } else if (x === east.x && y === east.y) { + } else if (level === east.level && x === east.x && y === east.y) { buffer[0] = 19.0; buffer[1] = 20.0; buffer[2] = 21.0; @@ -148,7 +146,7 @@ defineSuite([ buffer[6] = 33.0; buffer[7] = 34.0; buffer[8] = 35.0; - } else if (x === north.x && y === north.y) { + } else if (level === north.level && x === north.x && y === north.y) { buffer[0] = 3.0; buffer[1] = 4.0; buffer[2] = 5.0; @@ -158,7 +156,7 @@ defineSuite([ buffer[6] = 17.0; buffer[7] = 18.0; buffer[8] = 19.0; - } else if (x === southwest.x && y === southwest.y) { + } else if (level === southwest.level && x === southwest.x && y === southwest.y) { buffer[0] = 29.0; buffer[1] = 30.0; buffer[2] = 31.0; @@ -168,7 +166,7 @@ defineSuite([ buffer[6] = 43.0; buffer[7] = 44.0; buffer[8] = 45.0; - } else if (x === southeast.x && y === southeast.y) { + } else if (level === southeast.level && x === southeast.x && y === southeast.y) { buffer[0] = 33.0; buffer[1] = 34.0; buffer[2] = 35.0; @@ -178,7 +176,7 @@ defineSuite([ buffer[6] = 47.0; buffer[7] = 48.0; buffer[8] = 49.0; - } else if (x === northwest.x && y === northwest.y) { + } else if (level === northwest.level && x === northwest.x && y === northwest.y) { buffer[0] = 1.0; buffer[1] = 2.0; buffer[2] = 3.0; @@ -188,7 +186,7 @@ defineSuite([ buffer[6] = 15.0; buffer[7] = 16.0; buffer[8] = 17.0; - } else if (x === northeast.x && y === northeast.y) { + } else if (level === northeast.level && x === northeast.x && y === northeast.y) { buffer[0] = 5.0; buffer[1] = 6.0; buffer[2] = 7.0; @@ -296,6 +294,191 @@ defineSuite([ expectVertex(fill, 0.5, 0.5, (17.0 + 33.0) / 2.0); }); }); + + it('finds a suitable corner vertex in a less detailed tile', function() { + var sw = center.southwestChild; + var se = center.southeastChild; + var nw = center.northwestChild; + var ne = center.northeastChild; + + return processor.process([sw, se, nw, ne, west, south, east, north]).then(function() { + var fillSW = sw.data.fill = new TerrainFillMesh(sw); + var fillSE = se.data.fill = new TerrainFillMesh(se); + var fillNW = nw.data.fill = new TerrainFillMesh(nw); + var fillNE = ne.data.fill = new TerrainFillMesh(ne); + + fillSW.westTiles.push(west); + fillSW.westMeshes.push(west.data.mesh); + fillSW.southTiles.push(south); + fillSW.southMeshes.push(south.data.mesh); + + fillSE.eastTiles.push(east); + fillSE.eastMeshes.push(east.data.mesh); + fillSE.southTiles.push(south); + fillSE.southMeshes.push(south.data.mesh); + + fillNW.westTiles.push(west); + fillNW.westMeshes.push(west.data.mesh); + fillNW.northTiles.push(north); + fillNW.northMeshes.push(north.data.mesh); + + fillNE.eastTiles.push(east); + fillNE.eastMeshes.push(east.data.mesh); + fillNE.northTiles.push(north); + fillNE.northMeshes.push(north.data.mesh); + + fillSW.update(tileProvider, frameState); + fillSE.update(tileProvider, frameState); + fillNW.update(tileProvider, frameState); + fillNE.update(tileProvider, frameState); + + expectVertexCount(fillSW, 5); + expectVertex(fillSW, 0.0, 0.0, 31.0); + expectVertex(fillSW, 1.0, 0.0, 32.0); + expectVertex(fillSW, 0.0, 1.0, 24.0); + expectVertex(fillSW, 1.0, 1.0, (24.0 + 32.0) / 2); + expectVertex(fillSW, 0.5, 0.5, (24.0 + 32.0) / 2); + + expectVertexCount(fillSE, 5); + expectVertex(fillSE, 0.0, 0.0, 32.0); + expectVertex(fillSE, 1.0, 0.0, 33.0); + expectVertex(fillSE, 0.0, 1.0, (32.0 + 26.0) / 2); + expectVertex(fillSE, 1.0, 1.0, 26.0); + expectVertex(fillSE, 0.5, 0.5, (26.0 + 33.0) / 2); + + expectVertexCount(fillNW, 5); + expectVertex(fillNW, 0.0, 0.0, 24.0); + expectVertex(fillNW, 1.0, 0.0, (18.0 + 24.0) / 2); + expectVertex(fillNW, 0.0, 1.0, 17.0); + expectVertex(fillNW, 1.0, 1.0, 18.0); + expectVertex(fillNW, 0.5, 0.5, (17.0 + 24.0) / 2); + + expectVertexCount(fillNE, 5); + expectVertex(fillNE, 0.0, 0.0, (18.0 + 26.0) / 2); + expectVertex(fillNE, 1.0, 0.0, 26.0); + expectVertex(fillNE, 0.0, 1.0, 18.0); + expectVertex(fillNE, 1.0, 1.0, 19.0); + expectVertex(fillNE, 0.5, 0.5, (18.0 + 26.0) / 2); + }); + }); + + describe('correctly transforms texture coordinates across the anti-meridian', function() { + var westernHemisphere; + var easternHemisphere; + + beforeEach(function() { + westernHemisphere = rootTiles[0]; + easternHemisphere = rootTiles[1]; + + // Make sure we have a standard geographic tiling scheme with two root tiles, + // the first covering the western hemisphere and the second the eastern. + expect(rootTiles.length).toBe(2); + expect(westernHemisphere.x).toBe(0); + expect(easternHemisphere.x).toBe(1); + }); + + it('western hemisphere to eastern hemisphere', function() { + mockTerrain.requestTileGeometry.and.callFake(function(x, y, level) { + var buffer = new Float32Array(9); + if (x === easternHemisphere.x) { + // eastern hemisphere tile + return undefined; + } + + // western hemisphere tile + buffer[0] = 1.0; + buffer[1] = 2.0; + buffer[2] = 3.0; + buffer[3] = 4.0; + buffer[4] = 5.0; + buffer[5] = 6.0; + buffer[6] = 7.0; + buffer[7] = 8.0; + buffer[8] = 9.0; + + var terrainData = new HeightmapTerrainData({ + width: 3, + height: 3, + buffer: buffer, + createdByUpsampling: false + }); + return when(terrainData); + }); + + return processor.process([westernHemisphere, easternHemisphere]).then(function() { + var fill = easternHemisphere.data.fill = new TerrainFillMesh(easternHemisphere); + + fill.eastTiles.push(westernHemisphere); + fill.eastMeshes.push(westernHemisphere.data.mesh); + fill.westTiles.push(westernHemisphere); + fill.westMeshes.push(westernHemisphere.data.mesh); + + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 7); + expectVertex(fill, 0.0, 0.0, 9.0); + expectVertex(fill, 0.0, 0.5, 6.0); + expectVertex(fill, 0.0, 1.0, 3.0); + expectVertex(fill, 1.0, 0.0, 7.0); + expectVertex(fill, 1.0, 0.5, 4.0); + expectVertex(fill, 1.0, 1.0, 1.0); + expectVertex(fill, 0.5, 0.5, (1.0 + 9.0) / 2); + }); + }); + + it('eastern hemisphere to western hemisphere', function() { + mockTerrain.requestTileGeometry.and.callFake(function(x, y, level) { + var buffer = new Float32Array(9); + if (x === westernHemisphere.x) { + // western hemisphere tile + return undefined; + } + + // eastern hemisphere tile + buffer[0] = 10.0; + buffer[1] = 11.0; + buffer[2] = 12.0; + buffer[3] = 13.0; + buffer[4] = 14.0; + buffer[5] = 15.0; + buffer[6] = 16.0; + buffer[7] = 17.0; + buffer[8] = 18.0; + + var terrainData = new HeightmapTerrainData({ + width: 3, + height: 3, + buffer: buffer, + createdByUpsampling: false + }); + return when(terrainData); + }); + + return processor.process([westernHemisphere, easternHemisphere]).then(function() { + var fill = westernHemisphere.data.fill = new TerrainFillMesh(westernHemisphere); + + fill.eastTiles.push(easternHemisphere); + fill.eastMeshes.push(easternHemisphere.data.mesh); + fill.westTiles.push(easternHemisphere); + fill.westMeshes.push(easternHemisphere.data.mesh); + + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 7); + expectVertex(fill, 0.0, 0.0, 18.0); + expectVertex(fill, 0.0, 0.5, 15.0); + expectVertex(fill, 0.0, 1.0, 12.0); + expectVertex(fill, 1.0, 0.0, 16.0); + expectVertex(fill, 1.0, 0.5, 13.0); + expectVertex(fill, 1.0, 1.0, 10.0); + expectVertex(fill, 0.5, 0.5, (10.0 + 18.0) / 2); + }); + }); + }); + + describe('across levels', function() { + + }); }); var textureCoordinateScratch = new Cartesian2(); From eec83639897a1cf2317c74bd4fd28decf53b1532 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 8 Jan 2019 15:27:45 +1100 Subject: [PATCH 085/131] More specs, remove unused code, fix a bug. --- Source/Scene/TerrainFillMesh.js | 41 +------ Specs/Scene/TerrainFillMeshSpec.js | 185 ++++++++++++++++++++--------- 2 files changed, 132 insertions(+), 94 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index aba98289a1d..ae13169bcab 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -757,7 +757,7 @@ define([ AttributeCompression.octEncode(normal, vertex.encodedNormal); } else { // TODO: do we need to actually compute a normal? - // It's probably going to be unused to 0,0 is just as good. + // It's probably going to be unused so 0,0 is just as good. normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); AttributeCompression.octEncode(normal, vertex.encodedNormal); } @@ -1036,37 +1036,6 @@ define([ return undefined; } - function getHeightAtCorner(mesh, tile, edge, u, v) { - if (!defined(mesh) || mesh.changedThisFrame) { - return undefined; - } - - var terrainMesh = mesh; - - var indices; - switch (edge) { - case TileEdge.SOUTHWEST: - indices = terrainMesh.westIndicesSouthToNorth; - break; - case TileEdge.SOUTHEAST: - indices = terrainMesh.southIndicesEastToWest; - break; - case TileEdge.NORTHEAST: - indices = terrainMesh.eastIndicesNorthToSouth; - break; - case TileEdge.NORTHWEST: - indices = terrainMesh.northIndicesWestToEast; - break; - } - - var index = indices[0]; - if (defined(index)) { - return mesh.encoding.decodeHeight(terrainMesh.vertices, index); - } - - return undefined; - } - function getCornerFromEdge(terrainFillMesh, ellipsoid, edgeMeshes, edgeTiles, isNext, u, v, vertex) { var edgeVertices; var compareU; @@ -1122,13 +1091,13 @@ define([ var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch); if (increasing) { if (compareU) { - return u - targetUv.x; + return targetUv.x - u; } - return v - targetUv.y; + return targetUv.y - v; } else if (compareU) { - return targetUv.x - u; + return u - targetUv.x; } - return targetUv.y - v; + return v - targetUv.y; }); if (vertexIndexIndex < 0) { diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 439a9cd10a9..d516468f614 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -34,18 +34,67 @@ defineSuite([ TerrainTileProcessor) { 'use strict'; - describe('update', function() { - var processor; - var scene; - var camera; - var frameState; - var imageryLayerCollection; - var surfaceShaderSet; - var mockTerrain; - var tileProvider; - var quadtree; - var rootTiles; + var processor; + var scene; + var camera; + var frameState; + var imageryLayerCollection; + var surfaceShaderSet; + var mockTerrain; + var tileProvider; + var quadtree; + var rootTiles; + + beforeEach(function() { + scene = { + mapProjection: new GeographicProjection(), + drawingBufferWidth: 1000, + drawingBufferHeight: 1000 + }; + + camera = new Camera(scene); + + frameState = { + frameNumber: 0, + passes: { + render: true + }, + camera: camera, + fog: { + enabled: false + }, + context: { + drawingBufferWidth: scene.drawingBufferWidth, + drawingBufferHeight: scene.drawingBufferHeight + }, + mode: SceneMode.SCENE3D, + commandList: [], + cullingVolume: jasmine.createSpyObj('CullingVolume', ['computeVisibility']), + afterRender: [] + }; + + frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING); + + imageryLayerCollection = new ImageryLayerCollection(); + surfaceShaderSet = jasmine.createSpyObj('SurfaceShaderSet', ['getShaderProgram']); + mockTerrain = new MockTerrainProvider(); + tileProvider = new GlobeSurfaceTileProvider({ + terrainProvider: mockTerrain, + imageryLayers: imageryLayerCollection, + surfaceShaderSet: surfaceShaderSet + }); + quadtree = new QuadtreePrimitive({ + tileProvider: tileProvider + }); + + processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection); + processor.mockWebGL(); + quadtree.render(frameState); + rootTiles = quadtree._levelZeroTiles; + }); + + describe('update', function() { var center; var west; var south; @@ -57,53 +106,6 @@ defineSuite([ var northeast; beforeEach(function() { - scene = { - mapProjection: new GeographicProjection(), - drawingBufferWidth: 1000, - drawingBufferHeight: 1000 - }; - - camera = new Camera(scene); - - frameState = { - frameNumber: 0, - passes: { - render: true - }, - camera: camera, - fog: { - enabled: false - }, - context: { - drawingBufferWidth: scene.drawingBufferWidth, - drawingBufferHeight: scene.drawingBufferHeight - }, - mode: SceneMode.SCENE3D, - commandList: [], - cullingVolume: jasmine.createSpyObj('CullingVolume', ['computeVisibility']), - afterRender: [] - }; - - frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING); - - imageryLayerCollection = new ImageryLayerCollection(); - surfaceShaderSet = jasmine.createSpyObj('SurfaceShaderSet', ['getShaderProgram']); - mockTerrain = new MockTerrainProvider(); - tileProvider = new GlobeSurfaceTileProvider({ - terrainProvider: mockTerrain, - imageryLayers: imageryLayerCollection, - surfaceShaderSet: surfaceShaderSet - }); - quadtree = new QuadtreePrimitive({ - tileProvider: tileProvider - }); - - processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection); - processor.mockWebGL(); - - quadtree.render(frameState); - rootTiles = quadtree._levelZeroTiles; - center = rootTiles[0].northeastChild.southwestChild; west = center.findTileToWest(rootTiles); south = center.findTileToSouth(rootTiles); @@ -362,6 +364,73 @@ defineSuite([ }); }); + it('interpolates a suitable corner vertex from a less detailed tile', function() { + var sw = center.southwestChild.southwestChild; + var se = center.southeastChild.southeastChild; + var nw = center.northwestChild.northwestChild; + var ne = center.northeastChild.northeastChild; + + return processor.process([sw, se, nw, ne, west, south, east, north]).then(function() { + var fillSW = sw.data.fill = new TerrainFillMesh(sw); + var fillSE = se.data.fill = new TerrainFillMesh(se); + var fillNW = nw.data.fill = new TerrainFillMesh(nw); + var fillNE = ne.data.fill = new TerrainFillMesh(ne); + + fillSW.westTiles.push(west); + fillSW.westMeshes.push(west.data.mesh); + fillSW.southTiles.push(south); + fillSW.southMeshes.push(south.data.mesh); + + fillSE.eastTiles.push(east); + fillSE.eastMeshes.push(east.data.mesh); + fillSE.southTiles.push(south); + fillSE.southMeshes.push(south.data.mesh); + + fillNW.westTiles.push(west); + fillNW.westMeshes.push(west.data.mesh); + fillNW.northTiles.push(north); + fillNW.northMeshes.push(north.data.mesh); + + fillNE.eastTiles.push(east); + fillNE.eastMeshes.push(east.data.mesh); + fillNE.northTiles.push(north); + fillNE.northMeshes.push(north.data.mesh); + + fillSW.update(tileProvider, frameState); + fillSE.update(tileProvider, frameState); + fillNW.update(tileProvider, frameState); + fillNE.update(tileProvider, frameState); + + expectVertexCount(fillSW, 5); + expectVertex(fillSW, 0.0, 0.0, 31.0); + expectVertex(fillSW, 1.0, 0.0, (31.0 + 32.0) / 2); + expectVertex(fillSW, 0.0, 1.0, (31.0 + 24.0) / 2); + expectVertex(fillSW, 1.0, 1.0, ((31.0 + 32.0) / 2 + (31.0 + 24.0) / 2) / 2); + expectVertex(fillSW, 0.5, 0.5, ((31.0 + 32.0) / 2 + (31.0 + 24.0) / 2) / 2); + + expectVertexCount(fillSE, 5); + expectVertex(fillSE, 0.0, 0.0, (32.0 + 33.0) / 2); + expectVertex(fillSE, 1.0, 0.0, 33.0); + expectVertex(fillSE, 0.0, 1.0, ((32.0 + 33.0) / 2 + (33.0 + 26.0) / 2) / 2); + expectVertex(fillSE, 1.0, 1.0, (33.0 + 26.0) / 2); + expectVertex(fillSE, 0.5, 0.5, (33.0 + (33.0 + 26.0) / 2) / 2); + + expectVertexCount(fillNW, 5); + expectVertex(fillNW, 0.0, 0.0, (17.0 + 24.0) / 2); + expectVertex(fillNW, 1.0, 0.0, ((17.0 + 18.0) / 2 + (17.0 + 24.0) / 2) / 2); + expectVertex(fillNW, 0.0, 1.0, 17.0); + expectVertex(fillNW, 1.0, 1.0, (17.0 + 18.0) / 2); + expectVertex(fillNW, 0.5, 0.5, (17.0 + (17.0 + 24.0) / 2) / 2); + + expectVertexCount(fillNE, 5); + expectVertex(fillNE, 0.0, 0.0, ((19.0 + 26.0) / 2 + (18.0 + 19.0) / 2) / 2); + expectVertex(fillNE, 1.0, 0.0, (19.0 + 26.0) / 2); + expectVertex(fillNE, 0.0, 1.0, (18.0 + 19.0) / 2); + expectVertex(fillNE, 1.0, 1.0, 19.0); + expectVertex(fillNE, 0.5, 0.5, ((18.0 + 19.0) / 2 + (19.0 + 26.0) / 2) / 2); + }); + }); + describe('correctly transforms texture coordinates across the anti-meridian', function() { var westernHemisphere; var easternHemisphere; From 09ca3c6aff172824e6b273e12d63586752351c97 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 8 Jan 2019 16:10:30 +1100 Subject: [PATCH 086/131] updateFillTiles specs. --- Specs/Scene/TerrainFillMeshSpec.js | 280 +++++++++++++++++------------ 1 file changed, 169 insertions(+), 111 deletions(-) diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index d516468f614..ff6b4d6754d 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -12,6 +12,7 @@ defineSuite([ 'Scene/QuadtreePrimitive', 'Scene/SceneMode', 'Scene/TileBoundingRegion', + 'Scene/TileSelectionResult', 'ThirdParty/when', '../MockTerrainProvider', '../TerrainTileProcessor' @@ -29,6 +30,7 @@ defineSuite([ QuadtreePrimitive, SceneMode, TileBoundingRegion, + TileSelectionResult, when, MockTerrainProvider, TerrainTileProcessor) { @@ -45,6 +47,16 @@ defineSuite([ var quadtree; var rootTiles; + var center; + var west; + var south; + var east; + var north; + var southwest; + var southeast; + var northwest; + var northeast; + beforeEach(function() { scene = { mapProjection: new GeographicProjection(), @@ -92,124 +104,170 @@ defineSuite([ quadtree.render(frameState); rootTiles = quadtree._levelZeroTiles; + + center = rootTiles[0].northeastChild.southwestChild; + west = center.findTileToWest(rootTiles); + south = center.findTileToSouth(rootTiles); + east = center.findTileToEast(rootTiles); + north = center.findTileToNorth(rootTiles); + southwest = west.findTileToSouth(rootTiles); + southeast = east.findTileToSouth(rootTiles); + northwest = west.findTileToNorth(rootTiles); + northeast = east.findTileToNorth(rootTiles); + + spyOn(mockTerrain, 'requestTileGeometry').and.callFake(function(x, y, level) { + var buffer = new Float32Array(9); + if (level === west.level && x === west.x && y === west.y) { + buffer[0] = 15.0; + buffer[1] = 16.0; + buffer[2] = 17.0; + buffer[3] = 22.0; + buffer[4] = 23.0; + buffer[5] = 24.0; + buffer[6] = 29.0; + buffer[7] = 30.0; + buffer[8] = 31.0; + } else if (level === south.level && x === south.x && y === south.y) { + buffer[0] = 31.0; + buffer[1] = 32.0; + buffer[2] = 33.0; + buffer[3] = 38.0; + buffer[4] = 39.0; + buffer[5] = 40.0; + buffer[6] = 45.0; + buffer[7] = 46.0; + buffer[8] = 47.0; + } else if (level === east.level && x === east.x && y === east.y) { + buffer[0] = 19.0; + buffer[1] = 20.0; + buffer[2] = 21.0; + buffer[3] = 26.0; + buffer[4] = 27.0; + buffer[5] = 28.0; + buffer[6] = 33.0; + buffer[7] = 34.0; + buffer[8] = 35.0; + } else if (level === north.level && x === north.x && y === north.y) { + buffer[0] = 3.0; + buffer[1] = 4.0; + buffer[2] = 5.0; + buffer[3] = 10.0; + buffer[4] = 11.0; + buffer[5] = 12.0; + buffer[6] = 17.0; + buffer[7] = 18.0; + buffer[8] = 19.0; + } else if (level === southwest.level && x === southwest.x && y === southwest.y) { + buffer[0] = 29.0; + buffer[1] = 30.0; + buffer[2] = 31.0; + buffer[3] = 36.0; + buffer[4] = 37.0; + buffer[5] = 38.0; + buffer[6] = 43.0; + buffer[7] = 44.0; + buffer[8] = 45.0; + } else if (level === southeast.level && x === southeast.x && y === southeast.y) { + buffer[0] = 33.0; + buffer[1] = 34.0; + buffer[2] = 35.0; + buffer[3] = 40.0; + buffer[4] = 41.0; + buffer[5] = 42.0; + buffer[6] = 47.0; + buffer[7] = 48.0; + buffer[8] = 49.0; + } else if (level === northwest.level && x === northwest.x && y === northwest.y) { + buffer[0] = 1.0; + buffer[1] = 2.0; + buffer[2] = 3.0; + buffer[3] = 8.0; + buffer[4] = 9.0; + buffer[5] = 10.0; + buffer[6] = 15.0; + buffer[7] = 16.0; + buffer[8] = 17.0; + } else if (level === northeast.level && x === northeast.x && y === northeast.y) { + buffer[0] = 5.0; + buffer[1] = 6.0; + buffer[2] = 7.0; + buffer[3] = 12.0; + buffer[4] = 13.0; + buffer[5] = 14.0; + buffer[6] = 19.0; + buffer[7] = 20.0; + buffer[8] = 21.0; + } else { + return undefined; + } + + var terrainData = new HeightmapTerrainData({ + width: 3, + height: 3, + buffer: buffer, + createdByUpsampling: false + }); + return when(terrainData); + }); }); - describe('update', function() { - var center; - var west; - var south; - var east; - var north; - var southwest; - var southeast; - var northwest; - var northeast; - - beforeEach(function() { - center = rootTiles[0].northeastChild.southwestChild; - west = center.findTileToWest(rootTiles); - south = center.findTileToSouth(rootTiles); - east = center.findTileToEast(rootTiles); - north = center.findTileToNorth(rootTiles); - southwest = west.findTileToSouth(rootTiles); - southeast = east.findTileToSouth(rootTiles); - northwest = west.findTileToNorth(rootTiles); - northeast = east.findTileToNorth(rootTiles); - - spyOn(mockTerrain, 'requestTileGeometry').and.callFake(function(x, y, level) { - var buffer = new Float32Array(9); - if (level === west.level && x === west.x && y === west.y) { - buffer[0] = 15.0; - buffer[1] = 16.0; - buffer[2] = 17.0; - buffer[3] = 22.0; - buffer[4] = 23.0; - buffer[5] = 24.0; - buffer[6] = 29.0; - buffer[7] = 30.0; - buffer[8] = 31.0; - } else if (level === south.level && x === south.x && y === south.y) { - buffer[0] = 31.0; - buffer[1] = 32.0; - buffer[2] = 33.0; - buffer[3] = 38.0; - buffer[4] = 39.0; - buffer[5] = 40.0; - buffer[6] = 45.0; - buffer[7] = 46.0; - buffer[8] = 47.0; - } else if (level === east.level && x === east.x && y === east.y) { - buffer[0] = 19.0; - buffer[1] = 20.0; - buffer[2] = 21.0; - buffer[3] = 26.0; - buffer[4] = 27.0; - buffer[5] = 28.0; - buffer[6] = 33.0; - buffer[7] = 34.0; - buffer[8] = 35.0; - } else if (level === north.level && x === north.x && y === north.y) { - buffer[0] = 3.0; - buffer[1] = 4.0; - buffer[2] = 5.0; - buffer[3] = 10.0; - buffer[4] = 11.0; - buffer[5] = 12.0; - buffer[6] = 17.0; - buffer[7] = 18.0; - buffer[8] = 19.0; - } else if (level === southwest.level && x === southwest.x && y === southwest.y) { - buffer[0] = 29.0; - buffer[1] = 30.0; - buffer[2] = 31.0; - buffer[3] = 36.0; - buffer[4] = 37.0; - buffer[5] = 38.0; - buffer[6] = 43.0; - buffer[7] = 44.0; - buffer[8] = 45.0; - } else if (level === southeast.level && x === southeast.x && y === southeast.y) { - buffer[0] = 33.0; - buffer[1] = 34.0; - buffer[2] = 35.0; - buffer[3] = 40.0; - buffer[4] = 41.0; - buffer[5] = 42.0; - buffer[6] = 47.0; - buffer[7] = 48.0; - buffer[8] = 49.0; - } else if (level === northwest.level && x === northwest.x && y === northwest.y) { - buffer[0] = 1.0; - buffer[1] = 2.0; - buffer[2] = 3.0; - buffer[3] = 8.0; - buffer[4] = 9.0; - buffer[5] = 10.0; - buffer[6] = 15.0; - buffer[7] = 16.0; - buffer[8] = 17.0; - } else if (level === northeast.level && x === northeast.x && y === northeast.y) { - buffer[0] = 5.0; - buffer[1] = 6.0; - buffer[2] = 7.0; - buffer[3] = 12.0; - buffer[4] = 13.0; - buffer[5] = 14.0; - buffer[6] = 19.0; - buffer[7] = 20.0; - buffer[8] = 21.0; + describe('updateFillTiles', function() { + it('does nothing if no rendered tiles are provided', function() { + expect(function() { + TerrainFillMesh.updateFillTiles(tileProvider, [], frameState); + }).not.toThrow(); + }); + + it('propagates edges and corners to an unloaded tile', function() { + var tiles = [west, south, east, north, southwest, southeast, northwest, northeast]; + + tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain)); + + tiles.push(center); + + return processor.process(tiles).then(function() { + // Mark all the tiles rendered. + function markRendered(tile) { + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.RENDERED; + + var parent = tile.parent; + while (parent) { + if (parent._lastSelectionResultFrame !== frameState.frameNumber) { + parent._lastSelectionResultFrame = frameState.frameNumber; + parent._lastSelectionResult = TileSelectionResult.REFINED; + } + parent = parent.parent; + } } - var terrainData = new HeightmapTerrainData({ - width: 3, - height: 3, - buffer: buffer, - createdByUpsampling: false - }); - return when(terrainData); + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + + var fill = center.data.fill; + expect(fill).toBeDefined(); + expect(fill.westTiles).toEqual([west]); + expect(fill.westMeshes).toEqual([west.data.mesh]); + expect(fill.southTiles).toEqual([south]); + expect(fill.southMeshes).toEqual([south.data.mesh]); + expect(fill.eastTiles).toEqual([east]); + expect(fill.eastMeshes).toEqual([east.data.mesh]); + expect(fill.northTiles).toEqual([north]); + expect(fill.northMeshes).toEqual([north.data.mesh]); + expect(fill.southwestTile).toEqual(southwest); + expect(fill.southwestMesh).toEqual(southwest.data.mesh); + expect(fill.southeastTile).toEqual(southeast); + expect(fill.southeastMesh).toEqual(southeast.data.mesh); + expect(fill.northwestTile).toEqual(northwest); + expect(fill.northwestMesh).toEqual(northwest.data.mesh); + expect(fill.northeastTile).toEqual(northeast); + expect(fill.northeastMesh).toEqual(northeast.data.mesh); }); }); + }); + describe('update', function() { it('puts a middle height at the four corners and center when there are no adjacent tiles', function() { return processor.process([center]).then(function() { center.data.tileBoundingRegion = new TileBoundingRegion({ From 7fa75da344874781dabf4b46c8ad3776f1716817 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 8 Jan 2019 16:47:35 +1100 Subject: [PATCH 087/131] More fill specs. --- Specs/Scene/TerrainFillMeshSpec.js | 129 +++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 18 deletions(-) diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index ff6b4d6754d..cb62aac7ffc 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -219,28 +219,11 @@ defineSuite([ }); it('propagates edges and corners to an unloaded tile', function() { - var tiles = [west, south, east, north, southwest, southeast, northwest, northeast]; + var tiles = [center, west, south, east, north, southwest, southeast, northwest, northeast]; tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain)); - tiles.push(center); - return processor.process(tiles).then(function() { - // Mark all the tiles rendered. - function markRendered(tile) { - tile._lastSelectionResultFrame = frameState.frameNumber; - tile._lastSelectionResult = TileSelectionResult.RENDERED; - - var parent = tile.parent; - while (parent) { - if (parent._lastSelectionResultFrame !== frameState.frameNumber) { - parent._lastSelectionResultFrame = frameState.frameNumber; - parent._lastSelectionResult = TileSelectionResult.REFINED; - } - parent = parent.parent; - } - } - tiles.forEach(markRendered); TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); @@ -265,6 +248,116 @@ defineSuite([ expect(fill.northeastMesh).toEqual(northeast.data.mesh); }); }); + + it('propagates fill tile edges to an unloaded tile', function() { + var centerSW = center.southwestChild; + var centerSE = center.southeastChild; + var centerNW = center.northwestChild; + var centerNE = center.northeastChild; + + var tiles = [centerSW, centerSE, centerNW, centerNE, west, south, east, north, southwest, southeast, northwest, northeast]; + + tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain)); + + return processor.process(tiles).then(function() { + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + + var sw = centerSW.data.fill; + var se = centerSE.data.fill; + var nw = centerNW.data.fill; + var ne = centerNE.data.fill; + + expect(sw).toBeDefined(); + expect(sw.westTiles).toEqual([west]); + expect(sw.westMeshes).toEqual([west.data.mesh]); + expect(sw.southTiles).toEqual([south]); + expect(sw.southMeshes).toEqual([south.data.mesh]); + expect(sw.southwestTile).toEqual(southwest); + expect(sw.southwestMesh).toEqual(southwest.data.mesh); + expect(sw.southeastTile).toBeUndefined(); + expect(sw.southeastMesh).toBeUndefined(); + expect(sw.northwestTile).toBeUndefined(); + expect(sw.northwestMesh).toBeUndefined(); + + expect(se).toBeDefined(); + expect(se.eastTiles).toEqual([east]); + expect(se.eastMeshes).toEqual([east.data.mesh]); + expect(se.southTiles).toEqual([south]); + expect(se.southMeshes).toEqual([south.data.mesh]); + expect(se.southeastTile).toEqual(southeast); + expect(se.southeastMesh).toEqual(southeast.data.mesh); + expect(se.southwestTile).toBeUndefined(); + expect(se.southwestMesh).toBeUndefined(); + expect(se.northeastTile).toBeUndefined(); + expect(se.northeastMesh).toBeUndefined(); + + expect(nw).toBeDefined(); + expect(nw.westTiles).toEqual([west]); + expect(nw.westMeshes).toEqual([west.data.mesh]); + expect(nw.northTiles).toEqual([north]); + expect(nw.northMeshes).toEqual([north.data.mesh]); + expect(nw.northwestTile).toEqual(northwest); + expect(nw.northwestMesh).toEqual(northwest.data.mesh); + expect(nw.southwestTile).toBeUndefined(); + expect(nw.southwestMesh).toBeUndefined(); + expect(nw.northeastTile).toBeUndefined(); + expect(nw.northeastMesh).toBeUndefined(); + + expect(ne).toBeDefined(); + expect(ne.eastTiles).toEqual([east]); + expect(ne.eastMeshes).toEqual([east.data.mesh]); + expect(ne.northTiles).toEqual([north]); + expect(ne.northMeshes).toEqual([north.data.mesh]); + expect(ne.northeastTile).toEqual(northeast); + expect(ne.northeastMesh).toEqual(northeast.data.mesh); + expect(ne.southeastTile).toBeUndefined(); + expect(ne.southeastMesh).toBeUndefined(); + expect(ne.northwestTile).toBeUndefined(); + expect(ne.northwestMesh).toBeUndefined(); + + expect(sw.eastTiles[0] === centerSE || se.westTiles[0] === centerSW).toBe(true); + expect(nw.eastTiles[0] === centerNE || ne.westTiles[0] === centerNW).toBe(true); + + expect(sw.northTiles[0] === centerNW || nw.southTiles[0] === centerSW).toBe(true); + expect(se.northTiles[0] === centerNE || ne.southTiles[0] === centerSE).toBe(true); + + expect(sw.northeastTile === centerNE || ne.southwestTile === centerSW).toBe(true); + expect(nw.southeastTile === centerSE || se.northwestTile === centerNW).toBe(true); + }); + }); + + it('does not touch disconnected tiles', function() { + var disconnected = center.southwestChild.northeastChild; + + var tiles = [disconnected, west, south, east, north, southwest, southeast, northwest, northeast]; + + tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain)); + + return processor.process(tiles).then(function() { + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + + expect(disconnected.data.fill).toBeUndefined(); + }); + }); + + // Mark all the tiles rendered. + function markRendered(tile) { + tile._lastSelectionResultFrame = frameState.frameNumber; + tile._lastSelectionResult = TileSelectionResult.RENDERED; + + var parent = tile.parent; + while (parent) { + if (parent._lastSelectionResultFrame !== frameState.frameNumber) { + parent._lastSelectionResultFrame = frameState.frameNumber; + parent._lastSelectionResult = TileSelectionResult.REFINED; + } + parent = parent.parent; + } + } }); describe('update', function() { From 497024a56476919fbea1e959349037c4ae3f64d8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 8 Jan 2019 21:30:15 +1100 Subject: [PATCH 088/131] Tests. --- Source/Scene/TerrainFillMesh.js | 12 +-- Specs/Scene/TerrainFillMeshSpec.js | 121 ++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 10 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index ae13169bcab..ce99695fbe7 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -819,8 +819,8 @@ define([ if (v === 0.0) { // southwest height = getClosestHeightToCorner( - terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST, + terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH, u, v); } else { // northwest @@ -838,8 +838,8 @@ define([ } else { // northeast height = getClosestHeightToCorner( - terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST, + terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH, u, v); } @@ -853,12 +853,12 @@ define([ } function getClosestHeightToCorner( - adjacentEdge1Meshes, adjacentEdge1Tiles, adjacentEdge1, - adjacentEdge2Meshes, adjacentEdge2Tiles, adjacentEdge2, + previousMeshes, previousTiles, previousEdge, + nextMeshes, nextTiles, nextEdge, u, v ) { - var height1 = getNearestHeightOnEdge(adjacentEdge1Meshes, adjacentEdge1Tiles, false, adjacentEdge1, u, v); - var height2 = getNearestHeightOnEdge(adjacentEdge2Meshes, adjacentEdge2Tiles, true, adjacentEdge2, u, v); + var height1 = getNearestHeightOnEdge(previousMeshes, previousTiles, false, previousEdge, u, v); + var height2 = getNearestHeightOnEdge(nextMeshes, nextTiles, true, nextEdge, u, v); if (defined(height1) && defined(height2)) { // It would be slightly better to do a weighted average of the two heights // based on their distance from the corner, but it shouldn't matter much in practice. diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index cb62aac7ffc..90382692898 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -344,6 +344,30 @@ defineSuite([ }); }); + it('propagates multiple adjacent source tiles to a destination edge', function() { + var tiles = [center, west, south, east, north]; + [west, south, east, north].forEach(function(tile) { + tile.children.forEach(function(child) { + mockTerrain.willBeUnavailable(child); + mockTerrain.upsampleWillSucceed(child); + tiles.push(child); + }); + }); + + return processor.process(tiles).then(function() { + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + + var fill = center.data.fill; + expect(fill).toBeDefined(); + expect(fill.westTiles).toEqual([west.northeastChild, west.southeastChild]); + expect(fill.southTiles).toEqual([south.northwestChild, south.northeastChild]); + expect(fill.eastTiles).toEqual([east.southwestChild, east.northwestChild]); + expect(fill.northTiles).toEqual([north.southeastChild, north.southwestChild]); + }); + }); + // Mark all the tiles rendered. function markRendered(tile) { tile._lastSelectionResultFrame = frameState.frameNumber; @@ -582,6 +606,99 @@ defineSuite([ }); }); + it('uses the height of the closest vertex when an edge does not include the corner', function() { + var westN = west.northeastChild.southeastChild; + var westS = west.southeastChild.northeastChild; + var eastN = east.northwestChild.southwestChild; + var eastS = east.southwestChild.northwestChild; + var northW = north.southwestChild.southeastChild; + var northE = north.southeastChild.southwestChild; + var southW = south.northwestChild.northeastChild; + var southE = south.northeastChild.northwestChild; + + mockTerrain.requestTileGeometry.and.callFake(function(x, y, level) { + var buffer = new Float32Array(4); + if (level === westN.level && x === westN.x && y === westN.y) { + buffer[0] = 1.0; + buffer[1] = 1.0; + buffer[2] = 1.5; + buffer[3] = 1.5; + } else if (level === westS.level && x === westS.x && y === westS.y) { + buffer[0] = 1.5; + buffer[1] = 1.5; + buffer[2] = 2.0; + buffer[3] = 2.0; + } else if (level === eastN.level && x === eastN.x && y === eastN.y) { + buffer[0] = 3.0; + buffer[1] = 3.0; + buffer[2] = 3.5; + buffer[3] = 3.5; + } else if (level === eastS.level && x === eastS.x && y === eastS.y) { + buffer[0] = 3.5; + buffer[1] = 3.5; + buffer[2] = 4.0; + buffer[3] = 4.0; + } else if (level === northW.level && x === northW.x && y === northW.y) { + buffer[0] = 5.0; + buffer[1] = 5.5; + buffer[2] = 5.0; + buffer[3] = 5.5; + } else if (level === northE.level && x === northE.x && y === northE.y) { + buffer[0] = 5.5; + buffer[1] = 6.0; + buffer[2] = 5.5; + buffer[3] = 6.0; + } else if (level === southW.level && x === southW.x && y === southW.y) { + buffer[0] = 7.0; + buffer[1] = 7.5; + buffer[2] = 7.0; + buffer[3] = 7.5; + } else if (level === southE.level && x === southE.x && y === southE.y) { + buffer[0] = 7.5; + buffer[1] = 8.0; + buffer[2] = 7.5; + buffer[3] = 8.0; + } else { + return undefined; + } + + var terrainData = new HeightmapTerrainData({ + width: 2, + height: 2, + buffer: buffer, + createdByUpsampling: false + }); + return when(terrainData); + }); + + return processor.process([center, westN, westS, eastN, eastS, northE, northW, southE, southW]).then(function() { + var fill = center.data.fill = new TerrainFillMesh(center); + + fill.westTiles.push(westN, westS); + fill.westMeshes.push(westN.data.mesh, westS.data.mesh); + fill.eastTiles.push(eastS, eastN); + fill.eastMeshes.push(eastS.data.mesh, eastN.data.mesh); + fill.northTiles.push(northE, northW); + fill.northMeshes.push(northE.data.mesh, northW.data.mesh); + fill.southTiles.push(southW, southE); + fill.southMeshes.push(southW.data.mesh, southE.data.mesh); + + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 17); + expectVertex(fill, 0.0, 0.0, (2.0 + 7.0) / 2); + expectVertex(fill, 0.0, 0.25, 2.0); + expectVertex(fill, 0.0, 0.5, 1.5); + expectVertex(fill, 0.0, 0.75, 1.0); + expectVertex(fill, 0.0, 1.0, (1.0 + 5.0) / 2); + expectVertex(fill, 1.0, 0.0, (4.0 + 8.0) / 2); + expectVertex(fill, 1.0, 0.25, 4.0); + expectVertex(fill, 1.0, 0.5, 3.5); + expectVertex(fill, 1.0, 0.75, 3.0); + expectVertex(fill, 1.0, 1.0, (3.0 + 6.0) / 2); + }); + }); + describe('correctly transforms texture coordinates across the anti-meridian', function() { var westernHemisphere; var easternHemisphere; @@ -695,10 +812,6 @@ defineSuite([ }); }); }); - - describe('across levels', function() { - - }); }); var textureCoordinateScratch = new Cartesian2(); From 85bd4b8f8876418f1ba25282e02d9606c270e86c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 11 Jan 2019 11:08:51 +1100 Subject: [PATCH 089/131] Write some tests, fix some bugs. --- Source/Scene/TerrainFillMesh.js | 76 ++-- Specs/MockTerrainProvider.js | 63 ++-- Specs/Scene/TerrainFillMeshSpec.js | 538 ++++++++++++++++++----------- Specs/TerrainTileProcessor.js | 1 + 4 files changed, 414 insertions(+), 264 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index ce99695fbe7..3dc3d358a2f 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -61,6 +61,7 @@ define([ this.northeastTile = undefined; this.changedThisFrame = true; this.visitedFrame = undefined; + this.enqueuedFrame = undefined; this.mesh = undefined; this.vertexArray = undefined; } @@ -228,11 +229,14 @@ define([ if (destinationSurfaceTile.fill === undefined) { destinationSurfaceTile.fill = new TerrainFillMesh(destinationTile); + } else if (destinationSurfaceTile.fill.visitedFrame === frameNumber) { + // Don't propagate edges to tiles that have already been visited this frame. + return; } - if (destinationSurfaceTile.fill.visitedFrame !== frameNumber) { + if (destinationSurfaceTile.fill.enqueuedFrame !== frameNumber) { // First time visiting this tile this frame, add it to the traversal queue. - destinationSurfaceTile.fill.visitedFrame = frameNumber; + destinationSurfaceTile.fill.enqueuedFrame = frameNumber; destinationSurfaceTile.fill.changedThisFrame = false; traversalQueue.enqueue(destinationTile); } @@ -242,16 +246,20 @@ define([ function propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge) { var destinationFill = destinationTile.data.fill; - var sourceMesh = sourceTile.data.mesh; - if (sourceMesh === undefined) { + var sourceMesh; + var sourceFill = sourceTile.data.fill; + if (defined(sourceFill)) { + sourceFill.visitedFrame = frameState.frameNumber; + // Source is a fill, create/update it if necessary. - var sourceFill = sourceTile.data.fill; if (sourceFill.changedThisFrame) { createFillMesh(tileProvider, frameState, sourceTile); - sourceTile.data.fill.changedThisFrame = false; + sourceFill.changedThisFrame = false; } sourceMesh = sourceTile.data.fill.mesh; + } else { + sourceMesh = sourceTile.data.mesh; } var edgeMeshes; @@ -787,25 +795,24 @@ define([ var vertexIndex; - if (cornerMesh !== undefined && !cornerMesh.changedThisFrame) { + if (meshIsUsable(cornerTile, cornerMesh)) { // Corner mesh is valid, copy its corner vertex to this mesh. - var cornerTerrainMesh = cornerMesh.mesh === undefined ? cornerMesh : cornerMesh.mesh; if (u === 0.0) { if (v === 0.0) { // southwest destination, northeast source - vertexIndex = cornerTerrainMesh.eastIndicesNorthToSouth[0]; + vertexIndex = cornerMesh.eastIndicesNorthToSouth[0]; } else { // northwest destination, southeast source - vertexIndex = cornerTerrainMesh.southIndicesEastToWest[0]; + vertexIndex = cornerMesh.southIndicesEastToWest[0]; } } else if (v === 0.0) { // southeast destination, northwest source - vertexIndex = cornerTerrainMesh.northIndicesWestToEast[0]; + vertexIndex = cornerMesh.northIndicesWestToEast[0]; } else { // northeast destination, southwest source - vertexIndex = cornerTerrainMesh.westIndicesSouthToNorth[0]; + vertexIndex = cornerMesh.westIndicesSouthToNorth[0]; } - getVertexFromTileAtCorner(cornerTerrainMesh, vertexIndex, u, v, vertex); + getVertexFromTileAtCorner(cornerMesh, vertexIndex, u, v, vertex); return vertex; } @@ -1005,89 +1012,90 @@ define([ for (var meshIndex = meshStart; meshIndex !== meshEnd; meshIndex += meshStep) { var mesh = meshes[meshIndex]; - if (!defined(mesh) || mesh.changedThisFrame) { + var tile = tiles[meshIndex]; + if (!meshIsUsable(tile, mesh)) { continue; } - var terrainMesh = mesh; - var indices; switch (edge) { case TileEdge.WEST: - indices = terrainMesh.westIndicesSouthToNorth; + indices = mesh.westIndicesSouthToNorth; break; case TileEdge.SOUTH: - indices = terrainMesh.southIndicesEastToWest; + indices = mesh.southIndicesEastToWest; break; case TileEdge.EAST: - indices = terrainMesh.eastIndicesNorthToSouth; + indices = mesh.eastIndicesNorthToSouth; break; case TileEdge.NORTH: - indices = terrainMesh.northIndicesWestToEast; + indices = mesh.northIndicesWestToEast; break; } var index = indices[isNext ? 0 : indices.length - 1]; if (defined(index)) { - return mesh.encoding.decodeHeight(terrainMesh.vertices, index); + return mesh.encoding.decodeHeight(mesh.vertices, index); } } return undefined; } + function meshIsUsable(tile, mesh) { + return defined(mesh) && (!defined(tile.data.fill) || !tile.data.fill.changedThisFrame); + } + function getCornerFromEdge(terrainFillMesh, ellipsoid, edgeMeshes, edgeTiles, isNext, u, v, vertex) { var edgeVertices; var compareU; var increasing; var vertexIndexIndex; var vertexIndex; + var sourceTile = edgeTiles[isNext ? 0 : edgeMeshes.length - 1]; var sourceMesh = edgeMeshes[isNext ? 0 : edgeMeshes.length - 1]; - if (sourceMesh !== undefined && !sourceMesh.changedThisFrame) { + if (meshIsUsable(sourceTile, sourceMesh)) { // Previous mesh is valid, but we don't know yet if it covers this corner. - var sourceTerrainMesh = sourceMesh.mesh === undefined ? sourceMesh : sourceMesh.mesh; - if (u === 0.0) { if (v === 0.0) { // southwest - edgeVertices = isNext ? sourceTerrainMesh.northIndicesWestToEast : sourceTerrainMesh.eastIndicesNorthToSouth; + edgeVertices = isNext ? sourceMesh.northIndicesWestToEast : sourceMesh.eastIndicesNorthToSouth; compareU = isNext; increasing = isNext; } else { // northwest - edgeVertices = isNext ? sourceTerrainMesh.eastIndicesNorthToSouth : sourceTerrainMesh.southIndicesEastToWest; + edgeVertices = isNext ? sourceMesh.eastIndicesNorthToSouth : sourceMesh.southIndicesEastToWest; compareU = !isNext; increasing = false; } } else if (v === 0.0) { // southeast - edgeVertices = isNext ? sourceTerrainMesh.westIndicesSouthToNorth : sourceTerrainMesh.northIndicesWestToEast; + edgeVertices = isNext ? sourceMesh.westIndicesSouthToNorth : sourceMesh.northIndicesWestToEast; compareU = !isNext; increasing = true; } else { // northeast - edgeVertices = isNext ? sourceTerrainMesh.southIndicesEastToWest : sourceTerrainMesh.westIndicesSouthToNorth; + edgeVertices = isNext ? sourceMesh.southIndicesEastToWest : sourceMesh.westIndicesSouthToNorth; compareU = isNext; increasing = !isNext; } if (edgeVertices.length > 0) { // The vertex we want will very often be the first/last vertex so check that first. - var sourceTile = edgeTiles[isNext ? 0 : edgeTiles.length - 1]; vertexIndexIndex = isNext ? 0 : edgeVertices.length - 1; vertexIndex = edgeVertices[vertexIndexIndex]; - sourceTerrainMesh.encoding.decodeTextureCoordinates(sourceTerrainMesh.vertices, vertexIndex, uvScratch); + sourceMesh.encoding.decodeTextureCoordinates(sourceMesh.vertices, vertexIndex, uvScratch); var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch); if (targetUv.x === u && targetUv.y === v) { // Vertex is good! - getVertexFromTileAtCorner(sourceTerrainMesh, vertexIndex, u, v, vertex); + getVertexFromTileAtCorner(sourceMesh, vertexIndex, u, v, vertex); return true; } // The last vertex is not the one we need, try binary searching for the right one. vertexIndexIndex = binarySearch(edgeVertices, compareU ? u : v, function(vertexIndex, textureCoordinate) { - sourceTerrainMesh.encoding.decodeTextureCoordinates(sourceTerrainMesh.vertices, vertexIndex, uvScratch); + sourceMesh.encoding.decodeTextureCoordinates(sourceMesh.vertices, vertexIndex, uvScratch); var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch); if (increasing) { if (compareU) { @@ -1105,12 +1113,12 @@ define([ if (vertexIndexIndex > 0 && vertexIndexIndex < edgeVertices.length) { // The corner falls between two vertices, so interpolate between them. - getInterpolatedVertexAtCorner(ellipsoid, sourceTile, terrainFillMesh.tile, sourceTerrainMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, vertex); + getInterpolatedVertexAtCorner(ellipsoid, sourceTile, terrainFillMesh.tile, sourceMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, vertex); return true; } } else { // Found a vertex that fits in the corner exactly. - getVertexFromTileAtCorner(sourceTerrainMesh, edgeVertices[vertexIndexIndex], u, v, vertex); + getVertexFromTileAtCorner(sourceMesh, edgeVertices[vertexIndexIndex], u, v, vertex); return true; } } diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js index 0432dcc168c..775cde8afa7 100644 --- a/Specs/MockTerrainProvider.js +++ b/Specs/MockTerrainProvider.js @@ -27,6 +27,7 @@ define([ this._tileDataAvailable = {}; this._requestTileGeometryWillSucceed = {}; + this._requestTileGeometryWillSucceedWith = {}; this._willHaveWaterMask = {}; this._willHaveBvh = {}; this._createMeshWillSucceed = {}; @@ -75,6 +76,12 @@ define([ return this; }; + MockTerrainProvider.prototype.requestTileGeometryWillSucceedWith = function(terrainData, xOrTile, y, level) { + this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = true; + this._requestTileGeometryWillSucceedWith[createTileKey(xOrTile, y, level)] = terrainData; + return this; + }; + MockTerrainProvider.prototype.requestTileGeometryWillFail = function(xOrTile, y, level) { this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = false; return this; @@ -159,37 +166,41 @@ define([ }; function createTerrainData(terrainProvider, x, y, level, upsampled) { - var options = { - width: 5, - height: 5, - buffer: new Float32Array(25), - createdByUpsampling: upsampled - }; + var terrainData = terrainProvider._requestTileGeometryWillSucceedWith[createTileKey(x, y, level)]; + + if (!defined(terrainData)) { + var options = { + width: 5, + height: 5, + buffer: new Float32Array(25), + createdByUpsampling: upsampled + }; + + var willHaveWaterMask = terrainProvider._willHaveWaterMask[createTileKey(x, y, level)]; + if (defined(willHaveWaterMask)) { + if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { + options.waterMask = new Uint8Array(4); + options.waterMask[0] = 1; + options.waterMask[1] = 1; + options.waterMask[2] = 0; + options.waterMask[3] = 0; + } else if (willHaveWaterMask.includeLand) { + options.waterMask = new Uint8Array(1); + options.waterMask[0] = 0; + } else if (willHaveWaterMask.includeWater) { + options.waterMask = new Uint8Array(1); + options.waterMask[0] = 1; + } + } - var willHaveWaterMask = terrainProvider._willHaveWaterMask[createTileKey(x, y, level)]; - if (defined(willHaveWaterMask)) { - if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) { - options.waterMask = new Uint8Array(4); - options.waterMask[0] = 1; - options.waterMask[1] = 1; - options.waterMask[2] = 0; - options.waterMask[3] = 0; - } else if (willHaveWaterMask.includeLand) { - options.waterMask = new Uint8Array(1); - options.waterMask[0] = 0; - } else if (willHaveWaterMask.includeWater) { - options.waterMask = new Uint8Array(1); - options.waterMask[0] = 1; + var willHaveBvh = terrainProvider._willHaveBvh[createTileKey(x, y, level)]; + if (defined(willHaveBvh)) { + options.bvh = willHaveBvh; } - } - var willHaveBvh = terrainProvider._willHaveBvh[createTileKey(x, y, level)]; - if (defined(willHaveBvh)) { - options.bvh = willHaveBvh; + terrainData = new HeightmapTerrainData(options); } - var terrainData = new HeightmapTerrainData(options); - var originalUpsample = terrainData.upsample; terrainData.upsample = function(tilingScheme, thisX, thisY, thisLevel, descendantX, descendantY) { var willSucceed = terrainProvider._upsampleWillSucceed[createTileKey(descendantX, descendantY, thisLevel + 1)]; diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 90382692898..429120acb20 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -115,100 +115,93 @@ defineSuite([ northwest = west.findTileToNorth(rootTiles); northeast = east.findTileToNorth(rootTiles); - spyOn(mockTerrain, 'requestTileGeometry').and.callFake(function(x, y, level) { - var buffer = new Float32Array(9); - if (level === west.level && x === west.x && y === west.y) { - buffer[0] = 15.0; - buffer[1] = 16.0; - buffer[2] = 17.0; - buffer[3] = 22.0; - buffer[4] = 23.0; - buffer[5] = 24.0; - buffer[6] = 29.0; - buffer[7] = 30.0; - buffer[8] = 31.0; - } else if (level === south.level && x === south.x && y === south.y) { - buffer[0] = 31.0; - buffer[1] = 32.0; - buffer[2] = 33.0; - buffer[3] = 38.0; - buffer[4] = 39.0; - buffer[5] = 40.0; - buffer[6] = 45.0; - buffer[7] = 46.0; - buffer[8] = 47.0; - } else if (level === east.level && x === east.x && y === east.y) { - buffer[0] = 19.0; - buffer[1] = 20.0; - buffer[2] = 21.0; - buffer[3] = 26.0; - buffer[4] = 27.0; - buffer[5] = 28.0; - buffer[6] = 33.0; - buffer[7] = 34.0; - buffer[8] = 35.0; - } else if (level === north.level && x === north.x && y === north.y) { - buffer[0] = 3.0; - buffer[1] = 4.0; - buffer[2] = 5.0; - buffer[3] = 10.0; - buffer[4] = 11.0; - buffer[5] = 12.0; - buffer[6] = 17.0; - buffer[7] = 18.0; - buffer[8] = 19.0; - } else if (level === southwest.level && x === southwest.x && y === southwest.y) { - buffer[0] = 29.0; - buffer[1] = 30.0; - buffer[2] = 31.0; - buffer[3] = 36.0; - buffer[4] = 37.0; - buffer[5] = 38.0; - buffer[6] = 43.0; - buffer[7] = 44.0; - buffer[8] = 45.0; - } else if (level === southeast.level && x === southeast.x && y === southeast.y) { - buffer[0] = 33.0; - buffer[1] = 34.0; - buffer[2] = 35.0; - buffer[3] = 40.0; - buffer[4] = 41.0; - buffer[5] = 42.0; - buffer[6] = 47.0; - buffer[7] = 48.0; - buffer[8] = 49.0; - } else if (level === northwest.level && x === northwest.x && y === northwest.y) { - buffer[0] = 1.0; - buffer[1] = 2.0; - buffer[2] = 3.0; - buffer[3] = 8.0; - buffer[4] = 9.0; - buffer[5] = 10.0; - buffer[6] = 15.0; - buffer[7] = 16.0; - buffer[8] = 17.0; - } else if (level === northeast.level && x === northeast.x && y === northeast.y) { - buffer[0] = 5.0; - buffer[1] = 6.0; - buffer[2] = 7.0; - buffer[3] = 12.0; - buffer[4] = 13.0; - buffer[5] = 14.0; - buffer[6] = 19.0; - buffer[7] = 20.0; - buffer[8] = 21.0; - } else { - return undefined; - } - - var terrainData = new HeightmapTerrainData({ - width: 3, - height: 3, - buffer: buffer, - createdByUpsampling: false - }); - return when(terrainData); - }); + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 15.0, 16.0, 17.0, + 22.0, 23.0, 24.0, + 29.0, 30.0, 31.0 + ]) + }), west).createMeshWillSucceed(west); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 31.0, 32.0, 33.0, + 38.0, 39.0, 40.0, + 45.0, 46.0, 47.0 + ]) + }), south).createMeshWillSucceed(south); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 19.0, 20.0, 21.0, + 26.0, 27.0, 28.0, + 33.0, 34.0, 35.0 + ]) + }), east).createMeshWillSucceed(east); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 3.0, 4.0, 5.0, + 10.0, 11.0, 12.0, + 17.0, 18.0, 19.0 + ]) + }), north).createMeshWillSucceed(north); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 29.0, 30.0, 31.0, + 36.0, 37.0, 38.0, + 43.0, 44.0, 45.0 + ]) + }), southwest).createMeshWillSucceed(southwest); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 33.0, 34.0, 35.0, + 40.0, 41.0, 42.0, + 47.0, 48.0, 49.0 + ]) + }), southeast).createMeshWillSucceed(southeast); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 1.0, 2.0, 3.0, + 8.0, 9.0, 10.0, + 15.0, 16.0, 17.0 + ]) + }), northwest).createMeshWillSucceed(northwest); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 5.0, 6.0, 7.0, + 12.0, 13.0, 14.0, + 19.0, 20.0, 21.0 + ]) + }), northeast).createMeshWillSucceed(northeast); }); describe('updateFillTiles', function() { @@ -368,8 +361,147 @@ defineSuite([ }); }); + it('adjusts existing fill tiles when adjacent tiles are loaded', function() { + var tiles = [center, west, south, north]; + + tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain)); + + return processor.process(tiles).then(function() { + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + + var fill = center.data.fill; + expect(fill).toBeDefined(); + expect(fill.westTiles).toEqual([west]); + expect(fill.westMeshes).toEqual([west.data.mesh]); + expect(fill.southTiles).toEqual([south]); + expect(fill.southMeshes).toEqual([south.data.mesh]); + expect(fill.eastTiles).toEqual([]); + expect(fill.eastMeshes).toEqual([]); + expect(fill.northTiles).toEqual([north]); + expect(fill.northMeshes).toEqual([north.data.mesh]); + + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 8); + + tiles.push(east); + + return processor.process(tiles); + }).then(function() { + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + + var fill = center.data.fill; + expect(fill).toBeDefined(); + expect(fill.westTiles).toEqual([west]); + expect(fill.westMeshes).toEqual([west.data.mesh]); + expect(fill.southTiles).toEqual([south]); + expect(fill.southMeshes).toEqual([south.data.mesh]); + expect(fill.eastTiles).toEqual([east]); + expect(fill.eastMeshes).toEqual([east.data.mesh]); + expect(fill.northTiles).toEqual([north]); + expect(fill.northMeshes).toEqual([north.data.mesh]); + + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 9); + expectVertex(fill, 1.0, 0.5, 26.0); + }); + }); + + it('adjusts existing fill tiles when an adjacent fill tile changes', function() { + var dontLoad = [east, south, southeast]; + dontLoad.forEach(mockTerrain.requestTileGeometryWillDefer.bind(mockTerrain)); + + var tiles = [center, west, south, east, north, southwest, southeast, northwest, northeast]; + + return processor.process(tiles).then(function() { + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + center.data.fill.update(tileProvider, frameState); + south.data.fill.update(tileProvider, frameState); + east.data.fill.update(tileProvider, frameState); + southeast.data.fill.update(tileProvider, frameState); + + expectVertexCount(center.data.fill, 7); + expectVertex(center.data.fill, 0.0, 0.0, 31.0); + expectVertex(center.data.fill, 0.0, 0.5, 24.0); + expectVertex(center.data.fill, 0.0, 1.0, 17.0); + expectVertex(center.data.fill, 0.5, 1.0, 18.0); + expectVertex(center.data.fill, 1.0, 1.0, 19.0); + + expectVertexCount(south.data.fill, 6); + expectVertex(south.data.fill, 0.0, 1.0, 31.0); + expectVertex(south.data.fill, 0.0, 0.5, 38.0); + expectVertex(south.data.fill, 0.0, 0.0, 45.0); + + expectVertexCount(east.data.fill, 6); + expectVertex(east.data.fill, 0.0, 1.0, 19.0); + expectVertex(east.data.fill, 0.5, 1.0, 20.0); + expectVertex(east.data.fill, 1.0, 1.0, 21.0); + + expectVertexCount(southeast.data.fill, 5); + + expect(getHeight(center.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 0.0, 1.0)); + expect(getHeight(center.data.fill, 1.0, 0.0)).toBe(getHeight(south.data.fill, 1.0, 1.0)); + expect(getHeight(center.data.fill, 1.0, 0.0)).toBe(getHeight(east.data.fill, 0.0, 0.0)); + expect(getHeight(center.data.fill, 1.0, 1.0)).toBe(getHeight(east.data.fill, 0.0, 1.0)); + expect(getHeight(east.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 1.0, 1.0)); + expect(getHeight(south.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 0.0, 0.0)); + + // Now load the south tile. + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 31.0, 32.0, 33.0, + 38.0, 39.0, 40.0, + 45.0, 46.0, 47.0 + ]) + }), south).createMeshWillSucceed(south); + + return processor.process(tiles); + }).then(function() { + tiles.forEach(markRendered); + + TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState); + center.data.fill.update(tileProvider, frameState); + east.data.fill.update(tileProvider, frameState); + southeast.data.fill.update(tileProvider, frameState); + + expect(south.data.fill).toBeUndefined(); + + expectVertexCount(center.data.fill, 8); + expectVertex(center.data.fill, 0.0, 0.0, 31.0); + expectVertex(center.data.fill, 0.0, 0.5, 24.0); + expectVertex(center.data.fill, 0.0, 1.0, 17.0); + expectVertex(center.data.fill, 0.5, 1.0, 18.0); + expectVertex(center.data.fill, 1.0, 1.0, 19.0); + expectVertex(center.data.fill, 1.0, 0.0, 33.0); + + expectVertexCount(east.data.fill, 6); + expectVertex(east.data.fill, 0.0, 1.0, 19.0); + expectVertex(east.data.fill, 0.5, 1.0, 20.0); + expectVertex(east.data.fill, 1.0, 1.0, 21.0); + expectVertex(east.data.fill, 0.0, 0.0, 33.0); + + expectVertexCount(southeast.data.fill, 6); + expectVertex(southeast.data.fill, 0.0, 0.0, 47.0); + expectVertex(southeast.data.fill, 0.0, 0.5, 40.0); + expectVertex(southeast.data.fill, 0.0, 1.0, 33.0); + + expect(getHeight(east.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 1.0, 1.0)); + }); + }); + // Mark all the tiles rendered. function markRendered(tile) { + quadtree._lastSelectionFrameNumber = frameState.frameNumber; tile._lastSelectionResultFrame = frameState.frameNumber; tile._lastSelectionResult = TileSelectionResult.RENDERED; @@ -616,60 +748,85 @@ defineSuite([ var southW = south.northwestChild.northeastChild; var southE = south.northeastChild.northwestChild; - mockTerrain.requestTileGeometry.and.callFake(function(x, y, level) { - var buffer = new Float32Array(4); - if (level === westN.level && x === westN.x && y === westN.y) { - buffer[0] = 1.0; - buffer[1] = 1.0; - buffer[2] = 1.5; - buffer[3] = 1.5; - } else if (level === westS.level && x === westS.x && y === westS.y) { - buffer[0] = 1.5; - buffer[1] = 1.5; - buffer[2] = 2.0; - buffer[3] = 2.0; - } else if (level === eastN.level && x === eastN.x && y === eastN.y) { - buffer[0] = 3.0; - buffer[1] = 3.0; - buffer[2] = 3.5; - buffer[3] = 3.5; - } else if (level === eastS.level && x === eastS.x && y === eastS.y) { - buffer[0] = 3.5; - buffer[1] = 3.5; - buffer[2] = 4.0; - buffer[3] = 4.0; - } else if (level === northW.level && x === northW.x && y === northW.y) { - buffer[0] = 5.0; - buffer[1] = 5.5; - buffer[2] = 5.0; - buffer[3] = 5.5; - } else if (level === northE.level && x === northE.x && y === northE.y) { - buffer[0] = 5.5; - buffer[1] = 6.0; - buffer[2] = 5.5; - buffer[3] = 6.0; - } else if (level === southW.level && x === southW.x && y === southW.y) { - buffer[0] = 7.0; - buffer[1] = 7.5; - buffer[2] = 7.0; - buffer[3] = 7.5; - } else if (level === southE.level && x === southE.x && y === southE.y) { - buffer[0] = 7.5; - buffer[1] = 8.0; - buffer[2] = 7.5; - buffer[3] = 8.0; - } else { - return undefined; - } - - var terrainData = new HeightmapTerrainData({ - width: 2, - height: 2, - buffer: buffer, - createdByUpsampling: false - }); - return when(terrainData); - }); + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 1.0, 1.0, + 1.5, 1.5 + ]) + }), westN).createMeshWillSucceed(westN); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 1.5, 1.5, + 2.0, 2.0 + ]) + }), westS).createMeshWillSucceed(westS); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 3.0, 3.0, + 3.5, 3.5 + ]) + }), eastN).createMeshWillSucceed(eastN); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 3.5, 3.5, + 4.0, 4.0 + ]) + }), eastS).createMeshWillSucceed(eastS); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 5.0, 5.5, + 5.0, 5.5 + ]) + }), northW).createMeshWillSucceed(northW); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 5.5, 6.0, + 6.5, 6.0 + ]) + }), northE).createMeshWillSucceed(northE); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 7.0, 7.5, + 7.0, 7.5 + ]) + }), southW).createMeshWillSucceed(southW); + + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 2, + height: 2, + createdByUpsampling: false, + buffer: new Float32Array([ + 7.5, 8.0, + 7.5, 8.0 + ]) + }), southE).createMeshWillSucceed(southE); return processor.process([center, westN, westS, eastN, eastS, northE, northW, southE, southW]).then(function() { var fill = center.data.fill = new TerrainFillMesh(center); @@ -715,32 +872,17 @@ defineSuite([ }); it('western hemisphere to eastern hemisphere', function() { - mockTerrain.requestTileGeometry.and.callFake(function(x, y, level) { - var buffer = new Float32Array(9); - if (x === easternHemisphere.x) { - // eastern hemisphere tile - return undefined; - } - - // western hemisphere tile - buffer[0] = 1.0; - buffer[1] = 2.0; - buffer[2] = 3.0; - buffer[3] = 4.0; - buffer[4] = 5.0; - buffer[5] = 6.0; - buffer[6] = 7.0; - buffer[7] = 8.0; - buffer[8] = 9.0; - - var terrainData = new HeightmapTerrainData({ - width: 3, - height: 3, - buffer: buffer, - createdByUpsampling: false - }); - return when(terrainData); - }); + mockTerrain.requestTileGeometryWillDefer(easternHemisphere); + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ]) + }), westernHemisphere).createMeshWillSucceed(westernHemisphere); return processor.process([westernHemisphere, easternHemisphere]).then(function() { var fill = easternHemisphere.data.fill = new TerrainFillMesh(easternHemisphere); @@ -764,32 +906,17 @@ defineSuite([ }); it('eastern hemisphere to western hemisphere', function() { - mockTerrain.requestTileGeometry.and.callFake(function(x, y, level) { - var buffer = new Float32Array(9); - if (x === westernHemisphere.x) { - // western hemisphere tile - return undefined; - } - - // eastern hemisphere tile - buffer[0] = 10.0; - buffer[1] = 11.0; - buffer[2] = 12.0; - buffer[3] = 13.0; - buffer[4] = 14.0; - buffer[5] = 15.0; - buffer[6] = 16.0; - buffer[7] = 17.0; - buffer[8] = 18.0; - - var terrainData = new HeightmapTerrainData({ - width: 3, - height: 3, - buffer: buffer, - createdByUpsampling: false - }); - return when(terrainData); - }); + mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({ + width: 3, + height: 3, + createdByUpsampling: false, + buffer: new Float32Array([ + 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, + 16.0, 17.0, 18.0 + ]) + }), easternHemisphere).createMeshWillSucceed(easternHemisphere); + mockTerrain.requestTileGeometryWillDefer(westernHemisphere); return processor.process([westernHemisphere, easternHemisphere]).then(function() { var fill = westernHemisphere.data.fill = new TerrainFillMesh(westernHemisphere); @@ -818,7 +945,7 @@ defineSuite([ var positionScratch = new Cartesian3(); var expectedPositionScratch = new Cartesian3(); - function expectVertex(fill, u, v, height) { + function getHeight(fill, u, v) { var mesh = fill.mesh; var rectangle = fill.tile.rectangle; var encoding = mesh.encoding; @@ -831,19 +958,22 @@ defineSuite([ var vertexHeight = encoding.decodeHeight(vertices, i); var vertexPosition = encoding.decodePosition(vertices, i, positionScratch); if (Math.abs(u - tc.x) < 1e-5 && Math.abs(v - tc.y) < CesiumMath.EPSILON5) { - expect(vertexHeight).toEqualEpsilon(height, CesiumMath.EPSILON5); - var longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); var latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); var expectedPosition = Cartesian3.fromRadians(longitude, latitude, vertexHeight, undefined, expectedPositionScratch); expect(vertexPosition).toEqualEpsilon(expectedPosition, 1); - return; + return vertexHeight; } } fail('Vertex with u=' + u + ', v=' + v + ' does not exist.'); } + function expectVertex(fill, u, v, height) { + var vertexHeight = getHeight(fill, u, v); + expect(vertexHeight).toEqualEpsilon(height, CesiumMath.EPSILON5); + } + function expectVertexCount(fill, count) { // A fill tile may have space allocated for extra vertices, but not all will be used. var actualCount = fill.mesh.indices.reduce(function(high, current) { diff --git a/Specs/TerrainTileProcessor.js b/Specs/TerrainTileProcessor.js index 0270d17504d..97574b03fa7 100644 --- a/Specs/TerrainTileProcessor.js +++ b/Specs/TerrainTileProcessor.js @@ -62,6 +62,7 @@ define([ function next() { ++iterations; + ++that.frameState.frameNumber; // Keep going until all terrain and imagery provider are ready and states are no longer changing. var changed = !that.terrainProvider.ready; From 3bd110bb68703bdba7db0121d67a02c3dc591c43 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 13 Jan 2019 22:16:42 +1100 Subject: [PATCH 090/131] Fix some fill tile generation issues. --- Source/Scene/QuadtreePrimitive.js | 3 ++- Source/Scene/TerrainFillMesh.js | 17 ++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index df5a27bc8ae..e76e9ba3293 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -351,11 +351,12 @@ define([ // Gets commands for any texture re-projections this._tileProvider.initialize(frameState); + clearTileLoadQueue(this); + if (this._debug.suspendLodUpdate) { return; } - clearTileLoadQueue(this); this._tileReplacementQueue.markStartOfRenderFrame(); }; diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 3dc3d358a2f..2403a5f4712 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -189,23 +189,23 @@ define([ } // This tile was refined, so find rendered children, if any. - // Return the tiles in clockwise order. + // Visit the tiles in counter-clockwise order. switch (tileEdge) { case TileEdge.WEST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); break; case TileEdge.EAST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); break; case TileEdge.SOUTH: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); break; case TileEdge.NORTH: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); break; case TileEdge.NORTHWEST: visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); @@ -556,7 +556,6 @@ define([ // Add a single vertex at the center of the tile. // TODO: minimumHeight and maximumHeight only reflect the corners var obb = OrientedBoundingBox.fromRectangle(rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); - var center = obb.center; var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); @@ -566,7 +565,7 @@ define([ var centerEncodedNormal = AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); var centerIndex = nextIndex; - encoding.encode(typedArray, nextIndex * encoding.getStride(), center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); + encoding.encode(typedArray, nextIndex * encoding.getStride(), obb.center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); ++nextIndex; var vertexCount = nextIndex; @@ -605,13 +604,13 @@ define([ } var mesh = new TerrainMesh( - obb.center, + encoding.center, typedArray, indices, minimumHeight, maximumHeight, BoundingSphere.fromOrientedBoundingBox(obb), - computeOccludeePoint(tileProvider, center, rectangle, maximumHeight), + computeOccludeePoint(tileProvider, obb.center, rectangle, maximumHeight), encoding.getStride(), obb, encoding, From 06ea854b5b56739501ff5795d6f05146eb251ccc Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 14 Jan 2019 22:52:57 +1100 Subject: [PATCH 091/131] Less blocky fills for low-detail tiles. --- Source/Core/HeightmapTerrainData.js | 77 ++++++++ Source/Scene/TerrainFillMesh.js | 261 +++++++++++++++------------- 2 files changed, 221 insertions(+), 117 deletions(-) diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index fd581897f17..defc28d72d6 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -278,6 +278,83 @@ define([ }); }; + /** + * @private + */ + HeightmapTerrainData.prototype._createMeshSync = function(tilingScheme, x, y, level, exaggeration) { + //>>includeStart('debug', pragmas.debug); + if (!defined(tilingScheme)) { + throw new DeveloperError('tilingScheme is required.'); + } + if (!defined(x)) { + throw new DeveloperError('x is required.'); + } + if (!defined(y)) { + throw new DeveloperError('y is required.'); + } + if (!defined(level)) { + throw new DeveloperError('level is required.'); + } + //>>includeEnd('debug'); + + var ellipsoid = tilingScheme.ellipsoid; + var nativeRectangle = tilingScheme.tileXYToNativeRectangle(x, y, level); + var rectangle = tilingScheme.tileXYToRectangle(x, y, level); + exaggeration = defaultValue(exaggeration, 1.0); + + // Compute the center of the tile for RTC rendering. + var center = ellipsoid.cartographicToCartesian(Rectangle.center(rectangle)); + + var structure = this._structure; + + var levelZeroMaxError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(ellipsoid, this._width, tilingScheme.getNumberOfXTilesAtLevel(0)); + var thisLevelMaxError = levelZeroMaxError / (1 << level); + this._skirtHeight = Math.min(thisLevelMaxError * 4.0, 1000.0); + + var result = HeightmapTessellator.computeVertices({ + heightmap : this._buffer, + structure : structure, + includeWebMercatorT : true, + width : this._width, + height : this._height, + nativeRectangle : nativeRectangle, + rectangle : rectangle, + relativeToCenter : center, + ellipsoid : ellipsoid, + skirtHeight : this._skirtHeight, + isGeographic : tilingScheme.projection instanceof GeographicProjection, + exaggeration : exaggeration + }); + + // Free memory received from server after mesh is created. + this._buffer = undefined; + + var arrayWidth = this._width; + var arrayHeight = this._height; + + if (this._skirtHeight > 0.0) { + arrayWidth += 2; + arrayHeight += 2; + } + + return new TerrainMesh( + center, + result.vertices, + TerrainProvider.getRegularGridIndices(arrayWidth, arrayHeight), + result.minimumHeight, + result.maximumHeight, + result.boundingSphere3D, + result.occludeePointInScaledSpace, + result.encoding.getStride(), + result.orientedBoundingBox, + TerrainEncoding.clone(result.encoding), + exaggeration, + result.westIndicesSouthToNorth, + result.southIndicesEastToWest, + result.eastIndicesNorthToSouth, + result.northIndicesWestToEast); + }; + /** * Computes the terrain height at a specified longitude and latitude. * diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 2403a5f4712..17972f6a14c 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -6,6 +6,7 @@ define([ '../Core/Cartesian3', '../Core/Cartographic', '../Core/defined', + '../Core/HeightmapTerrainData', '../Core/Math', '../Core/DeveloperError', '../Core/OrientedBoundingBox', @@ -26,6 +27,7 @@ define([ Cartesian3, Cartographic, defined, + HeightmapTerrainData, CesiumMath, DeveloperError, OrientedBoundingBox, @@ -485,148 +487,157 @@ define([ var middleHeight = (minimumHeight + maximumHeight) * 0.5; - var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, true, true); - - var centerCartographic = centerCartographicScratch; - centerCartographic.longitude = (rectangle.east + rectangle.west) * 0.5; - centerCartographic.latitude = (rectangle.north + rectangle.south) * 0.5; - centerCartographic.height = middleHeight; - encoding.center = ellipsoid.cartographicToCartesian(centerCartographic, encoding.center); + fill.mesh = undefined; + + // For low-detail tiles, our usual fill tile approach will create tiles that + // look really blocky because they don't have enough vertices to account for the + // Earth's curvature. But the height range will also typically be well within + // the allowed geometric error for those levels. So fill such tiles with a + // constant-height heightmap. + var boundingRegion = surfaceTile.tileBoundingRegion; + if (tile.level <= 4 && defined(boundingRegion)) { + var heightRange = boundingRegion.maximumHeight - boundingRegion.minimumHeight; + var geometricError = tileProvider.getLevelMaximumGeometricError(tile.level); + if (geometricError >= heightRange) { + var terrainData = new HeightmapTerrainData({ + width: 9, + height: 9, + buffer: new Uint8Array(9 * 9), + structure: { + heightOffset: middleHeight + } + }); + fill.mesh = terrainData._createMeshSync(tile.tilingScheme, tile.x, tile.y, tile.level, 1.0); + } + } - // At _most_, we have vertices for the 4 corners, plus 1 center, plus every adjacent edge vertex. - // In reality there will be less most of the time, but close enough; better - // to overestimate than to re-allocate/copy/traverse the vertices twice. - var maxVertexCount = 5; var i; var len; - var meshes; - meshes = fill.westMeshes; - for (i = 0, len = meshes.length; i < len; ++i) { - maxVertexCount += meshes[i].eastIndicesNorthToSouth.length; - } + if (!defined(fill.mesh)) { + var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, true, true); - meshes = fill.southMeshes; - for (i = 0, len = meshes.length; i < len; ++i) { - maxVertexCount += meshes[i].northIndicesWestToEast.length; - } + var centerCartographic = centerCartographicScratch; + centerCartographic.longitude = (rectangle.east + rectangle.west) * 0.5; + centerCartographic.latitude = (rectangle.north + rectangle.south) * 0.5; + centerCartographic.height = middleHeight; + encoding.center = ellipsoid.cartographicToCartesian(centerCartographic, encoding.center); - meshes = fill.eastMeshes; - for (i = 0, len = meshes.length; i < len; ++i) { - maxVertexCount += meshes[i].westIndicesSouthToNorth.length; - } + // At _most_, we have vertices for the 4 corners, plus 1 center, plus every adjacent edge vertex. + // In reality there will be less most of the time, but close enough; better + // to overestimate than to re-allocate/copy/traverse the vertices twice. + var maxVertexCount = 5; + var meshes; - meshes = fill.northMeshes; - for (i = 0, len = meshes.length; i < len; ++i) { - maxVertexCount += meshes[i].southIndicesEastToWest.length; - } + meshes = fill.westMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].eastIndicesNorthToSouth.length; + } - var typedArray = new Float32Array(maxVertexCount * encoding.getStride()); + meshes = fill.southMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].northIndicesWestToEast.length; + } - function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT) { - var cartographic = cartographicScratch; - cartographic.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); - cartographic.latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); - cartographic.height = height; - var position = ellipsoid.cartographicToCartesian(cartographic, cartesianScratch); + meshes = fill.eastMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].westIndicesSouthToNorth.length; + } - var uv = uvScratch2; - uv.x = u; - uv.y = v; + meshes = fill.northMeshes; + for (i = 0, len = meshes.length; i < len; ++i) { + maxVertexCount += meshes[i].southIndicesEastToWest.length; + } - encoding.encode(buffer, index * encoding.getStride(), position, uv, height, encodedNormal, webMercatorT); + var typedArray = new Float32Array(maxVertexCount * encoding.getStride()); + + var nextIndex = 0; + var northwestIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 1.0, nwCorner.height, nwCorner.encodedNormal, 1.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.westTiles, fill.westMeshes, TileEdge.EAST); + var southwestIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 0.0, swCorner.height, swCorner.encodedNormal, 0.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.southTiles, fill.southMeshes, TileEdge.NORTH); + var southeastIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 0.0, seCorner.height, seCorner.encodedNormal, 0.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.eastTiles, fill.eastMeshes, TileEdge.WEST); + var northeastIndex = nextIndex; + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 1.0, neCorner.height, neCorner.encodedNormal, 1.0); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.northTiles, fill.northMeshes, TileEdge.SOUTH); + + // Add a single vertex at the center of the tile. + // TODO: minimumHeight and maximumHeight only reflect the corners + var obb = OrientedBoundingBox.fromRectangle(rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); + + var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); + var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); + var centerWebMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(centerCartographic.latitude) - southMercatorY) * oneOverMercatorHeight; + + ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); + var centerEncodedNormal = AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); + + var centerIndex = nextIndex; + encoding.encode(typedArray, nextIndex * encoding.getStride(), obb.center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); + ++nextIndex; - return index + 1; - } + var vertexCount = nextIndex; + var indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex + + var indexOut = 0; + for (i = 0; i < vertexCount - 2; ++i) { + indices[indexOut++] = centerIndex; + indices[indexOut++] = i; + indices[indexOut++] = i + 1; + } - var nextIndex = 0; - var northwestIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 1.0, nwCorner.height, nwCorner.encodedNormal, 1.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.westTiles, fill.westMeshes, TileEdge.EAST); - var southwestIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 0.0, swCorner.height, swCorner.encodedNormal, 0.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.southTiles, fill.southMeshes, TileEdge.NORTH); - var southeastIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 0.0, seCorner.height, seCorner.encodedNormal, 0.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.eastTiles, fill.eastMeshes, TileEdge.WEST); - var northeastIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 1.0, neCorner.height, neCorner.encodedNormal, 1.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.northTiles, fill.northMeshes, TileEdge.SOUTH); - - // Add a single vertex at the center of the tile. - // TODO: minimumHeight and maximumHeight only reflect the corners - var obb = OrientedBoundingBox.fromRectangle(rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); - - var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); - var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); - var centerWebMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(centerCartographic.latitude) - southMercatorY) * oneOverMercatorHeight; - - ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch); - var centerEncodedNormal = AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); - - var centerIndex = nextIndex; - encoding.encode(typedArray, nextIndex * encoding.getStride(), obb.center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); - ++nextIndex; - - var vertexCount = nextIndex; - var indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex - - var indexOut = 0; - for (i = 0; i < vertexCount - 2; ++i) { indices[indexOut++] = centerIndex; indices[indexOut++] = i; - indices[indexOut++] = i + 1; - } + indices[indexOut++] = 0; - indices[indexOut++] = centerIndex; - indices[indexOut++] = i; - indices[indexOut++] = 0; + var westIndicesSouthToNorth = []; + for (i = southwestIndex; i >= northwestIndex; --i) { + westIndicesSouthToNorth.push(i); + } - var westIndicesSouthToNorth = []; - for (i = southwestIndex; i >= northwestIndex; --i) { - westIndicesSouthToNorth.push(i); - } + var southIndicesEastToWest = []; + for (i = southeastIndex; i >= southwestIndex; --i) { + southIndicesEastToWest.push(i); + } - var southIndicesEastToWest = []; - for (i = southeastIndex; i >= southwestIndex; --i) { - southIndicesEastToWest.push(i); - } + var eastIndicesNorthToSouth = []; + for (i = northeastIndex; i >= southeastIndex; --i) { + eastIndicesNorthToSouth.push(i); + } - var eastIndicesNorthToSouth = []; - for (i = northeastIndex; i >= southeastIndex; --i) { - eastIndicesNorthToSouth.push(i); - } + var northIndicesWestToEast = []; + northIndicesWestToEast.push(0); + for (i = centerIndex - 1; i >= northeastIndex; --i) { + northIndicesWestToEast.push(i); + } - var northIndicesWestToEast = []; - northIndicesWestToEast.push(0); - for (i = centerIndex - 1; i >= northeastIndex; --i) { - northIndicesWestToEast.push(i); + fill.mesh = new TerrainMesh( + encoding.center, + typedArray, + indices, + minimumHeight, + maximumHeight, + BoundingSphere.fromOrientedBoundingBox(obb), + computeOccludeePoint(tileProvider, obb.center, rectangle, maximumHeight), + encoding.getStride(), + obb, + encoding, + frameState.terrainExaggeration, + westIndicesSouthToNorth, + southIndicesEastToWest, + eastIndicesNorthToSouth, + northIndicesWestToEast + ); } - var mesh = new TerrainMesh( - encoding.center, - typedArray, - indices, - minimumHeight, - maximumHeight, - BoundingSphere.fromOrientedBoundingBox(obb), - computeOccludeePoint(tileProvider, obb.center, rectangle, maximumHeight), - encoding.getStride(), - obb, - encoding, - frameState.terrainExaggeration, - westIndicesSouthToNorth, - southIndicesEastToWest, - eastIndicesNorthToSouth, - northIndicesWestToEast - ); - - fill.mesh = mesh; - var context = frameState.context; GlobeSurfaceTile._freeVertexArray(fill.vertexArray); - fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, mesh); + fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, fill.mesh); var tileImageryCollection = surfaceTile.imagery; @@ -665,6 +676,22 @@ define([ } } + function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT) { + var cartographic = cartographicScratch; + cartographic.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); + cartographic.latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); + cartographic.height = height; + var position = ellipsoid.cartographicToCartesian(cartographic, cartesianScratch); + + var uv = uvScratch2; + uv.x = u; + uv.y = v; + + encoding.encode(buffer, index * encoding.getStride(), position, uv, height, encodedNormal, webMercatorT); + + return index + 1; + } + var sourceRectangleScratch = new Rectangle(); function transformTextureCoordinates(sourceTile, targetTile, coordinates, result) { From 0290e3fac876ddef3e472f8fd0cb206a289b931f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 14 Jan 2019 23:51:08 +1100 Subject: [PATCH 092/131] Better math for selecting a heightmap over a normal fill. --- Source/Scene/TerrainFillMesh.js | 47 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 17972f6a14c..23d0a9cdd75 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -487,34 +487,37 @@ define([ var middleHeight = (minimumHeight + maximumHeight) * 0.5; - fill.mesh = undefined; + var i; + var len; // For low-detail tiles, our usual fill tile approach will create tiles that // look really blocky because they don't have enough vertices to account for the // Earth's curvature. But the height range will also typically be well within // the allowed geometric error for those levels. So fill such tiles with a // constant-height heightmap. - var boundingRegion = surfaceTile.tileBoundingRegion; - if (tile.level <= 4 && defined(boundingRegion)) { - var heightRange = boundingRegion.maximumHeight - boundingRegion.minimumHeight; - var geometricError = tileProvider.getLevelMaximumGeometricError(tile.level); - if (geometricError >= heightRange) { - var terrainData = new HeightmapTerrainData({ - width: 9, - height: 9, - buffer: new Uint8Array(9 * 9), - structure: { - heightOffset: middleHeight - } - }); - fill.mesh = terrainData._createMeshSync(tile.tilingScheme, tile.x, tile.y, tile.level, 1.0); - } - } - - var i; - var len; - - if (!defined(fill.mesh)) { + var geometricError = tileProvider.getLevelMaximumGeometricError(tile.level); + var minCutThroughRadius = ellipsoid.maximumRadius - geometricError; + var maxTileWidth = Math.acos(minCutThroughRadius / ellipsoid.maximumRadius) * 4.0; + + // When the tile width is greater than maxTileWidth as computed above, the error + // of a normal fill tile from globe curvature alone will exceed the allowed geometric + // error. Terrain won't change that much. However, we can allow more error than that. + // A little blockiness during load is acceptable. For the WGS84 ellipsoid and + // standard geometric error setup, the value here will have us use a heightmap + // at levels 1, 2, and 3. + maxTileWidth *= 1.5; + + if (rectangle.width > maxTileWidth) { + var terrainData = new HeightmapTerrainData({ + width: 9, + height: 9, + buffer: new Uint8Array(9 * 9), + structure: { + heightOffset: middleHeight + } + }); + fill.mesh = terrainData._createMeshSync(tile.tilingScheme, tile.x, tile.y, tile.level, 1.0); + } else { var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, true, true); var centerCartographic = centerCartographicScratch; From b58ccc9fc32d7900698703e43e8c1e635cc1f868 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 14 Jan 2019 23:53:53 +1100 Subject: [PATCH 093/131] Small optimization. --- Source/Scene/TerrainFillMesh.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 23d0a9cdd75..1ee00ec051d 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -459,6 +459,7 @@ define([ var seVertexScratch = new HeightAndNormal(); var nwVertexScratch = new HeightAndNormal(); var neVertexScratch = new HeightAndNormal(); + var heightmapBuffer = typeof Uint8Array !== 'undefined' ? new Uint8Array(9 * 9) : undefined; function createFillMesh(tileProvider, frameState, tile) { var surfaceTile = tile.data; @@ -511,7 +512,7 @@ define([ var terrainData = new HeightmapTerrainData({ width: 9, height: 9, - buffer: new Uint8Array(9 * 9), + buffer: heightmapBuffer, structure: { heightOffset: middleHeight } From a3b98e80c15bbf1da4f190718c1814ee9c671dce Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 15 Jan 2019 10:54:10 +1100 Subject: [PATCH 094/131] Fix test failures. --- Specs/Scene/TerrainFillMeshSpec.js | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 429120acb20..5d9a85b027f 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -105,7 +105,7 @@ defineSuite([ quadtree.render(frameState); rootTiles = quadtree._levelZeroTiles; - center = rootTiles[0].northeastChild.southwestChild; + center = rootTiles[0].northeastChild.southwestChild.northeastChild.southwestChild; west = center.findTileToWest(rootTiles); south = center.findTileToSouth(rootTiles); east = center.findTileToEast(rootTiles); @@ -861,14 +861,14 @@ defineSuite([ var easternHemisphere; beforeEach(function() { - westernHemisphere = rootTiles[0]; - easternHemisphere = rootTiles[1]; + westernHemisphere = rootTiles[0].southwestChild.northwestChild.southwestChild.northwestChild; + easternHemisphere = rootTiles[1].southeastChild.northeastChild.southeastChild.northeastChild; // Make sure we have a standard geographic tiling scheme with two root tiles, // the first covering the western hemisphere and the second the eastern. expect(rootTiles.length).toBe(2); expect(westernHemisphere.x).toBe(0); - expect(easternHemisphere.x).toBe(1); + expect(easternHemisphere.x).toBe(31); }); it('western hemisphere to eastern hemisphere', function() { @@ -889,19 +889,13 @@ defineSuite([ fill.eastTiles.push(westernHemisphere); fill.eastMeshes.push(westernHemisphere.data.mesh); - fill.westTiles.push(westernHemisphere); - fill.westMeshes.push(westernHemisphere.data.mesh); fill.update(tileProvider, frameState); - expectVertexCount(fill, 7); - expectVertex(fill, 0.0, 0.0, 9.0); - expectVertex(fill, 0.0, 0.5, 6.0); - expectVertex(fill, 0.0, 1.0, 3.0); + expectVertexCount(fill, 6); expectVertex(fill, 1.0, 0.0, 7.0); expectVertex(fill, 1.0, 0.5, 4.0); expectVertex(fill, 1.0, 1.0, 1.0); - expectVertex(fill, 0.5, 0.5, (1.0 + 9.0) / 2); }); }); @@ -921,21 +915,15 @@ defineSuite([ return processor.process([westernHemisphere, easternHemisphere]).then(function() { var fill = westernHemisphere.data.fill = new TerrainFillMesh(westernHemisphere); - fill.eastTiles.push(easternHemisphere); - fill.eastMeshes.push(easternHemisphere.data.mesh); fill.westTiles.push(easternHemisphere); fill.westMeshes.push(easternHemisphere.data.mesh); fill.update(tileProvider, frameState); - expectVertexCount(fill, 7); + expectVertexCount(fill, 6); expectVertex(fill, 0.0, 0.0, 18.0); expectVertex(fill, 0.0, 0.5, 15.0); expectVertex(fill, 0.0, 1.0, 12.0); - expectVertex(fill, 1.0, 0.0, 16.0); - expectVertex(fill, 1.0, 0.5, 13.0); - expectVertex(fill, 1.0, 1.0, 10.0); - expectVertex(fill, 0.5, 0.5, (10.0 + 18.0) / 2); }); }); }); From 2e1b0263695ff791855c2a662fe0082a95dfa03d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 15 Jan 2019 22:01:29 +1100 Subject: [PATCH 095/131] Allow rendering of not-fully-loaded tiles. --- Source/Scene/GlobeSurfaceTileProvider.js | 103 ++++++++++++++++++++++- Source/Scene/QuadtreePrimitive.js | 32 ++++--- 2 files changed, 123 insertions(+), 12 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 786f2073ff1..c3263b84fda 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -52,7 +52,8 @@ define([ './QuadtreeTileLoadState', './SceneMode', './ShadowMode', - './TerrainFillMesh' + './TerrainFillMesh', + './TerrainState' ], function( AttributeCompression, BoundingSphere, @@ -107,7 +108,8 @@ define([ QuadtreeTileLoadState, SceneMode, ShadowMode, - TerrainFillMesh) { + TerrainFillMesh, + TerrainState) { 'use strict'; /** @@ -666,6 +668,103 @@ define([ return childAvailable !== undefined; }; + var readyImageryScratch = []; + var canRenderTraversalStack = []; + + /** + * Determines if the given not-fully-loaded tile can be rendered without losing detail that + * was present last frame as a result of rendering descendant tiles. This method will only be + * called if this tile's descendants were rendered last frame. If the tile is fully loaded, + * it is assumed that this method will return true and it will not be called. + * @param {QuadtreeTile} tile The tile to check. + * @returns {boolean} True if the tile can be rendered without losing detail. + */ + GlobeSurfaceTileProvider.prototype.canRenderWithoutLosingDetail = function(tile, frameState) { + var surfaceTile = tile.data; + + var readyImagery = readyImageryScratch; + readyImagery.length = this._imageryLayers.length; + + var terrainReady = false; + var initialImageryState = false; + var imagery; + + if (defined(surfaceTile)) { + // We can render even with non-ready terrain as long as all our rendered descendants + // are missing terrain geometry too. i.e. if we rendered fills for more detailed tiles + // last frame, it's ok to render a fill for this tile this frame. + terrainReady = surfaceTile.terrainState === TerrainState.READY; + + // Initially assume all imagery layers are ready, unless imagery hasn't been initialized at all. + initialImageryState = true; + + imagery = surfaceTile.imagery; + } + + var i; + var len; + + for (i = 0, len = readyImagery.length; i < len; ++i) { + readyImagery[i] = initialImageryState; + } + + if (defined(imagery)) { + for (i = 0, len = imagery.length; i < len; ++i) { + var tileImagery = imagery[i]; + var loadingImagery = tileImagery.loadingImagery; + var isReady = !defined(loadingImagery) || loadingImagery.state === ImageryState.FAILED || loadingImagery.state === ImageryState.INVALID; + var layerIndex = (tileImagery.loadingImagery || tileImagery.readyImagery).imageryLayer._layerIndex; + + // For a layer to be ready, all tiles belonging to that layer must be ready. + readyImagery[layerIndex] = isReady && readyImagery[layerIndex]; + } + } + + var lastFrame = this.quadtree._lastSelectionFrameNumber; + + // Traverse the descendants looking for one with terrain or imagery that is not loaded on this tile. + var stack = canRenderTraversalStack; + stack.length = 0; + stack.push(tile.southwestChild, tile.southeastChild, tile.northwestChild, tile.northeastChild); + + while (stack.length > 0) { + var descendant = stack.pop(); + var lastFrameSelectionResult = descendant._lastSelectionResultFrame === lastFrame ? descendant._lastSelectionResult : TileSelectionResult.NONE; + + if (lastFrameSelectionResult === TileSelectionResult.RENDERED) { + var descendantSurface = descendant.data; + + if (!defined(descendantSurface)) { + // Descendant has no data, so it can't block rendering. + continue; + } + + if (!terrainReady && descendant.data.terrainState === TerrainState.READY) { + // Rendered descendant has real terrain, but we don't. Rendering is blocked. + return false; + } + + var descendantImagery = descendant.data.imagery; + for (i = 0, len = descendantImagery.length; i < len; ++i) { + var descendantTileImagery = descendantImagery[i]; + var descendantLoadingImagery = descendantTileImagery.loadingImagery; + var descendantIsReady = !defined(descendantLoadingImagery) || descendantLoadingImagery.state === ImageryState.FAILED || descendantLoadingImagery.state === ImageryState.INVALID; + var descendantLayerIndex = (descendantTileImagery.loadingImagery || descendantTileImagery.readyImagery).imageryLayer._layerIndex; + + // If this imagery tile of a descendant is ready but the layer isn't ready in this tile, + // then rendering is blocked. + if (descendantIsReady && !readyImagery[descendantLayerIndex]) { + return false; + } + } + } else if (lastFrameSelectionResult === TileSelectionResult.REFINED) { + stack.push(descendant.southwestChild, descendant.southeastChild, descendant.northwestChild, descendant.northeastChild); + } + } + + return true; + }; + var tileDirectionScratch = new Cartesian3(); /** diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index e76e9ba3293..40a435f0f0b 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -643,26 +643,40 @@ define([ var lastFrame = primitive._lastSelectionFrameNumber; var lastFrameSelectionResult = tile._lastSelectionResultFrame === lastFrame ? tile._lastSelectionResult : TileSelectionResult.NONE; + var tileProvider = primitive.tileProvider; + if (meetsSse || ancestorMeetsSse) { // This tile (or an ancestor) is the one we want to render this frame, but we'll do different things depending // on the state of this tile and on what we did _last_ frame. // We can render it if _any_ of the following are true: // 1. We rendered it (or kicked it) last frame. - // 2. a) Terrain is ready, and - // b) All necessary iagery is ready. Necessary imagery is imagery that was rendered with this tile + // 2. This tile was culled last frame, or it wasn't even visited because an ancestor was culled. + // 3. The tile is completely done loading. + // 4. a) Terrain is ready, and + // b) All necessary imagery is ready. Necessary imagery is imagery that was rendered with this tile // or any descendants last frame. Such imagery is required because rendering this tile without - // it would cause detail to disappear. But determining which imagery is actually necessary is - // challenging, so instead we are currently requiring that _all_ imagery is ready. - // 3. This tile was culled last frame, or it wasn't even visited because an ancestor was culled. + // it would cause detail to disappear. + // + // Determining condition 4 is more expensive, so we check the others first.. // // Note that even if we decide to render a tile here, it may later get "kicked" in favor of an ancestor. var oneRenderedLastFrame = TileSelectionResult.originalResult(lastFrameSelectionResult) === TileSelectionResult.RENDERED; - var twoCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE; - var threeCulledOrNotVisited = lastFrameSelectionResult === TileSelectionResult.CULLED || lastFrameSelectionResult === TileSelectionResult.NONE; + var twoCulledOrNotVisited = lastFrameSelectionResult === TileSelectionResult.CULLED || lastFrameSelectionResult === TileSelectionResult.NONE; + var threeCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE; - if (oneRenderedLastFrame || twoCompletelyLoaded || threeCulledOrNotVisited) { + var renderable = oneRenderedLastFrame || twoCulledOrNotVisited || threeCompletelyLoaded; + + if (!renderable) { + // Check the more expensive condition 4 above. This requires details of the thing + // we're rendering (e.g. the globe surface), so delegate it to the tile provider. + if (defined(tileProvider.canRenderWithoutLosingDetail)) { + renderable = tileProvider.canRenderWithoutLosingDetail(tile); + } + } + + if (renderable) { // Only load this tile if it (not just an ancestor) meets the SSE. if (meetsSse) { queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); @@ -698,8 +712,6 @@ define([ } } - var tileProvider = primitive.tileProvider; - if (tileProvider.canRefine(tile)) { var allAreUpsampled = southwestChild.upsampledFromParent && southeastChild.upsampledFromParent && northwestChild.upsampledFromParent && northeastChild.upsampledFromParent; From 193639a0e0672adb8fe5cbb2165f347ee262a831 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 16 Jan 2019 11:59:47 +1100 Subject: [PATCH 096/131] Eliminate duplicate imagery loading code. --- Source/Scene/GlobeSurfaceTile.js | 57 +++++++++++++++++++++----------- Source/Scene/TerrainFillMesh.js | 39 ++-------------------- 2 files changed, 40 insertions(+), 56 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 83a9eae19fb..6c4e91a8be3 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -240,7 +240,7 @@ define([ this.wireframeVertexArray = undefined; }; - GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, terrainOnly) { + GlobeSurfaceTile.initialize = function(tile, terrainProvider, imageryLayerCollection) { var surfaceTile = tile.data; if (!defined(surfaceTile)) { surfaceTile = tile.data = new GlobeSurfaceTile(); @@ -250,6 +250,12 @@ define([ prepareNewTile(tile, terrainProvider, imageryLayerCollection); tile.state = QuadtreeTileLoadState.LOADING; } + }; + + GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, terrainOnly) { + GlobeSurfaceTile.initialize(tile, terrainProvider, imageryLayerCollection); + + var surfaceTile = tile.data; if (defined(surfaceTile.boundingVolumeSourceTile) && surfaceTile.boundingVolumeSourceTile !== tile && terrainProvider.getNearestBvhLevel !== undefined) { // So here we are loading this tile, but we know our bounding volume isn't very good, and so our @@ -286,14 +292,38 @@ define([ } // The terrain is renderable as soon as we have a valid vertex array. - var isRenderable = defined(surfaceTile.vertexArray); + tile.renderable = defined(surfaceTile.vertexArray); // But it's not done loading until it's in the READY state. - var isDoneLoading = surfaceTile.terrainState === TerrainState.READY; + var isTerrainDoneLoading = surfaceTile.terrainState === TerrainState.READY; // If this tile's terrain and imagery are just upsampled from its parent, mark the tile as // upsampled only. We won't refine a tile if its four children are upsampled only. - var isUpsampledOnly = defined(surfaceTile.terrainData) && surfaceTile.terrainData.wasCreatedByUpsampling(); + tile.upsampledFromParent = defined(surfaceTile.terrainData) && surfaceTile.terrainData.wasCreatedByUpsampling(); + + var isImageryDoneLoading = surfaceTile.processImagery(tile, terrainProvider, frameState); + + if (isTerrainDoneLoading && isImageryDoneLoading) { + var callbacks = tile._loadedCallbacks; + var newCallbacks = {}; + for(var layerId in callbacks) { + if (callbacks.hasOwnProperty(layerId)) { + if(!callbacks[layerId](tile)) { + newCallbacks[layerId] = callbacks[layerId]; + } + } + } + tile._loadedCallbacks = newCallbacks; + + tile.state = QuadtreeTileLoadState.DONE; + } + }; + + GlobeSurfaceTile.prototype.processImagery = function(tile, terrainProvider, frameState, skipLoading) { + var surfaceTile = tile.data; + var isUpsampledOnly = tile.upsampledFromParent; + var isRenderable = tile.renderable; + var isDoneLoading = true; // Transition imagery states var tileImageryCollection = surfaceTile.imagery; @@ -321,7 +351,7 @@ define([ } } - var thisTileDoneLoading = tileImagery.processStateMachine(tile, frameState); + var thisTileDoneLoading = tileImagery.processStateMachine(tile, frameState, skipLoading); isDoneLoading = isDoneLoading && thisTileDoneLoading; // The imagery is renderable as soon as we have any renderable imagery for this region. @@ -331,23 +361,10 @@ define([ (tileImagery.loadingImagery.state === ImageryState.FAILED || tileImagery.loadingImagery.state === ImageryState.INVALID); } - tile.renderable = isRenderable; tile.upsampledFromParent = isUpsampledOnly; + tile.renderable = isRenderable; - if (isDoneLoading) { - var callbacks = tile._loadedCallbacks; - var newCallbacks = {}; - for(var layerId in callbacks) { - if (callbacks.hasOwnProperty(layerId)) { - if(!callbacks[layerId](tile)) { - newCallbacks[layerId] = callbacks[layerId]; - } - } - } - tile._loadedCallbacks = newCallbacks; - - tile.state = QuadtreeTileLoadState.DONE; - } + return isDoneLoading; }; function prepareNewTile(tile, terrainProvider, imageryLayerCollection) { diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 1ee00ec051d..6dc3de4b336 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -462,6 +462,8 @@ define([ var heightmapBuffer = typeof Uint8Array !== 'undefined' ? new Uint8Array(9 * 9) : undefined; function createFillMesh(tileProvider, frameState, tile) { + GlobeSurfaceTile.initialize(tile, tileProvider.terrainProvider, tileProvider._imageryLayers); + var surfaceTile = tile.data; var fill = surfaceTile.fill; var rectangle = tile.rectangle; @@ -642,42 +644,7 @@ define([ GlobeSurfaceTile._freeVertexArray(fill.vertexArray); fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, fill.mesh); - - var tileImageryCollection = surfaceTile.imagery; - - if (tileImageryCollection.length === 0) { - var imageryLayerCollection = tileProvider._imageryLayers; - var terrainProvider = tileProvider.terrainProvider; - for (i = 0, len = imageryLayerCollection.length; i < len; ++i) { - var layer = imageryLayerCollection.get(i); - if (layer.show) { - layer._createTileImagerySkeletons(tile, terrainProvider); - } - } - } - - for (i = 0, len = tileImageryCollection.length; i < len; ++i) { - var tileImagery = tileImageryCollection[i]; - if (!defined(tileImagery.loadingImagery)) { - continue; - } - - if (tileImagery.loadingImagery.state === ImageryState.PLACEHOLDER) { - var imageryLayer = tileImagery.loadingImagery.imageryLayer; - if (imageryLayer.imageryProvider.ready) { - // Remove the placeholder and add the actual skeletons (if any) - // at the same position. Then continue the loop at the same index. - tileImagery.freeResources(); - tileImageryCollection.splice(i, 1); - imageryLayer._createTileImagerySkeletons(tile, tileProvider.terrainProvider, i); - --i; - len = tileImageryCollection.length; - continue; - } - } - - tileImagery.processStateMachine(tile, frameState, true); - } + surfaceTile.processImagery(tile, tileProvider.terrainProvider, frameState, true); } function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT) { From a40657b3ff820ab31f5ab31b891d448d5f8ee0a5 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 16 Jan 2019 15:16:28 +1100 Subject: [PATCH 097/131] Tweak math for using heightmap fills, again. --- Source/Scene/TerrainFillMesh.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 6dc3de4b336..65dcd7db153 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -510,13 +510,15 @@ define([ // at levels 1, 2, and 3. maxTileWidth *= 1.5; - if (rectangle.width > maxTileWidth) { + if (rectangle.width > maxTileWidth && (maximumHeight - minimumHeight) <= geometricError) { var terrainData = new HeightmapTerrainData({ width: 9, height: 9, buffer: heightmapBuffer, structure: { - heightOffset: middleHeight + // Use the maximum as the constant height so that this tile's skirt + // covers any cracks with adjacent tiles. + heightOffset: maximumHeight } }); fill.mesh = terrainData._createMeshSync(tile.tilingScheme, tile.x, tile.y, tile.level, 1.0); From 792610d718c7d6f7c50111063ed578abb4ddf9c7 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 16 Jan 2019 23:34:30 +1100 Subject: [PATCH 098/131] Don't let the globe vanish when adding a layer. --- Source/Scene/GlobeSurfaceTile.js | 10 ++++++++++ Source/Scene/GlobeSurfaceTileProvider.js | 6 ++++++ Source/Scene/QuadtreePrimitive.js | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 6c4e91a8be3..4c5fe9b270d 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -291,6 +291,8 @@ define([ return; } + var wasAlreadyRenderable = tile.renderable; + // The terrain is renderable as soon as we have a valid vertex array. tile.renderable = defined(surfaceTile.vertexArray); @@ -317,6 +319,14 @@ define([ tile.state = QuadtreeTileLoadState.DONE; } + + // Once a tile is renderable, it stays renderable, because doing otherwise would + // cause detail (or maybe even the entire globe) to vanish when adding a new + // imagery layer. `GlobeSurfaceTileProvider._onLayerAdded` sets renderable to + // false for all affected tiles that are not currently being rendered. + if (wasAlreadyRenderable) { + tile.renderable = true; + } }; GlobeSurfaceTile.prototype.processImagery = function(tile, terrainProvider, frameState, skipLoading) { diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index c3263b84fda..470ba650a4e 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1141,6 +1141,12 @@ define([ this._quadtree.forEachLoadedTile(function(tile) { if (layer._createTileImagerySkeletons(tile, terrainProvider)) { tile.state = QuadtreeTileLoadState.LOADING; + + // Tiles that are not currently being rendered need to load the new layer before they're renderable. + // We don't mark the rendered tiles non-renderable, though, because that would make the globe disappear. + if (tile.level !== 0 && (tile._lastSelectionResultFrame !== that.quadtree._lastSelectionFrameNumber || tile._lastSelectionResult !== TileSelectionResult.RENDERED)) { + tile.renderable = false; + } } }); diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 40a435f0f0b..9723beb4d4b 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -549,8 +549,8 @@ define([ for (i = 0, len = levelZeroTiles.length; i < len; ++i) { tile = levelZeroTiles[i]; primitive._tileReplacementQueue.markTileRendered(tile); + queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); if (!tile.renderable) { - queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); ++debug.tilesWaitingForChildren; } else { visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile, false, rootTraversalDetails[i]); From 02a2209c68df00051583b7b5463f4801f6dc432e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 17 Jan 2019 12:24:44 +1100 Subject: [PATCH 099/131] Adjust root tile loading to reduce flickering when adding new layers. --- Source/Scene/QuadtreePrimitive.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 9723beb4d4b..3b0750ac37d 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -549,11 +549,12 @@ define([ for (i = 0, len = levelZeroTiles.length; i < len; ++i) { tile = levelZeroTiles[i]; primitive._tileReplacementQueue.markTileRendered(tile); - queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); if (!tile.renderable) { + queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); ++debug.tilesWaitingForChildren; } else { visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile, false, rootTraversalDetails[i]); + queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } } From f76ee90671d4e1814494996cfba5a80f6ff5823b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 17 Jan 2019 13:35:20 +1100 Subject: [PATCH 100/131] Fix test failures. --- Source/Scene/QuadtreePrimitive.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 3b0750ac37d..3e2bb4239f7 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -554,7 +554,6 @@ define([ ++debug.tilesWaitingForChildren; } else { visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile, false, rootTraversalDetails[i]); - queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } } @@ -895,7 +894,9 @@ define([ traversalDetails.anyWereRenderedLastFrame = false; traversalDetails.notYetRenderableCount = 0; - if (primitive.preloadSiblings) { + // Load culled level zero tiles with low priority. + // For all other levels, only load culled tiles if preloadSiblings is enabled. + if (primitive.preloadSiblings || tile.level === 0) { queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } } From 4b9b62d70b49420610acb074dd23a0b9631e3977 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 17 Jan 2019 17:22:41 +1100 Subject: [PATCH 101/131] Better debug stats. --- Source/Scene/QuadtreePrimitive.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 3e2bb4239f7..f754b1e800f 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -80,12 +80,14 @@ define([ enableDebugOutput : false, maxDepth : 0, + maxDepthVisited : 0, tilesVisited : 0, tilesCulled : 0, tilesRendered : 0, tilesWaitingForChildren : 0, lastMaxDepth : -1, + lastMaxDepthVisited : -1, lastTilesVisited : -1, lastTilesCulled : -1, lastTilesRendered : -1, @@ -139,7 +141,7 @@ define([ * its descendants are loaded and rendered. This means more feedback for the user that something * is happening at the cost of a longer overall load time. Setting this to 0 will cause each * tile level to be loaded successively, significantly increasing load time. Setting it to a large - * number (e.g. 100000) will minimize the number of tiles that are loaded but tend to make + * number (e.g. 1000) will minimize the number of tiles that are loaded but tend to make * detail appear all at once after a long wait. * @type {Number} * @default 20 @@ -323,6 +325,7 @@ define([ function clearTileLoadQueue(primitive) { var debug = primitive._debug; debug.maxDepth = 0; + debug.maxDepthVisited = 0; debug.tilesVisited = 0; debug.tilesCulled = 0; debug.tilesRendered = 0; @@ -395,20 +398,27 @@ define([ } var debug = primitive._debug; - if (debug.enableDebugOutput && !debug.suspendLodUpdate) { + if (debug.enableDebugOutput && !debug.suspendLodUpdate) { + debug.maxDepth = primitive._tilesToRender.reduce(function(max, tile) { + return Math.max(max, tile.level); + }, -1); + debug.tilesRendered = primitive._tilesToRender.length; + if (debug.tilesVisited !== debug.lastTilesVisited || debug.tilesRendered !== debug.lastTilesRendered || debug.tilesCulled !== debug.lastTilesCulled || debug.maxDepth !== debug.lastMaxDepth || - debug.tilesWaitingForChildren !== debug.lastTilesWaitingForChildren) { + debug.tilesWaitingForChildren !== debug.lastTilesWaitingForChildren || + debug.maxDepthVisited !== debug.lastMaxDepthVisited) { - console.log('Visited ' + debug.tilesVisited + ', Rendered: ' + debug.tilesRendered + ', Culled: ' + debug.tilesCulled + ', Max Depth: ' + debug.maxDepth + ', Waiting for children: ' + debug.tilesWaitingForChildren); + console.log('Visited ' + debug.tilesVisited + ', Rendered: ' + debug.tilesRendered + ', Culled: ' + debug.tilesCulled + ', Max Depth Rendered: ' + debug.maxDepth + ', Max Depth Visited: ' + debug.maxDepthVisited + ', Waiting for children: ' + debug.tilesWaitingForChildren); debug.lastTilesVisited = debug.tilesVisited; debug.lastTilesRendered = debug.tilesRendered; debug.lastTilesCulled = debug.tilesCulled; debug.lastMaxDepth = debug.maxDepth; debug.lastTilesWaitingForChildren = debug.tilesWaitingForChildren; + debug.lastMaxDepthVisited = debug.maxDepthVisited; } } } @@ -625,8 +635,8 @@ define([ primitive._tileReplacementQueue.markTileRendered(tile); tile._updateCustomData(frameState.frameNumber); - if (tile.level > debug.maxDepth) { - debug.maxDepth = tile.level; + if (tile.level > debug.maxDepthVisited) { + debug.maxDepthVisited = tile.level; } if (tile.renderable) { @@ -782,7 +792,7 @@ define([ } } - // Remove all descendants from the render list and update list. + // Remove all descendants from the render list and add this tile. primitive._tilesToRender.length = firstRenderedDescendantIndex; primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; primitive._tileToUpdateHeights.length = tilesToUpdateHeightsIndex; @@ -946,7 +956,6 @@ define([ function addTileToRenderList(primitive, tile, nearestRenderableTile) { primitive._tilesToRender.push(tile); primitive._nearestRenderableTiles.push(nearestRenderableTile); - ++primitive._debug.tilesRendered; } function processTileLoadQueue(primitive, frameState) { From cffcbb0b7a6d219792a03d5355b6e636a7bc340d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 17 Jan 2019 21:01:55 +1100 Subject: [PATCH 102/131] Push parent tiles to be completely upsamplable. --- Source/Core/QuantizedMeshTerrainData.js | 6 ++++++ Source/Scene/GlobeSurfaceTile.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index 1f3857fe07a..e1b34be2967 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -240,6 +240,12 @@ define([ get : function() { return this._bvh; } + }, + + canUpsample : { + get : function() { + return defined(this._mesh); + } } }); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 4c5fe9b270d..c6eff8413b5 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -399,7 +399,7 @@ define([ // ready for that, let's push it along. var parent = tile.parent; if (surfaceTile.terrainState === TerrainState.FAILED && parent !== undefined) { - var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined; + var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined && parent.data.terrainData.canUpsample !== false; if (!parentReady) { GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, true); } From 64d17a25ceb4c478b9e660472cbe23a47cf74d5f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 17 Jan 2019 22:07:11 +1100 Subject: [PATCH 103/131] Nice little memory optimization. --- Source/Core/IndexDatatype.js | 21 +++++++++++++++++++ Source/Core/TerrainMesh.js | 4 ++-- Source/Scene/GlobeSurfaceTile.js | 3 +-- Source/Scene/GlobeSurfaceTileProvider.js | 6 ++++-- Source/Scene/TerrainFillMesh.js | 26 +++++++++++++++++++++--- Specs/Scene/TerrainFillMeshSpec.js | 6 +----- 6 files changed, 52 insertions(+), 14 deletions(-) diff --git a/Source/Core/IndexDatatype.js b/Source/Core/IndexDatatype.js index f3ac0e8f0ae..a3bfb2779da 100644 --- a/Source/Core/IndexDatatype.js +++ b/Source/Core/IndexDatatype.js @@ -72,6 +72,27 @@ define([ //>>includeEnd('debug'); }; + /** + * Gets the datatype with a given size in bytes. + * + * @param {Number} sizeInBytes The size of a single index in bytes. + * @returns {IndexDatatype} The index datatype with the given size. + */ + IndexDatatype.fromSizeInBytes = function(sizeInBytes) { + switch (sizeInBytes) { + case 2: + return IndexDatatype.UNSIGNED_SHORT; + case 4: + return IndexDatatype.UNSIGNED_INT; + case 1: + return IndexDatatype.UNSIGNED_BYTE; + //>>includeStart('debug', pragmas.debug); + default: + throw new DeveloperError('Size in bytes cannot be mapped to an IndexDatatype'); + //>>includeEnd('debug'); + } + }; + /** * Validates that the provided index datatype is a valid {@link IndexDatatype}. * diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js index bc880615db6..5f69f8fc90f 100644 --- a/Source/Core/TerrainMesh.js +++ b/Source/Core/TerrainMesh.js @@ -18,7 +18,7 @@ define([ * The vertex data is in the order [X, Y, Z, H, U, V], where X, Y, and Z represent * the Cartesian position of the vertex, H is the height above the ellipsoid, and * U and V are the texture coordinates. - * @param {Uint16Array|Uint32Array} indices The indices describing how the vertices are connected to form triangles. + * @param {Uint8Array|Uint16Array|Uint32Array} indices The indices describing how the vertices are connected to form triangles. * @param {Number} minimumHeight The lowest height in the tile, in meters above the ellipsoid. * @param {Number} maximumHeight The highest height in the tile, in meters above the ellipsoid. * @param {BoundingSphere} boundingSphere3D A bounding sphere that completely contains the tile. @@ -68,7 +68,7 @@ define([ /** * The indices describing how the vertices are connected to form triangles. - * @type {Uint16Array|Uint32Array} + * @type {Uint8Array|Uint16Array|Uint32Array} */ this.indices = indices; diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index c6eff8413b5..b879778f3d0 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -555,12 +555,11 @@ define([ var indexBuffer = indexBuffers[context.id]; if (!defined(indexBuffer) || indexBuffer.isDestroyed()) { var indices = mesh.indices; - var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ? IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT; indexBuffer = Buffer.createIndexBuffer({ context : context, typedArray : indices, usage : BufferUsage.STATIC_DRAW, - indexDatatype : indexDatatype + indexDatatype : IndexDatatype.fromSizeInBytes(indices.BYTES_PER_ELEMENT) }); indexBuffer.vertexArrayDestroyable = false; indexBuffer.referenceCount = 1; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 470ba650a4e..acdc178a3f1 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1422,8 +1422,10 @@ define([ * @returns {VertexArray} The vertex array for wireframe rendering. */ function createWireframeVertexArray(context, vertexArray, terrainMesh) { + var indices = terrainMesh.indices; + var geometry = { - indices : terrainMesh.indices, + indices : indices, primitiveType : PrimitiveType.TRIANGLES }; @@ -1434,7 +1436,7 @@ define([ context : context, typedArray : wireframeIndices, usage : BufferUsage.STATIC_DRAW, - indexDatatype : IndexDatatype.UNSIGNED_SHORT + indexDatatype : IndexDatatype.fromSizeInBytes(wireframeIndices.BYTES_PER_ELEMENT) }); return new VertexArray({ context : context, diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 65dcd7db153..0bee301ef45 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -534,6 +534,7 @@ define([ // At _most_, we have vertices for the 4 corners, plus 1 center, plus every adjacent edge vertex. // In reality there will be less most of the time, but close enough; better // to overestimate than to re-allocate/copy/traverse the vertices twice. + // Also, we'll often be able to squeeze the index data into the extra space in the buffer. var maxVertexCount = 5; var meshes; @@ -557,7 +558,8 @@ define([ maxVertexCount += meshes[i].southIndicesEastToWest.length; } - var typedArray = new Float32Array(maxVertexCount * encoding.getStride()); + var stride = encoding.getStride(); + var typedArray = new Float32Array(maxVertexCount * stride); var nextIndex = 0; var northwestIndex = nextIndex; @@ -585,11 +587,29 @@ define([ var centerEncodedNormal = AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch); var centerIndex = nextIndex; - encoding.encode(typedArray, nextIndex * encoding.getStride(), obb.center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); + encoding.encode(typedArray, nextIndex * stride, obb.center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT); ++nextIndex; var vertexCount = nextIndex; - var indices = new Uint16Array((vertexCount - 1) * 3); // one triangle per edge vertex + + var bytesPerIndex = vertexCount < 256 ? 1 : 2; + var indexCount = (vertexCount - 1) * 3; // one triangle per edge vertex + var indexDataBytes = indexCount * bytesPerIndex; + var availableBytesInBuffer = (typedArray.length - vertexCount * stride) * Float32Array.BYTES_PER_ELEMENT; + + var indices; + if (availableBytesInBuffer >= indexDataBytes) { + // Store the index data in the same buffer as the vertex data. + var startIndex = vertexCount * stride * Float32Array.BYTES_PER_ELEMENT; + indices = vertexCount < 256 + ? new Uint8Array(typedArray.buffer, startIndex, indexCount) + : new Uint16Array(typedArray.buffer, startIndex, indexCount); + } else { + // Allocate a new buffer for the index data. + indices = vertexCount < 256 ? new Uint8Array(indexCount) : new Uint16Array(indexCount); + } + + typedArray = new Float32Array(typedArray.buffer, 0, vertexCount * stride); var indexOut = 0; for (i = 0; i < vertexCount - 2; ++i) { diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 5d9a85b027f..3ce6a98efa2 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -963,10 +963,6 @@ defineSuite([ } function expectVertexCount(fill, count) { - // A fill tile may have space allocated for extra vertices, but not all will be used. - var actualCount = fill.mesh.indices.reduce(function(high, current) { - return Math.max(high, current); - }, -1) + 1; - expect(actualCount).toBe(count); + expect(fill.mesh.vertices.length).toBe(count * fill.mesh.encoding.getStride()); } }); From 43490c86adab7ff4589adc860a9a27d243b5bdeb Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 22 Jan 2019 22:16:03 +1100 Subject: [PATCH 104/131] Remove all the BVH code because it's providing no benefit. --- Source/Core/CesiumTerrainProvider.js | 82 +----------- Source/Core/HeightmapTerrainData.js | 13 -- Source/Core/QuantizedMeshTerrainData.js | 13 -- Source/Scene/GlobeSurfaceTile.js | 44 ------- Source/Scene/GlobeSurfaceTileProvider.js | 17 +-- Specs/MockTerrainProvider.js | 25 ---- Specs/Scene/GlobeSurfaceTileSpec.js | 153 ----------------------- 7 files changed, 2 insertions(+), 345 deletions(-) diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index a22f484f6dd..58d0dfc2339 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -62,8 +62,6 @@ define([ this.availability = layer.availability; this.hasVertexNormals = layer.hasVertexNormals; this.hasWaterMask = layer.hasWaterMask; - this.hasBvh = layer.hasBvh; - this.bvhLevels = layer.bvhLevels; this.hasMetadata = layer.hasMetadata; this.availabilityLevels = layer.availabilityLevels; this.availabilityTilesLoaded = layer.availabilityTilesLoaded; @@ -83,7 +81,6 @@ define([ * @param {Resource|String|Promise|Promise} options.url The URL of the Cesium terrain server. * @param {Boolean} [options.requestVertexNormals=false] Flag that indicates if the client should request additional lighting information from the server, in the form of per vertex normals if available. * @param {Boolean} [options.requestWaterMask=false] Flag that indicates if the client should request per tile water masks from the server, if available. - * @param {Boolean} [options.requestBvh=true] Flag that indicates if the client should request bounding-volume hierarchy information along with tiles, if available. Using volume hierarchy information should significantly improve performance; there is little reason to disable it. * @param {Boolean} [options.requestMetadata=true] Flag that indicates if the client should request per tile metadata from the server, if available. * @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If not specified, the WGS84 ellipsoid is used. * @param {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas. @@ -120,7 +117,6 @@ define([ this._heightmapStructure = undefined; this._hasWaterMask = false; this._hasVertexNormals = false; - this._hasBvh = false; /** * Boolean flag that indicates if the client should request vertex normals from the server. @@ -138,15 +134,6 @@ define([ */ this._requestWaterMask = defaultValue(options.requestWaterMask, false); - /** - * Boolean flag that indicates if the client should request tile bounding-volume hierarchy information - * from the server. - * @type {Boolean} - * @default true - * @private - */ - this._requestBvh = defaultValue(options.requestBvh, true); - /** * Boolean flag that indicates if the client should request tile metadata from the server. * @type {Boolean} @@ -214,7 +201,6 @@ define([ var hasVertexNormals = false; var hasWaterMask = false; - var hasBvh = false; var hasMetadata = false; var littleEndianExtensionSize = true; var isHeightmap = false; @@ -294,13 +280,9 @@ define([ ]; availability.addAvailableTileRange(0, 0, 0, 1, 0); } - if (defined(data.extensions) && data.extensions.indexOf('bvh') !== -1) { - hasBvh = true; - } that._hasWaterMask = that._hasWaterMask || hasWaterMask; that._hasVertexNormals = that._hasVertexNormals || hasVertexNormals; - that._hasBvh = that._hasBvh || hasBvh; that._hasMetadata = that._hasMetadata || hasMetadata; if (defined(data.attribution)) { if (attribution.length > 0) { @@ -317,8 +299,6 @@ define([ availability: availability, hasVertexNormals: hasVertexNormals, hasWaterMask: hasWaterMask, - hasBvh: hasBvh, - bvhLevels: data.bvhlevels, hasMetadata: hasMetadata, availabilityLevels: availabilityLevels, availabilityTilesLoaded: availabilityTilesLoaded, @@ -433,14 +413,6 @@ define([ * @default 2 */ WATER_MASK: 2, - /** - * A bounding-volume hierarchy is included as an extension to the tile mesh - * - * @type {Number} - * @constant - * @default 3 - */ - BVH: 3, /** * A json object contain metadata about the tile * @@ -569,7 +541,6 @@ define([ var encodedNormalBuffer; var waterMaskBuffer; - var bvh; while (pos < view.byteLength) { var extensionId = view.getUint8(pos, true); pos += Uint8Array.BYTES_PER_ELEMENT; @@ -580,19 +551,6 @@ define([ encodedNormalBuffer = new Uint8Array(buffer, pos, vertexCount * 2); } else if (extensionId === QuantizedMeshExtensionIds.WATER_MASK && provider._requestWaterMask) { waterMaskBuffer = new Uint8Array(buffer, pos, extensionLength); - } else if (extensionId === QuantizedMeshExtensionIds.BVH && provider._requestBvh) { - var extensionPos = pos; - - // Align to 4 bytes. - if (extensionPos % 4 !== 0) { - extensionPos += (4 - (extensionPos % 4)); - } - - var numberOfHeights = view.getUint32(extensionPos, true); - extensionPos += Uint32Array.BYTES_PER_ELEMENT; - - bvh = new Float32Array(buffer, extensionPos, numberOfHeights); - extensionPos += Float32Array.BYTES_PER_ELEMENT * numberOfHeights; } else if (extensionId === QuantizedMeshExtensionIds.METADATA && provider._requestMetadata) { var stringLength = view.getUint32(pos, true); if (stringLength > 0) { @@ -657,45 +615,10 @@ define([ northSkirtHeight : skirtHeight, childTileMask: provider.availability.computeChildMaskForTile(level, x, y), waterMask: waterMaskBuffer, - credits: provider._tileCredits, - bvh: bvh + credits: provider._tileCredits }); } - /** - * Gets the level of the nearest ancestor of this tile that Bounding Volume Hierarchy (BVH) - * data. - * - * @param {Number} x The X coordinate of the tile. - * @param {Number} y The Y coordinate of the tile. - * @param {Number} level The level of the tile. - * @returns {Number} The level of the nearest BVH level less than or equal to level, or -1 if no BVH data is available for this tile. - */ - CesiumTerrainProvider.prototype.getNearestBvhLevel = function(x, y, level) { - var layers = this._layers; - var layerToUse; - var layerCount = layers.length; - - if (layerCount === 1) { // Optimized path for single layers - layerToUse = layers[0]; - } else { - for (var i = 0; i < layerCount; ++i) { - var layer = layers[i]; - if (!defined(layer.availability) || layer.availability.isTileAvailable(level, x, y)) { - layerToUse = layer; - break; - } - } - } - - if (!defined(layerToUse) || !layerToUse.hasBvh) { - return -1; - } - - var bvhLevels = layerToUse.bvhLevels - 1; - return ((level / bvhLevels) | 0) * bvhLevels; - }; - /** * Requests the geometry for a given tile. This function should not be called before * {@link CesiumTerrainProvider#ready} returns true. The result must include terrain data and @@ -760,9 +683,6 @@ define([ if (provider._requestWaterMask && layerToUse.hasWaterMask) { extensionList.push('watermask'); } - if (provider._requestBvh && layerToUse.hasBvh) { - extensionList.push('bvh'); - } if (provider._requestMetadata && layerToUse.hasMetadata) { extensionList.push('metadata'); } diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index defc28d72d6..e7f012c04b1 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -53,7 +53,6 @@ define([ * @param {Uint8Array} [options.waterMask] The water mask included in this terrain data, if any. A water mask is a square * Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land. * Values in between 0 and 255 are allowed as well to smoothly blend between land and water. - * @param {Float32Array} [options.bvh] The bounding-volume hierarchy for this tile and its descendents. TODO: describe its structure * @param {Object} [options.structure] An object describing the structure of the height data. * @param {Number} [options.structure.heightScale=1.0] The factor by which to multiply height samples in order to obtain * the height above the heightOffset, in meters. The heightOffset is added to the resulting @@ -121,7 +120,6 @@ define([ this._width = options.width; this._height = options.height; this._childTileMask = defaultValue(options.childTileMask, 15); - this._bvh = options.bvh; var defaultStructure = HeightmapTessellator.DEFAULT_STRUCTURE; var structure = options.structure; @@ -173,17 +171,6 @@ define([ get : function() { return this._childTileMask; } - }, - - /** - * Gets the bounding-volume hierarchy (BVH) starting with this tile. - * @memberof HeightmapTerrainData.prototype - * @type {Float32Array} - */ - bvh : { - get : function() { - return this._bvh; - } } }); diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index e1b34be2967..0af2ae0a550 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -77,7 +77,6 @@ define([ * @param {Uint8Array} [options.encodedNormals] The buffer containing per vertex normals, encoded using 'oct' encoding * @param {Uint8Array} [options.waterMask] The buffer containing the watermask. * @param {Credit[]} [options.credits] Array of credits for this tile. - * @param {Float32Array} [options.bvh] The bounding-volume hierarchy for this tile and its descendents. TODO: describe its structure * * * @example @@ -167,7 +166,6 @@ define([ this._orientedBoundingBox = options.orientedBoundingBox; this._horizonOcclusionPoint = options.horizonOcclusionPoint; this._credits = options.credits; - this._bvh = options.bvh; var vertexCount = this._quantizedVertices.length / 3; var uValues = this._uValues = this._quantizedVertices.subarray(0, vertexCount); @@ -231,17 +229,6 @@ define([ } }, - /** - * Gets the bounding-volume hierarchy (BVH) starting with this tile. - * @memberof QuantizedMeshTerrainData.prototype - * @type {Float32Array} - */ - bvh : { - get : function() { - return this._bvh; - } - }, - canUpsample : { get : function() { return defined(this._mesh); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index b879778f3d0..376648ea6d7 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -83,7 +83,6 @@ define([ this.vertexArray = undefined; this.orientedBoundingBox = undefined; this.boundingVolumeSourceTile = undefined; - this._bvh = undefined; this.renderableTile = undefined; @@ -140,28 +139,6 @@ define([ } }); - GlobeSurfaceTile.prototype.getBoundingVolumeHierarchy = function(tile) { - if (this._bvh === undefined) { - var terrainData = this.terrainData; - if (terrainData !== undefined && terrainData.bvh !== undefined) { - this._bvh = terrainData.bvh; - } - - var parent = tile.parent; - if (parent !== undefined && parent.data !== undefined) { - var parentBvh = parent.data.getBoundingVolumeHierarchy(parent); - if (parentBvh !== undefined && parentBvh.length > 2) { - var subsetLength = (parentBvh.length - 2) / 4; - var childIndex = (tile.y === parent.y * 2 ? 2 : 0) + (tile.x === parent.x * 2 ? 0 : 1); - var start = 2 + subsetLength * childIndex; - this._bvh = parentBvh.subarray(start, start + subsetLength); - } - } - } - - return this._bvh; - }; - function getPosition(encoding, mode, projection, vertices, index, result) { encoding.decodePosition(vertices, index, result); @@ -257,26 +234,6 @@ define([ var surfaceTile = tile.data; - if (defined(surfaceTile.boundingVolumeSourceTile) && surfaceTile.boundingVolumeSourceTile !== tile && terrainProvider.getNearestBvhLevel !== undefined) { - // So here we are loading this tile, but we know our bounding volume isn't very good, and so our - // judgement that it's visible is kind of suspect. If this terrain source has bounding volume data - // outside of individual tiles, let's get our hands on that before we waste time downloading - // potentially not-actually-visible tiles like this one. - var bvhLevel = terrainProvider.getNearestBvhLevel(tile.x, tile.y, tile.level); - if (bvhLevel !== -1 && bvhLevel !== tile.level) { - var ancestor = tile.parent; - while (ancestor.level !== bvhLevel) { - ancestor = ancestor.parent; - } - - if (ancestor.data === undefined || ancestor.data.terrainData === undefined) { - // The ancestor that holds the BVH data isn't loaded yet; load it (terrain only!) instead of this tile. - GlobeSurfaceTile.processStateMachine(ancestor, frameState, terrainProvider, imageryLayerCollection, true); - return; - } - } - } - if (tile.state === QuadtreeTileLoadState.LOADING) { processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection); } @@ -285,7 +242,6 @@ define([ // we're certain that the terrain tiles are actually visible, though. We'll load terrainOnly // in these scenarios: // * our bounding volume isn't accurate so we're not certain this tile is really visible (see GlobeSurfaceTileProvider#loadTile). - // * we want ancestor BVH data from this tile but don't plan to render it (see code above). // * we want to upsample from this tile but don't plan to render it (see processTerrainStateMachine). if (terrainOnly) { return; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index acdc178a3f1..48ee8a3f091 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -963,15 +963,7 @@ define([ return tile; } - var bvh = surfaceTile.getBoundingVolumeHierarchy(tile); - if (bvh !== undefined && bvh[0] === bvh[0] && bvh[1] === bvh[1]) { - // Have a BVH that covers this tile and the heights are not NaN. - tileBoundingRegion.minimumHeight = bvh[0] * frameState.terrainExaggeration; - tileBoundingRegion.maximumHeight = bvh[1] * frameState.terrainExaggeration; - return tile; - } - - // No accurate BVH data available, so we're stuck with min/max heights from an ancestor tile. + // No accurate min/max heights available, so we're stuck with min/max heights from an ancestor tile. tileBoundingRegion.minimumHeight = Number.NaN; tileBoundingRegion.maximumHeight = Number.NaN; @@ -992,13 +984,6 @@ define([ tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight * frameState.terrainExaggeration; return ancestor; } - - var ancestorBvh = ancestorSurfaceTile._bvh; - if (ancestorBvh !== undefined && ancestorBvh[0] === ancestorBvh[0] && ancestorBvh[1] === ancestorBvh[1]) { - tileBoundingRegion.minimumHeight = ancestorBvh[0] * frameState.terrainExaggeration; - tileBoundingRegion.maximumHeight = ancestorBvh[1] * frameState.terrainExaggeration; - return ancestor; - } } ancestor = ancestor.parent; } diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js index 775cde8afa7..beabf712554 100644 --- a/Specs/MockTerrainProvider.js +++ b/Specs/MockTerrainProvider.js @@ -29,10 +29,8 @@ define([ this._requestTileGeometryWillSucceed = {}; this._requestTileGeometryWillSucceedWith = {}; this._willHaveWaterMask = {}; - this._willHaveBvh = {}; this._createMeshWillSucceed = {}; this._upsampleWillSucceed = {}; - this._nearestBvhLevel = {}; } MockTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { @@ -59,14 +57,6 @@ define([ return this._tileDataAvailable[createTileKey(xOrTile, y, level)]; }; - MockTerrainProvider.prototype.getNearestBvhLevel = function(x, y, level) { - var bvhLevel = this._nearestBvhLevel[createTileKey(x, y, level)]; - if (!defined(bvhLevel)) { - bvhLevel = -1; - } - return bvhLevel; - }; - MockTerrainProvider.prototype.getLevelMaximumGeometricError = function(level) { return this.levelZeroMaximumGeometricError / (1 << level); }; @@ -105,11 +95,6 @@ define([ return this; }; - MockTerrainProvider.prototype.willHaveBvh = function(bvh, xOrTile, y, level) { - this._willHaveBvh[createTileKey(xOrTile, y, level)] = bvh; - return this; - }; - MockTerrainProvider.prototype.createMeshWillSucceed = function(xOrTile, y, level) { this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = true; return this; @@ -160,11 +145,6 @@ define([ return this; }; - MockTerrainProvider.prototype.willHaveNearestBvhLevel = function(nearestBvhLevel, xOrTile, y, level) { - this._nearestBvhLevel[createTileKey(xOrTile, y, level)] = nearestBvhLevel; - return this; - }; - function createTerrainData(terrainProvider, x, y, level, upsampled) { var terrainData = terrainProvider._requestTileGeometryWillSucceedWith[createTileKey(x, y, level)]; @@ -193,11 +173,6 @@ define([ } } - var willHaveBvh = terrainProvider._willHaveBvh[createTileKey(x, y, level)]; - if (defined(willHaveBvh)) { - options.bvh = willHaveBvh; - } - terrainData = new HeightmapTerrainData(options); } diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index dcfb49feabd..b4bb3e7877c 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -167,100 +167,6 @@ defineSuite([ }); }); - it('loads BVH nodes instead when the tile\'s bounding volume is unreliable', function() { - mockTerrain - .requestTileGeometryWillSucceed(rootTile) - .willHaveNearestBvhLevel(0, rootTile.southwestChild); - - return processor.process([rootTile.southwestChild]).then(function() { - // Indicate that the SW tile's bounding volume comes from the root. - rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; - - // Monitor calls to requestTileGeometry - we should only see one for the root tile now. - spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - - return processor.process([rootTile.southwestChild]); - }).then(function() { - expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); - }); - }); - - it('loads this tile if the nearest BVH level is unknown', function() { - mockTerrain - .requestTileGeometryWillSucceed(rootTile) - .willHaveNearestBvhLevel(-1, rootTile.southwestChild); - - return processor.process([rootTile.southwestChild]).then(function() { - // Indicate that the SW tile's bounding volume comes from the root. - rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; - - // Monitor calls to requestTileGeometry - we should only see one for the southwest tile now. - spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - - return processor.process([rootTile.southwestChild]); - }).then(function() { - expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); - }); - }); - - it('loads this tile if the terrain provider does not implement getNearestBvhLevel', function() { - mockTerrain.getNearestBvhLevel = undefined; - - mockTerrain - .requestTileGeometryWillSucceed(rootTile); - - return processor.process([rootTile.southwestChild]).then(function() { - // Indicate that the SW tile's bounding volume comes from the root. - rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; - - // Monitor calls to requestTileGeometry - we should only see one for the southwest tile now. - spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - - return processor.process([rootTile.southwestChild]); - }).then(function() { - expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(1); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(1); - }); - }); - - it('loads only terrain (not imagery) when loading BVH nodes', function() { - var mockImagery = new MockImageryProvider(); - imageryLayerCollection.addImageryProvider(mockImagery); - - mockImagery - .requestImageWillSucceed(rootTile); - - mockTerrain - .requestTileGeometryWillSucceed(rootTile) - .willHaveNearestBvhLevel(0, rootTile.southwestChild); - - return processor.process([rootTile.southwestChild]).then(function() { - // Indicate that the SW tile's bounding volume comes from the root. - rootTile.southwestChild.data.boundingVolumeSourceTile = rootTile; - - // Monitor calls to requestTileGeometry and requestImage. - // We should see a terrain request but not an imagery request. - spyOn(mockImagery, 'requestImage').and.callThrough(); - spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); - - return processor.process([rootTile.southwestChild]); - }).then(function() { - expect(mockImagery.requestImage.calls.count()).toBe(0); - expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(0); - expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(0); - }); - }); - it('marks an upsampled tile as such', function() { mockTerrain .willBeAvailable(rootTile) @@ -389,65 +295,6 @@ defineSuite([ }); }); - describe('getBoundingVolumeHierarchy', function() { - beforeEach(function() { - processor.mockWebGL(); - }); - - it('gets the BVH from the TerrainData if available', function() { - mockTerrain - .requestTileGeometryWillSucceed(rootTile) - .willHaveBvh(new Float32Array([1.0, 2.0]), rootTile); - - return processor.process([rootTile]).then(function() { - expect(rootTile.data.getBoundingVolumeHierarchy(rootTile)).toEqual([1.0, 2.0]); - }); - }); - - it('gets the BVH from the parent tile if available', function() { - var sw = rootTile.southwestChild; - var nw = rootTile.northwestChild; - var se = rootTile.southeastChild; - var ne = rootTile.northeastChild; - - mockTerrain - .requestTileGeometryWillSucceed(rootTile) - .willHaveBvh(new Float32Array([1.0, 10.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]), rootTile) - .requestTileGeometryWillSucceed(sw) - .requestTileGeometryWillFail(nw) - .requestTileGeometryWillSucceed(ne) - .requestTileGeometryWillFail(se); - - return processor.process([rootTile, sw, nw, se, ne]).then(function() { - expect(sw.data.getBoundingVolumeHierarchy(sw)).toEqual([3.0, 4.0]); - expect(se.data.getBoundingVolumeHierarchy(se)).toEqual([5.0, 6.0]); - expect(nw.data.getBoundingVolumeHierarchy(nw)).toEqual([7.0, 8.0]); - expect(ne.data.getBoundingVolumeHierarchy(ne)).toEqual([9.0, 10.0]); - }); - }); - - it('returns undefined if the parent BVH does not extend to this tile', function() { - mockTerrain - .requestTileGeometryWillSucceed(rootTile) - .willHaveBvh(new Float32Array([1.0, 10.0]), rootTile) - .requestTileGeometryWillSucceed(rootTile.southwestChild); - - return processor.process([rootTile, rootTile.southwestChild]).then(function() { - expect(rootTile.southwestChild.data.getBoundingVolumeHierarchy(rootTile.southwestChild)).toBeUndefined(); - }); - }); - - it('returns undefined if the parent does not have a BVH', function() { - mockTerrain - .requestTileGeometryWillSucceed(rootTile) - .requestTileGeometryWillSucceed(rootTile.southwestChild); - - return processor.process([rootTile, rootTile.southwestChild]).then(function() { - expect(rootTile.southwestChild.data.getBoundingVolumeHierarchy(rootTile.southwestChild)).toBeUndefined(); - }); - }); - }); - describe('pick', function() { var scene; From 9534f192a026900ff0068027771bcd36887468ad Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 23 Jan 2019 18:10:34 +1100 Subject: [PATCH 105/131] Fix some problems in Sandcastle examples. --- Source/Scene/Globe.js | 25 ++++---- Source/Scene/GlobeSurfaceShaderSet.js | 2 +- Source/Scene/GlobeSurfaceTile.js | 35 ++++++++--- Source/Scene/GlobeSurfaceTileProvider.js | 25 +++++--- Source/Scene/ShadowMap.js | 2 +- Source/Scene/TerrainFillMesh.js | 74 +++++++++++++----------- 6 files changed, 99 insertions(+), 64 deletions(-) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index d21368be92a..c152094fb26 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -26,7 +26,8 @@ define([ './ImageryLayerCollection', './QuadtreePrimitive', './SceneMode', - './ShadowMode' + './ShadowMode', + './TileSelectionResult' ], function( BoundingSphere, buildModuleUrl, @@ -55,7 +56,8 @@ define([ ImageryLayerCollection, QuadtreePrimitive, SceneMode, - ShadowMode) { + ShadowMode, + TileSelectionResult) { 'use strict'; /** @@ -494,10 +496,8 @@ define([ // TODO: ok to allocate / recreate the bounding sphere every time here? boundingVolume = BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, surfaceTile.tileBoundingRegion.minimumHeight, surfaceTile.tileBoundingRegion.maximumHeight, boundingVolume); Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); - } else if (surfaceTile.renderableTile !== undefined) { - BoundingSphere.fromRectangle3D(tile.rectangle, tile.tilingScheme.ellipsoid, surfaceTile.tileBoundingRegion.maximumHeight, boundingVolume); - } else if (surfaceTile.mesh !== undefined) { - BoundingSphere.clone(surfaceTile.mesh.boundingSphere3D, boundingVolume); + } else if (defined(surfaceTile.renderedMesh)) { + BoundingSphere.clone(surfaceTile.renderedMesh.boundingSphere3D, boundingVolume); } else { // So wait how did we render this thing then? It shouldn't be possible to get here. continue; @@ -586,22 +586,19 @@ define([ } } - if (!defined(tile) || !Rectangle.contains(tile.rectangle, cartographic)) { + if (i >= length) { return undefined; } - while (tile.renderable) { + while (tile._lastSelectionResult === TileSelectionResult.REFINED) { tile = tileIfContainsCartographic(tile.southwestChild, cartographic) || tileIfContainsCartographic(tile.southeastChild, cartographic) || tileIfContainsCartographic(tile.northwestChild, cartographic) || tile.northeastChild; } - while (defined(tile) && (!defined(tile.data) || !defined(tile.data.mesh))) { - tile = tile.parent; - } - - if (!defined(tile) || !defined(tile.data) || !defined(tile.data.tileBoundingRegion)) { + if (tile._lastSelectionResult !== TileSelectionResult.RENDERED) { + // Tile was not rendered (culled). return undefined; } @@ -621,7 +618,7 @@ define([ if (!defined(rayOrigin)) { // intersection point is outside the ellipsoid, try other value // minimum height (-11500.0) for the terrain set, need to get this information from the terrain provider - var magnitude = Math.min(defaultValue(tile.data.minimumHeight, 0.0),-11500.0); + var magnitude = Math.min(defaultValue(tile.data.minimumHeight, 0.0), -11500.0); // multiply by the *positive* value of the magnitude var vectorToMinimumPoint = Cartesian3.multiplyByScalar(surfaceNormal, Math.abs(magnitude) + 1, scratchGetHeightIntersection); diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 99b9c6f365f..fce5badd422 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -95,7 +95,7 @@ define([ var quantization = 0; var quantizationDefine = ''; - var mesh = surfaceTile.vertexArray !== undefined ? surfaceTile.mesh : surfaceTile.fill.mesh; + var mesh = surfaceTile.renderedMesh; var terrainEncoding = mesh.encoding; var quantizationMode = terrainEncoding.quantization; if (quantizationMode === TerrainQuantization.BITS12) { diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 376648ea6d7..9d66948f6c0 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -136,6 +136,27 @@ define([ return shouldRemoveTile; } + }, + + /** + * Gets the {@link TerrainMesh} that is used for rendering this tile, if any. + * Returns the value of the {@link GlobeSurfaceTile#mesh} property if + * {@link GlobeSurfaceTile#vertexArray} is defined. Otherwise, It returns the + * {@link TerrainFillMesh#mesh} property of the {@link GlobeSurfaceTile#fill}. + * If there is no fill, it returns undefined. + * + * @memberof GlobeSurfaceTile.prototype + * @type {TerrainMesh} + */ + renderedMesh : { + get : function() { + if (defined(this.vertexArray)) { + return this.mesh; + } else if (defined(this.fill)) { + return this.fill.mesh; + } + return undefined; + } } }); @@ -158,7 +179,7 @@ define([ var scratchResult = new Cartesian3(); GlobeSurfaceTile.prototype.pick = function(ray, mode, projection, cullBackFaces, result) { - var mesh = this.mesh || (this.fill && this.fill.mesh); + var mesh = this.renderedMesh; if (!defined(mesh)) { return undefined; } @@ -229,13 +250,13 @@ define([ } }; - GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, terrainOnly) { + GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, terrainOnly) { GlobeSurfaceTile.initialize(tile, terrainProvider, imageryLayerCollection); var surfaceTile = tile.data; if (tile.state === QuadtreeTileLoadState.LOADING) { - processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection); + processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy); } // From here down we're loading imagery, not terrain. We don't want to load imagery until @@ -348,7 +369,7 @@ define([ } } - function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection) { + function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { var surfaceTile = tile.data; // If this tile is FAILED, we'll need to upsample from the parent. If the parent isn't @@ -374,7 +395,7 @@ define([ } if (surfaceTile.terrainState === TerrainState.TRANSFORMED) { - createResources(surfaceTile, frameState.context, terrainProvider, tile.x, tile.y, tile.level); + createResources(surfaceTile, frameState.context, terrainProvider, tile.x, tile.y, tile.level, vertexArraysToDestroy); } if (surfaceTile.terrainState >= TerrainState.RECEIVED && surfaceTile.waterMaskTexture === undefined && terrainProvider.hasWaterMask) { @@ -548,10 +569,10 @@ define([ }; - function createResources(surfaceTile, context, terrainProvider, x, y, level) { + function createResources(surfaceTile, context, terrainProvider, x, y, level, vertexArraysToDestroy) { surfaceTile.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, surfaceTile.mesh); surfaceTile.terrainState = TerrainState.READY; - surfaceTile.fill = surfaceTile.fill && surfaceTile.fill.destroy(); + surfaceTile.fill = surfaceTile.fill && surfaceTile.fill.destroy(vertexArraysToDestroy); } function getContextWaterMaskData(context) { diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 48ee8a3f091..17eca815090 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -182,6 +182,8 @@ define([ this._uniformMaps = []; this._usedDrawCommands = 0; + this._vertexArraysToDestroy = []; + this._debug = { wireframe : false, boundingSphereTile : undefined @@ -398,6 +400,13 @@ define([ // Add credits for terrain and imagery providers. updateCredits(this, frameState); + + var vertexArraysToDestroy = this._vertexArraysToDestroy; + var length = vertexArraysToDestroy.length; + for (var j = 0; j < length; ++j) { + GlobeSurfaceTile._freeVertexArray(vertexArraysToDestroy[j]); + } + vertexArraysToDestroy.length = 0; }; /** @@ -458,7 +467,7 @@ define([ // If this frame has a mix of loaded and fill tiles, we need to propagate // loaded heights to the fill tiles. if (this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) { - TerrainFillMesh.updateFillTiles(this, this._quadtree._tilesToRender, frameState); + TerrainFillMesh.updateFillTiles(this, this._quadtree._tilesToRender, frameState, this._vertexArraysToDestroy); } // Add the tile render commands to the command list, sorted by texture count. @@ -531,7 +540,7 @@ define([ terrainStateBefore = surfaceTile.terrainState; } - GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, terrainOnly); + GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly); surfaceTile = tile.data; if (terrainOnly && terrainStateBefore !== tile.data.terrainState) { @@ -541,7 +550,7 @@ define([ // Then we'll load imagery, too. if (this.computeTileVisibility(tile, frameState, this.quadtree.occluders) && surfaceTile.boundingVolumeSourceTile === tile) { terrainOnly = false; - GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, terrainOnly); + GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly); } } }; @@ -615,8 +624,8 @@ define([ BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume); Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); - if (frameState.mode === SceneMode.MORPHING && surfaceTile.mesh !== undefined) { - boundingVolume = BoundingSphere.union(surfaceTile.mesh.boundingSphere3D, boundingVolume, boundingVolume); + if (frameState.mode === SceneMode.MORPHING && defined(surfaceTile.renderedMesh)) { + boundingVolume = BoundingSphere.union(surfaceTile.renderedMesh.boundingSphere3D, boundingVolume, boundingVolume); } } @@ -1370,10 +1379,10 @@ define([ var mesh; var vertexArray; - if (surfaceTile.vertexArray !== undefined) { + if (defined(surfaceTile.vertexArray)) { mesh = surfaceTile.mesh; vertexArray = surfaceTile.vertexArray; - } else if (surfaceTile.fill !== undefined && surfaceTile.fill.vertexArray !== undefined) { + } else if (defined(surfaceTile.fill) && defined(surfaceTile.fill.vertexArray)) { mesh = surfaceTile.fill.mesh; vertexArray = surfaceTile.fill.vertexArray; } @@ -1593,7 +1602,7 @@ define([ --maxTextures; } - var mesh = surfaceTile.vertexArray ? surfaceTile.mesh : surfaceTile.fill.mesh; + var mesh = surfaceTile.renderedMesh; var rtc = mesh.center; var encoding = mesh.encoding; diff --git a/Source/Scene/ShadowMap.js b/Source/Scene/ShadowMap.js index 8b8d2fe022d..851a9260d3d 100644 --- a/Source/Scene/ShadowMap.js +++ b/Source/Scene/ShadowMap.js @@ -1541,7 +1541,7 @@ define([ var hasTerrainNormal = false; if (isTerrain) { - hasTerrainNormal = command.owner.data.mesh.encoding.hasVertexNormals; + hasTerrainNormal = command.owner.data.renderedMesh.encoding.hasVertexNormals; } if (command.receiveShadows && lightShadowMapsEnabled) { diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 0bee301ef45..4c4e5a25b09 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -68,22 +68,26 @@ define([ this.vertexArray = undefined; } - TerrainFillMesh.prototype.update = function(tileProvider, frameState) { + TerrainFillMesh.prototype.update = function(tileProvider, frameState, vertexArraysToDestroy) { if (this.changedThisFrame) { - createFillMesh(tileProvider, frameState, this.tile); + createFillMesh(tileProvider, frameState, this.tile, vertexArraysToDestroy); this.changedThisFrame = false; } }; - TerrainFillMesh.prototype.destroy = function() { - GlobeSurfaceTile._freeVertexArray(this.vertexArray); + TerrainFillMesh.prototype.destroy = function(vertexArraysToDestroy) { + if (defined(vertexArraysToDestroy)) { + vertexArraysToDestroy.push(this.vertexArray); + } else { + GlobeSurfaceTile._freeVertexArray(this.vertexArray, vertexArraysToDestroy); + } this.vertexArray = undefined; return undefined; }; var traversalQueueScratch = new Queue(); - TerrainFillMesh.updateFillTiles = function(tileProvider, renderedTiles, frameState) { + TerrainFillMesh.updateFillTiles = function(tileProvider, renderedTiles, frameState, vertexArraysToDestroy) { // We want our fill tiles to look natural, which means they should align perfectly with // adjacent loaded tiles, and their edges that are not adjacent to loaded tiles should have // sensible heights (e.g. the average of the heights of loaded edges). Some fill tiles may @@ -118,25 +122,25 @@ define([ var tileToSouth = tile.findTileToSouth(levelZeroTiles); var tileToEast = tile.findTileToEast(levelZeroTiles); var tileToNorth = tile.findTileToNorth(levelZeroTiles); - visitRenderedTiles(tileProvider, frameState, tile, tileToWest, lastSelectionFrameNumber, TileEdge.EAST, false, traversalQueue); - visitRenderedTiles(tileProvider, frameState, tile, tileToSouth, lastSelectionFrameNumber, TileEdge.NORTH, false, traversalQueue); - visitRenderedTiles(tileProvider, frameState, tile, tileToEast, lastSelectionFrameNumber, TileEdge.WEST, false, traversalQueue); - visitRenderedTiles(tileProvider, frameState, tile, tileToNorth, lastSelectionFrameNumber, TileEdge.SOUTH, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToWest, lastSelectionFrameNumber, TileEdge.EAST, false, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, tile, tileToSouth, lastSelectionFrameNumber, TileEdge.NORTH, false, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, tile, tileToEast, lastSelectionFrameNumber, TileEdge.WEST, false, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, tile, tileToNorth, lastSelectionFrameNumber, TileEdge.SOUTH, false, traversalQueue, vertexArraysToDestroy); var tileToNorthwest = tileToWest.findTileToNorth(levelZeroTiles); var tileToSouthwest = tileToWest.findTileToSouth(levelZeroTiles); var tileToNortheast = tileToEast.findTileToNorth(levelZeroTiles); var tileToSoutheast = tileToEast.findTileToSouth(levelZeroTiles); - visitRenderedTiles(tileProvider, frameState, tile, tileToNorthwest, lastSelectionFrameNumber, TileEdge.SOUTHEAST, false, traversalQueue); - visitRenderedTiles(tileProvider, frameState, tile, tileToNortheast, lastSelectionFrameNumber, TileEdge.SOUTHWEST, false, traversalQueue); - visitRenderedTiles(tileProvider, frameState, tile, tileToSouthwest, lastSelectionFrameNumber, TileEdge.NORTHEAST, false, traversalQueue); - visitRenderedTiles(tileProvider, frameState, tile, tileToSoutheast, lastSelectionFrameNumber, TileEdge.NORTHWEST, false, traversalQueue); + visitRenderedTiles(tileProvider, frameState, tile, tileToNorthwest, lastSelectionFrameNumber, TileEdge.SOUTHEAST, false, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, tile, tileToNortheast, lastSelectionFrameNumber, TileEdge.SOUTHWEST, false, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, tile, tileToSouthwest, lastSelectionFrameNumber, TileEdge.NORTHEAST, false, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, tile, tileToSoutheast, lastSelectionFrameNumber, TileEdge.NORTHWEST, false, traversalQueue, vertexArraysToDestroy); tile = traversalQueue.dequeue(); } }; - function visitRenderedTiles(tileProvider, frameState, sourceTile, startTile, currentFrameNumber, tileEdge, downOnly, traversalQueue) { + function visitRenderedTiles(tileProvider, frameState, sourceTile, startTile, currentFrameNumber, tileEdge, downOnly, traversalQueue, vertexArraysToDestroy) { if (startTile === undefined) { // There are no tiles North or South of the poles. return; @@ -182,7 +186,7 @@ define([ // No further processing necessary for renderable tiles. return; } - visitTile(tileProvider, frameState, sourceTile, tile, tileEdge, currentFrameNumber, traversalQueue); + visitTile(tileProvider, frameState, sourceTile, tile, tileEdge, currentFrameNumber, traversalQueue, vertexArraysToDestroy); return; } @@ -194,39 +198,39 @@ define([ // Visit the tiles in counter-clockwise order. switch (tileEdge) { case TileEdge.WEST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; case TileEdge.EAST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; case TileEdge.SOUTH: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; case TileEdge.NORTH: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; case TileEdge.NORTHWEST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; case TileEdge.NORTHEAST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; case TileEdge.SOUTHWEST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; case TileEdge.SOUTHEAST: - visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue); + visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy); break; default: throw new DeveloperError('Invalid edge'); } } - function visitTile(tileProvider, frameState, sourceTile, destinationTile, tileEdge, frameNumber, traversalQueue) { + function visitTile(tileProvider, frameState, sourceTile, destinationTile, tileEdge, frameNumber, traversalQueue, vertexArraysToDestroy) { var destinationSurfaceTile = destinationTile.data; if (destinationSurfaceTile.fill === undefined) { @@ -243,10 +247,10 @@ define([ traversalQueue.enqueue(destinationTile); } - propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge); + propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge, vertexArraysToDestroy); } - function propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge) { + function propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge, vertexArraysToDestroy) { var destinationFill = destinationTile.data.fill; var sourceMesh; @@ -256,7 +260,7 @@ define([ // Source is a fill, create/update it if necessary. if (sourceFill.changedThisFrame) { - createFillMesh(tileProvider, frameState, sourceTile); + createFillMesh(tileProvider, frameState, sourceTile, vertexArraysToDestroy); sourceFill.changedThisFrame = false; } sourceMesh = sourceTile.data.fill.mesh; @@ -461,7 +465,7 @@ define([ var neVertexScratch = new HeightAndNormal(); var heightmapBuffer = typeof Uint8Array !== 'undefined' ? new Uint8Array(9 * 9) : undefined; - function createFillMesh(tileProvider, frameState, tile) { + function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { GlobeSurfaceTile.initialize(tile, tileProvider.terrainProvider, tileProvider._imageryLayers); var surfaceTile = tile.data; @@ -664,7 +668,11 @@ define([ var context = frameState.context; - GlobeSurfaceTile._freeVertexArray(fill.vertexArray); + if (defined(vertexArraysToDestroy)) { + vertexArraysToDestroy.push(fill.vertexArray); + } else { + GlobeSurfaceTile._freeVertexArray(fill.vertexArray); + } fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, fill.mesh); surfaceTile.processImagery(tile, tileProvider.terrainProvider, frameState, true); } From ebb8760d47917f7bb1af2cc4af2becd240c319d4 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 23 Jan 2019 21:41:25 +1100 Subject: [PATCH 106/131] Don't add undefined vertex arrays to free list. --- Source/Scene/TerrainFillMesh.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 4c4e5a25b09..eaf60377cfe 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -76,12 +76,14 @@ define([ }; TerrainFillMesh.prototype.destroy = function(vertexArraysToDestroy) { - if (defined(vertexArraysToDestroy)) { - vertexArraysToDestroy.push(this.vertexArray); - } else { - GlobeSurfaceTile._freeVertexArray(this.vertexArray, vertexArraysToDestroy); + if (defined(this.vertexArray)) { + if (defined(vertexArraysToDestroy)) { + vertexArraysToDestroy.push(this.vertexArray); + } else { + GlobeSurfaceTile._freeVertexArray(this.vertexArray, vertexArraysToDestroy); + } + this.vertexArray = undefined; } - this.vertexArray = undefined; return undefined; }; @@ -668,11 +670,14 @@ define([ var context = frameState.context; - if (defined(vertexArraysToDestroy)) { - vertexArraysToDestroy.push(fill.vertexArray); - } else { - GlobeSurfaceTile._freeVertexArray(fill.vertexArray); + if (defined(fill.vertexArray)) { + if (defined(vertexArraysToDestroy)) { + vertexArraysToDestroy.push(fill.vertexArray); + } else { + GlobeSurfaceTile._freeVertexArray(fill.vertexArray); + } } + fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, fill.mesh); surfaceTile.processImagery(tile, tileProvider.terrainProvider, frameState, true); } From 4adf7d327f2d7ba0f05461dd2cf3a62dfc8bbaa8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 24 Jan 2019 23:39:39 +1100 Subject: [PATCH 107/131] Fix Clamp to Terrain Sandcastle example. Maybe a little dodgily... --- Source/Scene/Globe.js | 7 +++- Source/Scene/GlobeSurfaceTileProvider.js | 14 ++++++++ Source/Scene/QuadtreePrimitive.js | 46 +++++++++++++++++++----- Source/Scene/TerrainFillMesh.js | 4 +-- Source/Scene/TileSelectionResult.js | 14 ++++++-- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index c152094fb26..8e6bac018da 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -597,7 +597,12 @@ define([ tile.northeastChild; } - if (tile._lastSelectionResult !== TileSelectionResult.RENDERED) { + // This tile was either rendered or culled. + // It is sometimes useful to get a height from culled tile, + // e.g. when we're getting a height in order to place a billboard + // on terrain, and the camera is centered on that same billboard. + // The culled tile must have a valid mesh, though. + if (!defined(tile.data) || !defined(tile.data.renderedMesh)) { // Tile was not rendered (culled). return undefined; } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 17eca815090..6e861122c53 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -535,9 +535,13 @@ define([ var surfaceTile = tile.data; var terrainOnly = true; var terrainStateBefore; + var hadFillBefore = false; + var hadVertexArrayBefore = false; if (defined(surfaceTile)) { terrainOnly = surfaceTile.boundingVolumeSourceTile !== tile; terrainStateBefore = surfaceTile.terrainState; + hadFillBefore = defined(surfaceTile.fill); + hadVertexArrayBefore = defined(surfaceTile.vertexArray); } GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly); @@ -553,6 +557,16 @@ define([ GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly); } } + + if (hadFillBefore && !define(surfaceTile.fill)) { + // Transitioned from a fill to real geometry, so we'll need to update the heights + // of things in this tile. + this._quadtree._tileToUpdateHeights.push(tile); + } else if (tile._lastSelectionResult === TileSelectionResult.CULLED_BUT_NEEDED && !hadVertexArrayBefore && defined(surfaceTile.vertexArray)) { + // This tile is not being rendered but IS used for getting heights, and it just acquired some geometry. + // So we need to update the heights based on the new geometry. + this._quadtree._tileToUpdateHeights.push(tile); + } }; var boundingSphereScratch = new BoundingSphere(); diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index f754b1e800f..cb6ca498ea3 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -8,6 +8,7 @@ define([ '../Core/Event', '../Core/getTimestamp', '../Core/Math', + '../Core/Matrix4', '../Core/OrthographicFrustum', '../Core/OrthographicOffCenterFrustum', '../Core/Ray', @@ -29,6 +30,7 @@ define([ Event, getTimestamp, CesiumMath, + Matrix4, OrthographicFrustum, OrthographicOffCenterFrustum, Ray, @@ -668,12 +670,12 @@ define([ // or any descendants last frame. Such imagery is required because rendering this tile without // it would cause detail to disappear. // - // Determining condition 4 is more expensive, so we check the others first.. + // Determining condition 4 is more expensive, so we check the others first. // // Note that even if we decide to render a tile here, it may later get "kicked" in favor of an ancestor. var oneRenderedLastFrame = TileSelectionResult.originalResult(lastFrameSelectionResult) === TileSelectionResult.RENDERED; - var twoCulledOrNotVisited = lastFrameSelectionResult === TileSelectionResult.CULLED || lastFrameSelectionResult === TileSelectionResult.NONE; + var twoCulledOrNotVisited = TileSelectionResult.originalResult(lastFrameSelectionResult) === TileSelectionResult.CULLED || lastFrameSelectionResult === TileSelectionResult.NONE; var threeCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE; var renderable = oneRenderedLastFrame || twoCulledOrNotVisited || threeCompletelyLoaded; @@ -890,13 +892,14 @@ define([ quadDetails.combine(traversalDetails); } + var cameraOriginScratch = new Cartesian3(); + var cameraOriginCartographicScratch = new Cartographic(); + function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { return visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails); } - tile._lastSelectionResultFrame = frameState.frameNumber; - tile._lastSelectionResult = TileSelectionResult.CULLED; ++primitive._debug.tilesCulled; primitive._tileReplacementQueue.markTileRendered(tile); @@ -904,11 +907,35 @@ define([ traversalDetails.anyWereRenderedLastFrame = false; traversalDetails.notYetRenderableCount = 0; - // Load culled level zero tiles with low priority. - // For all other levels, only load culled tiles if preloadSiblings is enabled. - if (primitive.preloadSiblings || tile.level === 0) { + var camera = frameState.camera; + var cameraOrigin = Matrix4.getTranslation(camera.transform, cameraOriginScratch); + var cameraOriginCartographic; + if (cameraOrigin.x > 1000.0 || cameraOrigin.y > 1000.0 || cameraOrigin.z > 1000.0) { + cameraOriginCartographic = primitive.tileProvider.tilingScheme.ellipsoid.cartesianToCartographic(cameraOrigin, cameraOriginCartographicScratch); + } + + if (Rectangle.contains(tile.rectangle, camera.positionCartographic) || (defined(cameraOriginCartographic) && Rectangle.contains(tile.rectangle, cameraOriginCartographic))) { + // Load the tile(s) that contains the camera's position and + // the origin of its reference frame with medium priority. + queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + + var lastFrame = primitive._lastSelectionFrameNumber; + var lastFrameSelectionResult = tile._lastSelectionResultFrame === lastFrame ? tile._lastSelectionResult : TileSelectionResult.NONE; + if (lastFrameSelectionResult !== TileSelectionResult.CULLED_BUT_NEEDED && lastFrameSelectionResult !== TileSelectionResult.RENDERED) { + primitive._tileToUpdateHeights.push(tile); + } + + tile._lastSelectionResult = TileSelectionResult.CULLED_BUT_NEEDED; + } else if (primitive.preloadSiblings || tile.level === 0) { + // Load culled level zero tiles with low priority. + // For all other levels, only load culled tiles if preloadSiblings is enabled. queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); + tile._lastSelectionResult = TileSelectionResult.CULLED; + } else { + tile._lastSelectionResult = TileSelectionResult.CULLED; } + + tile._lastSelectionResultFrame = frameState.frameNumber; } function screenSpaceError(primitive, frameState, tile) { @@ -1027,7 +1054,10 @@ define([ if (tile.state !== QuadtreeTileLoadState.DONE) { // Tile isn't loaded yet, so try again next frame if this tile is still // being rendered. - if (tile._lastSelectionResultFrame === primitive._lastSelectionFrameNumber && tile._lastSelectionResult === TileSelectionResult.RENDERED) { + var selectionResult = tile._lastSelectionResultFrame === primitive._lastSelectionFrameNumber + ? tile._lastSelectionResult + : TileSelectionResult.NONE; + if (selectionResult === TileSelectionResult.RENDERED || selectionResult === TileSelectionResult.CULLED_BUT_NEEDED) { tryNextFrame.push(tile); } tilesToUpdateHeights.shift(); diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index eaf60377cfe..5c1b84262ed 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -149,7 +149,7 @@ define([ } var tile = startTile; - while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || TileSelectionResult.wasKicked(tile._lastSelectionResult) || tile._lastSelectionResult === TileSelectionResult.CULLED)) { + while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || TileSelectionResult.wasKicked(tile._lastSelectionResult) || TileSelectionResult.originalResult(tile._lastSelectionResult) === TileSelectionResult.CULLED)) { // This tile wasn't visited or it was visited and then kicked, so walk up to find the closest ancestor that was rendered. // We also walk up if the tile was culled, because if siblings were kicked an ancestor may have been rendered. if (downOnly) { @@ -192,7 +192,7 @@ define([ return; } - if (startTile._lastSelectionResult === TileSelectionResult.CULLED) { + if (TileSelectionResult.originalResult(startTile._lastSelectionResult) === TileSelectionResult.CULLED) { return; } diff --git a/Source/Scene/TileSelectionResult.js b/Source/Scene/TileSelectionResult.js index 1773db78167..46637e37584 100644 --- a/Source/Scene/TileSelectionResult.js +++ b/Source/Scene/TileSelectionResult.js @@ -40,6 +40,16 @@ define([ */ REFINED_AND_KICKED: 3 | 4, + /** + * This tile was culled because it was not visible, but it still needs to be loaded + * and any heights on it need to be updated because the camera's position or the + * camera's reference frame's origin falls inside this tile. Loading this tile + * could affect the position of the camera if the camera is currently below + * terrain or if it is tracking an object whose height is referenced to terrain. + * And a change in the camera position may, in turn, affect what is culled. + */ + CULLED_BUT_NEEDED: 1 | 8, + /** * Determines if a selection result indicates that this tile or its descendants were * kicked from the render list. In other words, if it is RENDERED_AND_KICKED @@ -53,8 +63,8 @@ define([ }, /** - * Determines the original selection result prior to being kicked. - * If the tile wasn't kicked, the original value is returned. + * Determines the original selection result prior to being kicked or CULLED_BUT_NEEDED. + * If the tile wasn't kicked or CULLED_BUT_NEEDED, the original value is returned. * @param {TileSelectionResult} value The selection result. * @returns {TileSelectionResult} The original selection result prior to kicking. */ From a676e662d501af5f48843cf83595e88dfb34c9be Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 25 Jan 2019 10:51:46 +1100 Subject: [PATCH 108/131] Fix test failure. --- Specs/Scene/QuadtreePrimitiveSpec.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index 33712132ee1..c2816087d14 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -480,9 +480,8 @@ defineSuite([ return Intersect.INTERSECTING; }); - // Look down at the center of the edge between the visible and non-visible tiles. - var middle = new Cartographic(visibleTile.rectangle.west, (visibleTile.rectangle.south + visibleTile.rectangle.north) * 0.5, 0.0); - setCameraPosition(quadtree, frameState, middle, visibleTile.level); + // Look down at the center of the visible tile. + setCameraPosition(quadtree, frameState, Rectangle.center(visibleTile.rectangle), visibleTile.level); spyOn(mockTerrain, 'requestTileGeometry').and.callThrough(); From e250a39b71f42877a9e8c09533204391dba0cd05 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 25 Jan 2019 18:00:03 +1100 Subject: [PATCH 109/131] Possibly dodgy fix for Clamp to Terrain example. --- Source/DataSources/LabelVisualizer.js | 15 ++++++++++++++- Source/DataSources/PointVisualizer.js | 11 +++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Source/DataSources/LabelVisualizer.js b/Source/DataSources/LabelVisualizer.js index 94057552ade..ab63dacd866 100644 --- a/Source/DataSources/LabelVisualizer.js +++ b/Source/DataSources/LabelVisualizer.js @@ -136,10 +136,19 @@ define([ cluster._clusterDirty = true; } + var updateClamping = false; + var heightReference = Property.getValueOrDefault(labelGraphics._heightReference, time, defaultHeightReference); + if (!defined(label)) { label = cluster.getLabel(entity); label.id = entity; item.label = label; + + // If this new label happens to have a position and height reference that match our new values, + // label._updateClamping will not be called automatically. That's a problem because the clamped + // height may be based on different terrain than is now loaded. So we'll manually call + // _updateClamping below. + updateClamping = Cartesian3.equals(label.position, position) && label.heightReference === heightReference; } label.show = true; @@ -156,7 +165,7 @@ define([ label.backgroundPadding = Property.getValueOrDefault(labelGraphics._backgroundPadding, time, defaultBackgroundPadding, backgroundPaddingScratch); label.pixelOffset = Property.getValueOrDefault(labelGraphics._pixelOffset, time, defaultPixelOffset, pixelOffsetScratch); label.eyeOffset = Property.getValueOrDefault(labelGraphics._eyeOffset, time, defaultEyeOffset, eyeOffsetScratch); - label.heightReference = Property.getValueOrDefault(labelGraphics._heightReference, time, defaultHeightReference); + label.heightReference = heightReference; label.horizontalOrigin = Property.getValueOrDefault(labelGraphics._horizontalOrigin, time, defaultHorizontalOrigin); label.verticalOrigin = Property.getValueOrDefault(labelGraphics._verticalOrigin, time, defaultVerticalOrigin); label.translucencyByDistance = Property.getValueOrUndefined(labelGraphics._translucencyByDistance, time, translucencyByDistanceScratch); @@ -164,6 +173,10 @@ define([ label.scaleByDistance = Property.getValueOrUndefined(labelGraphics._scaleByDistance, time, scaleByDistanceScratch); label.distanceDisplayCondition = Property.getValueOrUndefined(labelGraphics._distanceDisplayCondition, time, distanceDisplayConditionScratch); label.disableDepthTestDistance = Property.getValueOrUndefined(labelGraphics._disableDepthTestDistance, time); + + if (updateClamping) { + label._updateClamping(); + } } return true; }; diff --git a/Source/DataSources/PointVisualizer.js b/Source/DataSources/PointVisualizer.js index d674c05e3f3..c2aa7a70245 100644 --- a/Source/DataSources/PointVisualizer.js +++ b/Source/DataSources/PointVisualizer.js @@ -114,6 +114,7 @@ define([ } var needsRedraw = false; + var updateClamping = false; if ((heightReference !== HeightReference.NONE) && !defined(billboard)) { if (defined(pointPrimitive)) { returnPrimitive(item, entity, cluster); @@ -125,6 +126,12 @@ define([ billboard.image = undefined; item.billboard = billboard; needsRedraw = true; + + // If this new billboard happens to have a position and height reference that match our new values, + // billboard._updateClamping will not be called automatically. That's a problem because the clamped + // height may be based on different terrain than is now loaded. So we'll manually call + // _updateClamping below. + updateClamping = Cartesian3.equals(billboard.position, position) && billboard.heightReference === heightReference; } else if ((heightReference === HeightReference.NONE) && !defined(pointPrimitive)) { if (defined(billboard)) { returnPrimitive(item, entity, cluster); @@ -190,6 +197,10 @@ define([ billboard.setImage(textureId, createBillboardPointCallback(centerAlpha, cssColor, cssOutlineColor, newOutlineWidth, newPixelSize)); } + + if (updateClamping) { + billboard._updateClamping(); + } } } return true; From 992d01897251f1029153fcd8f4333551b46dedfa Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 25 Jan 2019 22:02:40 +1100 Subject: [PATCH 110/131] Less per-tile work. --- Source/Scene/QuadtreePrimitive.js | 36 ++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index cb6ca498ea3..b7494d1755d 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -118,6 +118,12 @@ define([ this._lastTileIndex = 0; this._updateHeightsTimeSlice = 2.0; + // If a culled tile contains a cartographic positions in this list, it will be marked + // TileSelection.CULLED_BUT_NEEDED and added to the list of tiles to update heights, + // even though it is not rendered. The first position will be the position of the + // camera and the second will be the origin of the camera's reference frame. + this._neededPositions = [undefined, undefined]; + /** * 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 @@ -495,6 +501,7 @@ define([ return (alon * alon + alat * alat) - (blon * blon + blat * blat); } + var cameraOriginScratch = new Cartesian3(); var rootTraversalDetails = []; function selectTilesForRendering(primitive, frameState) { @@ -557,6 +564,11 @@ define([ customDataRemoved.length = 0; } + var camera = frameState.camera; + primitive._neededPositions[0] = camera.positionCartographic; + var cameraFrameOrigin = Matrix4.getTranslation(camera.transform, cameraOriginScratch); + primitive._neededPositions[1] = primitive.tileProvider.tilingScheme.ellipsoid.cartesianToCartographic(cameraFrameOrigin, primitive._neededPositions[1]); + // Traverse in depth-first, near-to-far order. for (i = 0, len = levelZeroTiles.length; i < len; ++i) { tile = levelZeroTiles[i]; @@ -892,8 +904,19 @@ define([ quadDetails.combine(traversalDetails); } - var cameraOriginScratch = new Cartesian3(); - var cameraOriginCartographicScratch = new Cartographic(); + function containsNeededPosition(primitive, tile) { + var needed = primitive._neededPositions; + var rectangle = tile.rectangle; + + for (var i = 0, len = needed.length; i < len; ++i) { + var position = needed[i]; + if (defined(position) && Rectangle.contains(rectangle, position)) { + return true; + } + } + + return false; + } function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { @@ -907,14 +930,7 @@ define([ traversalDetails.anyWereRenderedLastFrame = false; traversalDetails.notYetRenderableCount = 0; - var camera = frameState.camera; - var cameraOrigin = Matrix4.getTranslation(camera.transform, cameraOriginScratch); - var cameraOriginCartographic; - if (cameraOrigin.x > 1000.0 || cameraOrigin.y > 1000.0 || cameraOrigin.z > 1000.0) { - cameraOriginCartographic = primitive.tileProvider.tilingScheme.ellipsoid.cartesianToCartographic(cameraOrigin, cameraOriginCartographicScratch); - } - - if (Rectangle.contains(tile.rectangle, camera.positionCartographic) || (defined(cameraOriginCartographic) && Rectangle.contains(tile.rectangle, cameraOriginCartographic))) { + if (containsNeededPosition(primitive, tile)) { // Load the tile(s) that contains the camera's position and // the origin of its reference frame with medium priority. queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); From 1e60c7d1ead1632a6b6831319dc732fbcb5be325 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 25 Jan 2019 22:26:27 +1100 Subject: [PATCH 111/131] Remove unused requires. --- Source/Scene/GlobeSurfaceTile.js | 4 ---- Source/Scene/GlobeSurfaceTileProvider.js | 6 ------ Source/Scene/TerrainFillMesh.js | 2 -- 3 files changed, 12 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 9d66948f6c0..79880c33f84 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -4,10 +4,8 @@ define([ '../Core/Cartesian4', '../Core/defined', '../Core/defineProperties', - '../Core/DeveloperError', '../Core/IndexDatatype', '../Core/IntersectionTests', - '../Core/OrientedBoundingBox', '../Core/PixelFormat', '../Core/Request', '../Core/RequestState', @@ -34,10 +32,8 @@ define([ Cartesian4, defined, defineProperties, - DeveloperError, IndexDatatype, IntersectionTests, - OrientedBoundingBox, PixelFormat, Request, RequestState, diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 6e861122c53..c5862fe69ee 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1,5 +1,4 @@ define([ - '../Core/AttributeCompression', '../Core/BoundingSphere', '../Core/BoxOutlineGeometry', '../Core/Cartesian2', @@ -20,14 +19,12 @@ define([ '../Core/IndexDatatype', '../Core/Intersect', '../Core/Math', - '../Core/Matrix3', '../Core/Matrix4', '../Core/OrientedBoundingBox', '../Core/OrthographicFrustum', '../Core/PrimitiveType', '../Core/Rectangle', '../Core/SphereOutlineGeometry', - '../Core/TerrainEncoding', '../Core/TerrainMesh', '../Core/TerrainQuantization', '../Core/Visibility', @@ -55,7 +52,6 @@ define([ './TerrainFillMesh', './TerrainState' ], function( - AttributeCompression, BoundingSphere, BoxOutlineGeometry, Cartesian2, @@ -76,14 +72,12 @@ define([ IndexDatatype, Intersect, CesiumMath, - Matrix3, Matrix4, OrientedBoundingBox, OrthographicFrustum, PrimitiveType, Rectangle, SphereOutlineGeometry, - TerrainEncoding, TerrainMesh, TerrainQuantization, Visibility, diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 5c1b84262ed..f5156729d76 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -17,7 +17,6 @@ define([ '../Core/TerrainMesh', '../Core/WebMercatorProjection', './GlobeSurfaceTile', - './ImageryState', './TileSelectionResult' ], function( AttributeCompression, @@ -38,7 +37,6 @@ define([ TerrainMesh, WebMercatorProjection, GlobeSurfaceTile, - ImageryState, TileSelectionResult) { 'use strict'; From 72d4d39e282c24ad195963380494a21e91b82080 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sun, 27 Jan 2019 22:29:48 +1100 Subject: [PATCH 112/131] Remove unused require. --- Specs/Scene/TerrainFillMeshSpec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 3ce6a98efa2..abd987c9efc 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -13,7 +13,6 @@ defineSuite([ 'Scene/SceneMode', 'Scene/TileBoundingRegion', 'Scene/TileSelectionResult', - 'ThirdParty/when', '../MockTerrainProvider', '../TerrainTileProcessor' ], function( @@ -31,7 +30,6 @@ defineSuite([ SceneMode, TileBoundingRegion, TileSelectionResult, - when, MockTerrainProvider, TerrainTileProcessor) { 'use strict'; From 354423d5678aa4f46d660d271491c12b7bf5cab1 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Jan 2019 18:08:58 +1100 Subject: [PATCH 113/131] Fix a typo making Firefox explode. --- Source/Scene/GlobeSurfaceTileProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index c5862fe69ee..c04b535c1aa 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -552,7 +552,7 @@ define([ } } - if (hadFillBefore && !define(surfaceTile.fill)) { + if (hadFillBefore && !defined(surfaceTile.fill)) { // Transitioned from a fill to real geometry, so we'll need to update the heights // of things in this tile. this._quadtree._tileToUpdateHeights.push(tile); From 17547a0461722904517f2fdb3fd0df83beb81b1d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Jan 2019 17:08:37 +1100 Subject: [PATCH 114/131] A few small cleanup things. --- Apps/Sandcastle/gallery/Terrain.html | 18 ------------------ Source/Core/TerrainMesh.js | 2 +- Source/Scene/Globe.js | 4 ++-- Source/Scene/QuadtreePrimitive.js | 2 +- Source/Workers/upsampleQuantizedTerrainMesh.js | 4 +--- 5 files changed, 5 insertions(+), 25 deletions(-) diff --git a/Apps/Sandcastle/gallery/Terrain.html b/Apps/Sandcastle/gallery/Terrain.html index 94fee14ce38..730417f53e7 100644 --- a/Apps/Sandcastle/gallery/Terrain.html +++ b/Apps/Sandcastle/gallery/Terrain.html @@ -42,8 +42,6 @@ terrainProvider: worldTerrain }); -Cesium.viewerCesiumInspectorMixin(viewer); - // set lighting to true viewer.scene.globe.enableLighting = true; @@ -61,22 +59,6 @@ viewer.terrainProvider = worldTerrain; viewer.scene.globe.enableLighting = true; } -}, { - text : 'STK World Terrain', - onselect : function() { - viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ - url: 'http://assets.agi.com/stk-terrain/world' - }); - viewer.scene.globe.enableLighting = false; - } -}, { - text : 'Local with Half Dome', - onselect : function() { - viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ - url: 'http://localhost:8081/2018-07-10' - }); - viewer.scene.globe.enableLighting = false; - } }, { text : 'CesiumTerrainProvider - Cesium World Terrain - no effects', onselect : function() { diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js index 5f69f8fc90f..0b2683c01d3 100644 --- a/Source/Core/TerrainMesh.js +++ b/Source/Core/TerrainMesh.js @@ -32,7 +32,7 @@ define([ * @param {Number[]} westIndicesSouthToNorth The indices of the vertices on the Western edge of the tile, ordered from South to North (clockwise). * @param {Number[]} southIndicesEastToWest The indices of the vertices on the Southern edge of the tile, ordered from East to West (clockwise). * @param {Number[]} eastIndicesNorthToSouth The indices of the vertices on the Eastern edge of the tile, ordered from North to South (clockwise). - * @param {Number[]} northIndicesWestToEast The indices of the vertices on the Northern edge of the tile, ordered tom West to East (clockwise). + * @param {Number[]} northIndicesWestToEast The indices of the vertices on the Northern edge of the tile, ordered from West to East (clockwise). * * @private */ diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 4daabad33e0..26199eadff2 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -609,9 +609,9 @@ define([ } // This tile was either rendered or culled. - // It is sometimes useful to get a height from culled tile, + // It is sometimes useful to get a height from a culled tile, // e.g. when we're getting a height in order to place a billboard - // on terrain, and the camera is centered on that same billboard. + // on terrain, and the camera is looking at that same billboard. // The culled tile must have a valid mesh, though. if (!defined(tile.data) || !defined(tile.data.renderedMesh)) { // Tile was not rendered (culled). diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index b7494d1755d..2f106d64786 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -119,7 +119,7 @@ define([ this._updateHeightsTimeSlice = 2.0; // If a culled tile contains a cartographic positions in this list, it will be marked - // TileSelection.CULLED_BUT_NEEDED and added to the list of tiles to update heights, + // TileSelectionResult.CULLED_BUT_NEEDED and added to the list of tiles to update heights, // even though it is not rendered. The first position will be the position of the // camera and the second will be the origin of the camera's reference frame. this._neededPositions = [undefined, undefined]; diff --git a/Source/Workers/upsampleQuantizedTerrainMesh.js b/Source/Workers/upsampleQuantizedTerrainMesh.js index c78b0471a09..8143ff428fc 100644 --- a/Source/Workers/upsampleQuantizedTerrainMesh.js +++ b/Source/Workers/upsampleQuantizedTerrainMesh.js @@ -531,7 +531,5 @@ define([ } } - var worker = createTaskProcessorWorker(upsampleQuantizedTerrainMesh); - worker._workerFunction = upsampleQuantizedTerrainMesh; - return worker; + return createTaskProcessorWorker(upsampleQuantizedTerrainMesh); }); From 5b6d200b726fc9afba6f3ad8f779937ba5f0040e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Jan 2019 22:05:27 +1100 Subject: [PATCH 115/131] Move Terrain Performance Sandcastle to development folder. --- .../Terrain Performance.html} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename Apps/Sandcastle/gallery/{TerrainPerformance.html => development/Terrain Performance.html} (97%) diff --git a/Apps/Sandcastle/gallery/TerrainPerformance.html b/Apps/Sandcastle/gallery/development/Terrain Performance.html similarity index 97% rename from Apps/Sandcastle/gallery/TerrainPerformance.html rename to Apps/Sandcastle/gallery/development/Terrain Performance.html index 5fb457e2c01..4a33698aa41 100644 --- a/Apps/Sandcastle/gallery/TerrainPerformance.html +++ b/Apps/Sandcastle/gallery/development/Terrain Performance.html @@ -4,8 +4,8 @@ - - + + Cesium Demo From c6d651a6687f4d44f471e788d578487a4911f174 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Jan 2019 22:47:30 +1100 Subject: [PATCH 116/131] Address TODOs. --- Source/Scene/Globe.js | 3 +-- Source/Scene/GlobeSurfaceShaderSet.js | 2 +- Source/Scene/GlobeSurfaceTile.js | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 26199eadff2..46680e73626 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -504,8 +504,7 @@ define([ var boundingVolume = surfaceTile.pickBoundingSphere; if (mode !== SceneMode.SCENE3D) { - // TODO: ok to allocate / recreate the bounding sphere every time here? - boundingVolume = BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, surfaceTile.tileBoundingRegion.minimumHeight, surfaceTile.tileBoundingRegion.maximumHeight, boundingVolume); + surfaceTile.pickBoundingSphere = boundingVolume = BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, surfaceTile.tileBoundingRegion.minimumHeight, surfaceTile.tileBoundingRegion.maximumHeight, boundingVolume); Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center); } else if (defined(surfaceTile.renderedMesh)) { BoundingSphere.clone(surfaceTile.renderedMesh.boundingSphere3D, boundingVolume); diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index fce5badd422..56429d4d573 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -105,7 +105,7 @@ define([ var vertexLogDepth = 0; var vertexLogDepthDefine = ''; - if (surfaceTile.terrainData === undefined || surfaceTile.terrainData._createdByUpsampling) { + if (!defined(surfaceTile.vertexArray) || !defined(surfaceTile.terrainData) || surfaceTile.terrainData._createdByUpsampling) { vertexLogDepth = 1; vertexLogDepthDefine = 'DISABLE_GL_POSITION_LOG_DEPTH'; } diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 79880c33f84..d86eed65f20 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -96,7 +96,6 @@ define([ this.mesh = undefined; this.fill = undefined; - // TODO: probably better to have a bounding sphere for 2D rather than one for picking. this.pickBoundingSphere = new BoundingSphere(); this.surfaceShader = undefined; From ee7744d9d5d6f3f1e126e0e0c3e8a506d40702e4 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 31 Jan 2019 21:16:55 +1100 Subject: [PATCH 117/131] Remove unnecessary renderable tile tracking. --- Source/Scene/GlobeSurfaceTile.js | 2 - Source/Scene/GlobeSurfaceTileProvider.js | 14 ++--- Source/Scene/QuadtreePrimitive.js | 66 ++++++++++-------------- 3 files changed, 31 insertions(+), 51 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index d86eed65f20..61dca08083d 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -80,8 +80,6 @@ define([ this.orientedBoundingBox = undefined; this.boundingVolumeSourceTile = undefined; - this.renderableTile = undefined; - /** * A bounding region used to estimate distance to the tile. The horizontal bounds are always tight-fitting, * but the `minimumHeight` and `maximumHeight` properties may be derived from the min/max of an ancestor tile diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index c04b535c1aa..8ad1b3d0f87 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -823,9 +823,8 @@ define([ * * @param {QuadtreeTile} tile The tile instance. * @param {FrameState} frameState The state information of the current rendering frame. - * @param {QuadtreeTile} [nearestRenderableTile] The nearest ancestor tile that is renderable. */ - GlobeSurfaceTileProvider.prototype.showTileThisFrame = function(tile, frameState, nearestRenderableTile) { + GlobeSurfaceTileProvider.prototype.showTileThisFrame = function(tile, frameState) { var readyTextureCount = 0; var tileImageryCollection = tile.data.imagery; for (var i = 0, len = tileImageryCollection.length; i < len; ++i) { @@ -844,17 +843,10 @@ define([ tileSet.push(tile); var surfaceTile = tile.data; - if (nearestRenderableTile !== undefined && nearestRenderableTile !== tile) { + if (!defined(surfaceTile.vertexArray)) { this._hasFillTilesThisFrame = true; - - surfaceTile.renderableTile = nearestRenderableTile; - - // The renderable tile may have previously deferred to an ancestor. - // But we know it's renderable now, so mark it as such. - nearestRenderableTile.data.renderableTile = undefined; } else { this._hasLoadedTilesThisFrame = true; - surfaceTile.renderableTile = undefined; } var debug = this._debug; @@ -1545,7 +1537,7 @@ define([ function addDrawCommandsForTile(tileProvider, tile, frameState) { var surfaceTile = tile.data; - if (surfaceTile.renderableTile !== undefined) { + if (!defined(surfaceTile.vertexArray)) { if (surfaceTile.fill === undefined) { // No fill was created for this tile, probably because this tile is not connected to // any renderable tiles. So create a simple tile in the middle of the tile's possible diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 2f106d64786..d6784c71f65 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -102,7 +102,6 @@ define([ var ellipsoid = tilingScheme.ellipsoid; this._tilesToRender = []; - this._nearestRenderableTiles = []; this._tileLoadQueueHigh = []; // high priority tiles are preventing refinement this._tileLoadQueueMedium = []; // medium priority tiles are being rendered this._tileLoadQueueLow = []; // low priority tiles were refined past or are non-visible parts of quads. @@ -513,7 +512,6 @@ define([ // Clear the render list. var tilesToRender = primitive._tilesToRender; tilesToRender.length = 0; - primitive._nearestRenderableTiles.length = 0; // We can't render anything before the level zero tiles exist. var i; @@ -577,7 +575,7 @@ define([ queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); ++debug.tilesWaitingForChildren; } else { - visitIfVisible(primitive, tile, tileProvider, frameState, occluders, tile, false, rootTraversalDetails[i]); + visitIfVisible(primitive, tile, tileProvider, frameState, occluders, false, rootTraversalDetails[i]); } } @@ -636,12 +634,11 @@ define([ * @param {Primitive} primitive The QuadtreePrimitive. * @param {FrameState} frameState The frame state. * @param {QuadtreeTile} tile The tile to visit - * @param {QuadtreeTile} nearestRenderableTile The nearest ancestor tile for which the `renderable` property is true. * @param {Boolean} ancestorMeetsSse True if a tile higher in the tile tree already met the SSE and we're refining further only * to maintain detail while that higher tile loads. * @param {TraversalDetails} traveralDetails On return, populated with details of how the traversal of this tile went. */ - function visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { + function visitTile(primitive, frameState, tile, ancestorMeetsSse, traversalDetails) { var debug = primitive._debug; ++debug.tilesVisited; @@ -653,10 +650,6 @@ define([ debug.maxDepthVisited = tile.level; } - if (tile.renderable) { - nearestRenderableTile = tile; - } - var meetsSse = screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError; var southwestChild = tile.southwestChild; @@ -705,7 +698,7 @@ define([ if (meetsSse) { queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); } - addTileToRenderList(primitive, tile, nearestRenderableTile); + addTileToRenderList(primitive, tile); traversalDetails.allAreRenderable = tile.renderable; traversalDetails.anyWereRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED; @@ -742,7 +735,7 @@ define([ if (allAreUpsampled) { // No point in rendering the children because they're all upsampled. Render this tile instead. - addTileToRenderList(primitive, tile, nearestRenderableTile); + addTileToRenderList(primitive, tile); // Rendered tile that's not waiting on children loads with medium priority. queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); @@ -779,7 +772,7 @@ define([ var tilesToUpdateHeightsIndex = primitive._tileToUpdateHeights.length; // No need to add the children to the load queue because they'll be added (if necessary) when they're visited. - visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, nearestRenderableTile, ancestorMeetsSse, traversalDetails); + visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, ancestorMeetsSse, traversalDetails); // If no descendant tiles were added to the render list by the function above, it means they were all // culled even though this tile was deemed visible. That's pretty common. @@ -808,9 +801,8 @@ define([ // Remove all descendants from the render list and add this tile. primitive._tilesToRender.length = firstRenderedDescendantIndex; - primitive._nearestRenderableTiles.length = firstRenderedDescendantIndex; primitive._tileToUpdateHeights.length = tilesToUpdateHeightsIndex; - addTileToRenderList(primitive, tile, nearestRenderableTile); + addTileToRenderList(primitive, tile); tile._lastSelectionResult = TileSelectionResult.RENDERED; @@ -854,7 +846,7 @@ define([ // so we have no idea if refinining would involve a load or an upsample. We'll have to finish // loading this tile first in order to find that out, so load this refinement blocker with // high priority. - addTileToRenderList(primitive, tile, nearestRenderableTile); + addTileToRenderList(primitive, tile); queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); traversalDetails.allAreRenderable = tile.renderable; @@ -862,7 +854,7 @@ define([ traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; } - function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { + function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, ancestorMeetsSse, traversalDetails) { var cameraPosition = frameState.camera.positionCartographic; var tileProvider = primitive._tileProvider; var occluders = primitive._occluders; @@ -876,29 +868,29 @@ define([ if (cameraPosition.longitude < southwest.rectangle.east) { if (cameraPosition.latitude < southwest.rectangle.north) { // Camera in southwest quadrant - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails); } else { // Camera in northwest quadrant - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails); } } else if (cameraPosition.latitude < southwest.rectangle.north) { // Camera southeast quadrant - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails); } else { // Camera in northeast quadrant - visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northeastDetails); - visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, northwestDetails); - visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southeastDetails); - visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, southwestDetails); + visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails); + visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails); + visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails); + visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails); } quadDetails.combine(traversalDetails); @@ -918,9 +910,9 @@ define([ return false; } - function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, nearestRenderableTile, ancestorMeetsSse, traversalDetails) { + function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, ancestorMeetsSse, traversalDetails) { if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) { - return visitTile(primitive, frameState, tile, nearestRenderableTile, ancestorMeetsSse, traversalDetails); + return visitTile(primitive, frameState, tile, ancestorMeetsSse, traversalDetails); } ++primitive._debug.tilesCulled; @@ -996,9 +988,8 @@ define([ return error; } - function addTileToRenderList(primitive, tile, nearestRenderableTile) { + function addTileToRenderList(primitive, tile) { primitive._tilesToRender.push(tile); - primitive._nearestRenderableTiles.push(nearestRenderableTile); } function processTileLoadQueue(primitive, frameState) { @@ -1171,11 +1162,10 @@ define([ function createRenderCommandsForSelectedTiles(primitive, frameState) { var tileProvider = primitive._tileProvider; var tilesToRender = primitive._tilesToRender; - var nearestRenderableTiles = primitive._nearestRenderableTiles; for (var i = 0, len = tilesToRender.length; i < len; ++i) { var tile = tilesToRender[i]; - tileProvider.showTileThisFrame(tile, frameState, nearestRenderableTiles[i]); + tileProvider.showTileThisFrame(tile, frameState); } } From a89fc609b0f01fc76472e5684a25c9bca12548a1 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 31 Jan 2019 21:39:32 +1100 Subject: [PATCH 118/131] Remove unnecessary height updating. --- Source/Scene/GlobeSurfaceTileProvider.js | 16 +--------------- Source/Scene/QuadtreePrimitive.js | 4 ++-- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 8ad1b3d0f87..8026d225197 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -529,13 +529,9 @@ define([ var surfaceTile = tile.data; var terrainOnly = true; var terrainStateBefore; - var hadFillBefore = false; - var hadVertexArrayBefore = false; if (defined(surfaceTile)) { - terrainOnly = surfaceTile.boundingVolumeSourceTile !== tile; + terrainOnly = surfaceTile.boundingVolumeSourceTile !== tile || tile._lastSelectionResult === TileSelectionResult.CULLED_BUT_NEEDED; terrainStateBefore = surfaceTile.terrainState; - hadFillBefore = defined(surfaceTile.fill); - hadVertexArrayBefore = defined(surfaceTile.vertexArray); } GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly); @@ -551,16 +547,6 @@ define([ GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly); } } - - if (hadFillBefore && !defined(surfaceTile.fill)) { - // Transitioned from a fill to real geometry, so we'll need to update the heights - // of things in this tile. - this._quadtree._tileToUpdateHeights.push(tile); - } else if (tile._lastSelectionResult === TileSelectionResult.CULLED_BUT_NEEDED && !hadVertexArrayBefore && defined(surfaceTile.vertexArray)) { - // This tile is not being rendered but IS used for getting heights, and it just acquired some geometry. - // So we need to update the heights based on the new geometry. - this._quadtree._tileToUpdateHeights.push(tile); - } }; var boundingSphereScratch = new BoundingSphere(); diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index d6784c71f65..3a2785e8124 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -1058,8 +1058,8 @@ define([ while (tilesToUpdateHeights.length > 0) { var tile = tilesToUpdateHeights[0]; - if (tile.state !== QuadtreeTileLoadState.DONE) { - // Tile isn't loaded yet, so try again next frame if this tile is still + if (!defined(tile.data) || !defined(tile.data.mesh)) { + // Tile isn't loaded enough yet, so try again next frame if this tile is still // being rendered. var selectionResult = tile._lastSelectionResultFrame === primitive._lastSelectionFrameNumber ? tile._lastSelectionResult From f56791f472fa947ed2a51b81850088a0a1bd96ba Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 1 Feb 2019 14:33:35 +1100 Subject: [PATCH 119/131] Handle possibility that camera is right at tile OBB center. --- Source/Scene/GlobeSurfaceTileProvider.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 8026d225197..9b197538c09 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -789,7 +789,12 @@ define([ var cameraPosition = frameState.camera.positionWC; var cameraDirection = frameState.camera.directionWC; - var tileDirection = Cartesian3.normalize(Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch), tileDirectionScratch); + var tileDirection = Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch); + var magnitude = Cartesian3.magnitude(tileDirection); + if (magnitude < CesiumMath.EPSILON5) { + return 0.0; + } + Cartesian3.divideByScalar(tileDirection, magnitude, tileDirection); return (1.0 - Cartesian3.dot(tileDirection, cameraDirection)) * tile._distance; }; From 712325ca91a6c333a599b33447b8eb08a55db096 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 1 Feb 2019 14:55:13 +1100 Subject: [PATCH 120/131] Address TODOs. --- Source/Scene/QuadtreePrimitive.js | 5 ++-- Source/Scene/TerrainFillMesh.js | 49 ++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 3a2785e8124..8859c580364 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -784,6 +784,7 @@ define([ var allAreRenderable = traversalDetails.allAreRenderable; var anyWereRenderedLastFrame = traversalDetails.anyWereRenderedLastFrame; var notYetRenderableCount = traversalDetails.notYetRenderableCount; + var queuedForLoad = false; if (!allAreRenderable && !anyWereRenderedLastFrame) { // Some of our descendants aren't ready to render yet, and none were rendered last frame, @@ -817,6 +818,7 @@ define([ primitive._tileLoadQueueHigh.length = loadIndexHigh; queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1; + queuedForLoad = true; } traversalDetails.allAreRenderable = tile.renderable; @@ -830,8 +832,7 @@ define([ ++debug.tilesWaitingForChildren; } - if (primitive.preloadAncestors) { - // TODO: don't queue here if this tile was queued at medium above. + if (primitive.preloadAncestors && !queuedForLoad) { queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState); } } diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index f5156729d76..b5cbd41f1cc 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -459,6 +459,11 @@ define([ return vertex; } + var heightRangeScratch = { + minimumHeight: 0.0, + maximumHeight: 0.0 + }; + var swVertexScratch = new HeightAndNormal(); var seVertexScratch = new HeightAndNormal(); var nwVertexScratch = new HeightAndNormal(); @@ -527,7 +532,7 @@ define([ }); fill.mesh = terrainData._createMeshSync(tile.tilingScheme, tile.x, tile.y, tile.level, 1.0); } else { - var encoding = new TerrainEncoding(undefined, minimumHeight, maximumHeight, undefined, true, true); + var encoding = new TerrainEncoding(undefined, undefined, undefined, undefined, true, true); var centerCartographic = centerCartographicScratch; centerCartographic.longitude = (rectangle.east + rectangle.west) * 0.5; @@ -562,27 +567,33 @@ define([ maxVertexCount += meshes[i].southIndicesEastToWest.length; } + var heightRange = heightRangeScratch; + heightRange.minimumHeight = minimumHeight; + heightRange.maximumHeight = maximumHeight; + var stride = encoding.getStride(); var typedArray = new Float32Array(maxVertexCount * stride); var nextIndex = 0; var northwestIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 1.0, nwCorner.height, nwCorner.encodedNormal, 1.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.westTiles, fill.westMeshes, TileEdge.EAST); + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 1.0, nwCorner.height, nwCorner.encodedNormal, 1.0, heightRange); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.westTiles, fill.westMeshes, TileEdge.EAST, heightRange); var southwestIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 0.0, swCorner.height, swCorner.encodedNormal, 0.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.southTiles, fill.southMeshes, TileEdge.NORTH); + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 0.0, swCorner.height, swCorner.encodedNormal, 0.0, heightRange); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.southTiles, fill.southMeshes, TileEdge.NORTH, heightRange); var southeastIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 0.0, seCorner.height, seCorner.encodedNormal, 0.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.eastTiles, fill.eastMeshes, TileEdge.WEST); + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 0.0, seCorner.height, seCorner.encodedNormal, 0.0, heightRange); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.eastTiles, fill.eastMeshes, TileEdge.WEST, heightRange); var northeastIndex = nextIndex; - nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 1.0, neCorner.height, neCorner.encodedNormal, 1.0); - nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.northTiles, fill.northMeshes, TileEdge.SOUTH); + nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 1.0, neCorner.height, neCorner.encodedNormal, 1.0, heightRange); + nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.northTiles, fill.northMeshes, TileEdge.SOUTH, heightRange); + + minimumHeight = heightRange.minimumHeight; + maximumHeight = heightRange.maximumHeight; - // Add a single vertex at the center of the tile. - // TODO: minimumHeight and maximumHeight only reflect the corners var obb = OrientedBoundingBox.fromRectangle(rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid); + // Add a single vertex at the center of the tile. var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south); var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY); var centerWebMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(centerCartographic.latitude) - southMercatorY) * oneOverMercatorHeight; @@ -680,7 +691,7 @@ define([ surfaceTile.processImagery(tile, tileProvider.terrainProvider, frameState, true); } - function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT) { + function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT, heightRange) { var cartographic = cartographicScratch; cartographic.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); cartographic.latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v); @@ -693,6 +704,9 @@ define([ encoding.encode(buffer, index * encoding.getStride(), position, uv, height, encodedNormal, webMercatorT); + heightRange.minimumHeight = Math.min(heightRange.minimumHeight, height); + heightRange.maximumHeight = Math.max(heightRange.maximumHeight, height); + return index + 1; } @@ -794,8 +808,6 @@ define([ Cartesian3.normalize(normal, normal); AttributeCompression.octEncode(normal, vertex.encodedNormal); } else { - // TODO: do we need to actually compute a normal? - // It's probably going to be unused so 0,0 is just as good. normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch); AttributeCompression.octEncode(normal, vertex.encodedNormal); } @@ -906,14 +918,14 @@ define([ return height2; } - function addEdge(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles, edgeMeshes, tileEdge) { + function addEdge(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles, edgeMeshes, tileEdge, heightRange) { for (var i = 0; i < edgeTiles.length; ++i) { - nextIndex = addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles[i], edgeMeshes[i], tileEdge); + nextIndex = addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles[i], edgeMeshes[i], tileEdge, heightRange); } return nextIndex; } - function addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTile, edgeMesh, tileEdge) { + function addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTile, edgeMesh, tileEdge, heightRange) { // Handle copying edges across the anti-meridian. var sourceRectangle = edgeTile.rectangle; if (tileEdge === TileEdge.EAST && terrainFillMesh.tile.x === 0) { @@ -1019,6 +1031,9 @@ define([ encoding.encode(typedArray, nextIndex * targetStride, position, uv, height, normal, webMercatorT); + heightRange.minimumHeight = Math.min(heightRange.minimumHeight, height); + heightRange.maximumHeight = Math.max(heightRange.maximumHeight, height); + ++nextIndex; } From 21394f00c893c840ef8780923386489aa668ef12 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 1 Feb 2019 15:02:47 +1100 Subject: [PATCH 121/131] Remove terrainnotes.md. --- terrainnotes.md | 113 ------------------------------------------------ 1 file changed, 113 deletions(-) delete mode 100644 terrainnotes.md diff --git a/terrainnotes.md b/terrainnotes.md deleted file mode 100644 index 4d423e4882a..00000000000 --- a/terrainnotes.md +++ /dev/null @@ -1,113 +0,0 @@ -To refine past a tile, one of the following conditions must be met: - - * The tile is loaded. - * The tile has a SSE that is _known_ (not just suspected) to be too high AND we know which of the tile's children exist. We'll know the SSE is too high if: - * The distance to the tile is known accurately (e.g. we know the precise min/max height), or - * The bounding volume for the tile is known to be X or _closer_, and X is sufficient to necessitate refinement. For example, if we have no min/max height information for this tile, but the parent tile has a min of A and a max of B, we know the child must fall within these bounds as well. Therefore, the child tile can be no farther away than max(distance to A, distance to B). - -Idea: when a tile is determined to be _fully_ visible based on min/max heights from an ancestor, and it meets the SSE, we can be certain that either that tile or its children are relevant for rendering. But if it's only partially visible, we might load it only to find out that it's culled. So, when we have BVH data available, and a tile is only partially visible based on estimated heights, we should load the nearest ancestor BVH node instead of loading the tile. - -GlobeSurfaceTile properties: - - * `tileBoundingRegion` is used to estimate the distance to the tile. It is _not_ used for culling. - * `orientedBoundingBox` is used for culling, _not_ for distance estimation. - * `minimumHeight` and `maximumheight` are used for picking, for culling in 2D, and for `updateHeights` in `QuadtreePrimitive`. - - -A tile conceptually has three sets of min/max heights: - - * The spatially coherent min/max of the real terrain in this horizontal extent, i.e. the min/max of this tile and all its descendants. - * Distance estimation: Great! - * Culling for rendering: Great! - * Skipping loading: Great! - * The min/max of the geometry, which may not reflect the full range of heights of descendants. - * Distance estimation: Not perfect, but should be good enough. - * Culling for rendering: Great! - * Skipping loading: Not perfect, but should be good enough. - * The (spatially coherent) min/max of the parent tile, applied to this tile. - * Distance estimation: Tile is guaranteed to be closer than the farther-away height surface (min or max). - * Culling for rendering: If test says its invisible, it definitely is. If test says its visible, it may or may not be. - * Skipping loading: We can skip loading if the tile is culled. - - - -## Tile properties - -* Available from construction: - * level - * x - * y - * rectangle - * tilingScheme - * parent - * southwestChild - * southeastChild - * northwestChild - * northeastChild - * state -* Populated by loading or upsampling - * vertexArray (rendering) - * center (rendering vertexArray RTC) - * imagery (rendering) - * waterMaskTexture (rendering) - * waterMaskTranslationAndScale (rendering) - * orientedBoundingBox (culling) - * occludeePointInScaledSpace (culling) - * boundingSphere3D (culling, when orientedBoundingBox isn't available) - * terrainData (used to upsample children, determine child tile mask) - * upsampledFromParent (true if terrain and all imagery are just upsampled from the parent) - * renderable (true if the tile is renderable at all) -* Populated by non-tile loading - * childTileMask / getTileDataAvailable (not stored on tile directly, comes from terrainData.childTileMask or TerrainProvider.getTileDataAvailable, determines if we need to upsample) - * tileBoundingRegion (distance estimation for SSE, load priority) -* Caching / per-frame intermediates - * surfaceShader (shader used to render this tile last frame) - * _distance (distance from the camera to this tile, set as a side-effect of GlobeSurfaceTileProvider#computeTileVisibility) - * isClipped (true if the tile is clipped by a custom clipping plane, set as a side-effect of GlobeSurfaceTileProvider#computeTileVisibility) - * replacementPrevious / replacementNext (This tile's position in the TileReplacementQueue linked list) -* Probably not needed - * pickBoundingSphere (assigned and used in Globe#pick, not clear why this needs to be a tile property) - * pickTerrain (points to either the loaded or upsampled TileTerrain instance) - * minimumHeight / maximumHeight (same as the min/max height in tileBoundingRegion?) -* Not sure - * _priorityFunction (used to prioritize requests, returns the tile's distance to the tileBoundingRegion) - * _customData (custom data that is inside the bounds of this tile) - * _frameUpdated (used in QuadtreeTile#_updateCustomData to determine when a parent tile was updated more (?) recently than this tile) - * _frameRendered (used in QuadtreePrimitive#createRenderCommandsForSelectedTiles to determine which tiles need updated heights) - * _loadedCallbacks (used for WMTS (?) to remove old imagery once new imagery is loaded. I think.) - - -TileBoundingRegion has a both an oriented bounding box and a bounding sphere, but we don't use either of them for terrain. -Instead, tiles store separate copies of these things (orientingBoundingBox and boundingSphere3D). That's probably good because we update the min/max height in publishToTile but we don't update either of the other two. - -## Rendering without data - -When the camera moves, new tiles become visible. If those tiles aren't loaded yet, what do we do? Options are: - - 1. Render nothing. There will be holes in the Earth's surface. - 2. Upsample synchronously. Each upsample takes 2-3ms on a fast computer so we can't do much of this without the frame rate taking a dive. - 3. Render an ancestor tile and discard fragments outside the bounds of the tile. This is nearly free on the CPU, but expensive on the GPU because we end up rendering large tiles and discarding most of the fragments. - 4. Create a tile on-the-fly to fill the space, e.g. that aligns with the edge vertices of adjacent tiles. - 5. Don't refine until all descendants are renderable (but do load the descendants we want to render, of course!). - 6. Show some kind of blurry blobby placeholder for the not-yet-available tile. - -Number 5 is a tempting strategy, with one big problem: it can cause us to lose detail from the scene with small camera movements. For example, if we're looking at a detailed scene that shows 3 out of 4 children of a level 14 tile, and then move the camera so that the 4th is visible, suddenly that level 14 tile isn't refinable anymore. If we start rendering that level 14 tiles intead of its children, we'll lose detail from the scene for a second until that 4th tile loads. This looks really bad. - -Important rules: -* if we rendered a tile last frame, and it's still visible and the correct SSE this frame, we must render it this frame. In the scenario above, we must use one of our other strategies to fill the space of that 4th child of the level 14 tiles. -* Detail should never disappear when zooming in. i.e. upsampling is ok, but creating fill tiles is not. -* We can create fill tiles for areas the user hasn't seen recently. - -Specifically: -* Ancestor rendered last frame -> upsample (e.g. zooming in) -* Descendants rendered last frame but now this tile meets SSE -> continue rendering descendants, create fill tiles as necessary for areas that weren't previously visible (e.g. zooming out) - * _Optionally_ we can preload ancestors to optimize the zoom out experience. - * Note that this applies to imagery too! Zooming out should never cause detail to disappear and then come back. -* Sub-tree was culled last frame but now it's visible -> create fill tiles as necessary (e.g. panning) - -## Min/max heights - -Tiles have min/max heights in a whole bunch of places 😨: -* GlobeSurfaceTile.tileBoundingRegion -* GlobeSurfaceTile.terrainData -* GlobeSurfaceTile.mesh.encoding From cf657be157a706a30fed1c5da59af549fca44079 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 2 Feb 2019 14:13:57 +1100 Subject: [PATCH 122/131] Fix water mask in fill tiles. --- Source/Scene/GlobeSurfaceTile.js | 36 ++++++++++++------------ Source/Scene/GlobeSurfaceTileProvider.js | 8 +++++- Source/Scene/TerrainFillMesh.js | 31 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 61dca08083d..79c3a221b85 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -396,7 +396,12 @@ define([ if (terrainData.waterMask !== undefined) { createWaterMaskTextureIfNeeded(frameState.context, surfaceTile); } else { - upsampleWaterMask(tile); + var sourceTile = surfaceTile._findAncestorTileWithTerrainData(tile); + if (defined(sourceTile) && defined(sourceTile.data.waterMaskTexture)) { + surfaceTile.waterMaskTexture = sourceTile.data.waterMaskTexture; + ++surfaceTile.waterMaskTexture.referenceCount; + surfaceTile._computeWaterMaskTranslationAndScale(tile, sourceTile, surfaceTile.waterMaskTranslationAndScale); + } } } } @@ -644,24 +649,17 @@ define([ Cartesian4.fromElements(0.0, 0.0, 1.0, 1.0, surfaceTile.waterMaskTranslationAndScale); } - function upsampleWaterMask(tile) { - var surfaceTile = tile.data; - - // Find the nearest ancestor with loaded terrain. + GlobeSurfaceTile.prototype._findAncestorTileWithTerrainData = function(tile) { var sourceTile = tile.parent; + while (defined(sourceTile) && (!defined(sourceTile.data) || !defined(sourceTile.data.terrainData) || sourceTile.data.terrainData.wasCreatedByUpsampling())) { sourceTile = sourceTile.parent; } - if (!defined(sourceTile) || !defined(sourceTile.data.waterMaskTexture)) { - // No ancestors have a water mask texture - try again later. - return; - } - - surfaceTile.waterMaskTexture = sourceTile.data.waterMaskTexture; - ++surfaceTile.waterMaskTexture.referenceCount; + return sourceTile; + }; - // Compute the water mask translation and scale + GlobeSurfaceTile.prototype._computeWaterMaskTranslationAndScale = function(tile, sourceTile, result) { var sourceTileRectangle = sourceTile.rectangle; var tileRectangle = tile.rectangle; var tileWidth = tileRectangle.width; @@ -669,11 +667,13 @@ define([ var scaleX = tileWidth / sourceTileRectangle.width; var scaleY = tileHeight / sourceTileRectangle.height; - surfaceTile.waterMaskTranslationAndScale.x = scaleX * (tileRectangle.west - sourceTileRectangle.west) / tileWidth; - surfaceTile.waterMaskTranslationAndScale.y = scaleY * (tileRectangle.south - sourceTileRectangle.south) / tileHeight; - surfaceTile.waterMaskTranslationAndScale.z = scaleX; - surfaceTile.waterMaskTranslationAndScale.w = scaleY; - } + result.x = scaleX * (tileRectangle.west - sourceTileRectangle.west) / tileWidth; + result.y = scaleY * (tileRectangle.south - sourceTileRectangle.south) / tileHeight; + result.z = scaleX; + result.w = scaleY; + + return result; + }; return GlobeSurfaceTile; }); diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 9b197538c09..958531c41d2 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -1552,6 +1552,12 @@ define([ var maxTextures = ContextLimits.maximumTextureImageUnits; var waterMaskTexture = surfaceTile.waterMaskTexture; + var waterMaskTranslationAndScale = surfaceTile.waterMaskTranslationAndScale; + if (!defined(waterMaskTexture) && defined(surfaceTile.fill)) { + waterMaskTexture = surfaceTile.fill.waterMaskTexture; + waterMaskTranslationAndScale = surfaceTile.fill.waterMaskTranslationAndScale; + } + var showReflectiveOcean = tileProvider.hasWaterMask && defined(waterMaskTexture); var oceanNormalMap = tileProvider.oceanNormalMap; var showOceanWaves = showReflectiveOcean && defined(oceanNormalMap); @@ -1865,7 +1871,7 @@ define([ // which might get destroyed eventually uniformMapProperties.dayTextures.length = numberOfDayTextures; uniformMapProperties.waterMask = waterMaskTexture; - Cartesian4.clone(surfaceTile.waterMaskTranslationAndScale, uniformMapProperties.waterMaskTranslationAndScale); + Cartesian4.clone(waterMaskTranslationAndScale, uniformMapProperties.waterMaskTranslationAndScale); uniformMapProperties.minMaxHeight.x = encoding.minimumHeight; uniformMapProperties.minMaxHeight.y = encoding.maximumHeight; diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index b5cbd41f1cc..83aca7a078e 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -4,6 +4,7 @@ define([ '../Core/BoundingSphere', '../Core/Cartesian2', '../Core/Cartesian3', + '../Core/Cartesian4', '../Core/Cartographic', '../Core/defined', '../Core/HeightmapTerrainData', @@ -24,6 +25,7 @@ define([ BoundingSphere, Cartesian2, Cartesian3, + Cartesian4, Cartographic, defined, HeightmapTerrainData, @@ -64,6 +66,8 @@ define([ this.enqueuedFrame = undefined; this.mesh = undefined; this.vertexArray = undefined; + this.waterMaskTexture = undefined; + this.waterMaskTranslationAndScale = new Cartesian4(); } TerrainFillMesh.prototype.update = function(tileProvider, frameState, vertexArraysToDestroy) { @@ -82,6 +86,15 @@ define([ } this.vertexArray = undefined; } + + if (defined(this.waterMaskTexture)) { + --this.waterMaskTexture.referenceCount; + if (this.waterMaskTexture.referenceCount === 0) { + this.waterMaskTexture.destroy(); + } + this.waterMaskTexture = undefined; + } + return undefined; }; @@ -689,6 +702,24 @@ define([ fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, fill.mesh); surfaceTile.processImagery(tile, tileProvider.terrainProvider, frameState, true); + + var oldTexture = fill.waterMaskTexture; + + if (tileProvider.terrainProvider.hasWaterMask) { + var waterSourceTile = surfaceTile._findAncestorTileWithTerrainData(tile); + if (defined(waterSourceTile) && defined(waterSourceTile.data.waterMaskTexture)) { + fill.waterMaskTexture = waterSourceTile.data.waterMaskTexture; + ++fill.waterMaskTexture.referenceCount; + surfaceTile._computeWaterMaskTranslationAndScale(tile, waterSourceTile, fill.waterMaskTranslationAndScale); + } + } + + if (defined(oldTexture)) { + --oldTexture.referenceCount; + if (oldTexture.referenceCount === 0) { + oldTexture.destroy(); + } + } } function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT, heightRange) { From 09760c97024635400e8308ebeb3baa8c7b434300 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 4 Feb 2019 13:14:36 +1100 Subject: [PATCH 123/131] Changes from Omar's review. --- .../gallery/development/Terrain Performance.html | 6 ------ Source/Scene/QuadtreePrimitive.js | 14 +++++++------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Apps/Sandcastle/gallery/development/Terrain Performance.html b/Apps/Sandcastle/gallery/development/Terrain Performance.html index 4a33698aa41..672417f14b9 100644 --- a/Apps/Sandcastle/gallery/development/Terrain Performance.html +++ b/Apps/Sandcastle/gallery/development/Terrain Performance.html @@ -35,12 +35,6 @@ var globe = scene.globe; var statistics = Cesium.RequestScheduler.statistics; -// var cesiumTerrainProviderMeshes = new Cesium.CesiumTerrainProvider({ -// url : 'http://localhost:8001/2018-07-11b', -// requestWaterMask : true, -// requestVertexNormals : true -// }); -// viewer.terrainProvider = cesiumTerrainProviderMeshes; viewer.terrainProvider = Cesium.createWorldTerrain(); var startTime; diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 8859c580364..165fc0a57ec 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -1009,28 +1009,28 @@ define([ var endTime = getTimestamp() + primitive._loadQueueTimeSlice; var tileProvider = primitive._tileProvider; - var didSomething = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueHigh, false); - didSomething = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueMedium, didSomething); - processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow, didSomething); + var didSomeLoading = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueHigh, false); + didSomeLoading = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueMedium, didSomeLoading); + processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow, didSomeLoading); } function sortByLoadPriority(a, b) { return a._loadPriority - b._loadPriority; } - function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue, didSomething) { + function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue, didSomeLoading) { if (tileProvider.computeTileLoadPriority !== undefined) { loadQueue.sort(sortByLoadPriority); } - for (var i = 0, len = loadQueue.length; i < len && (getTimestamp() < endTime || !didSomething); ++i) { + for (var i = 0, len = loadQueue.length; i < len && (getTimestamp() < endTime || !didSomeLoading); ++i) { var tile = loadQueue[i]; primitive._tileReplacementQueue.markTileRendered(tile); tileProvider.loadTile(frameState, tile); - didSomething = true; + didSomeLoading = true; } - return didSomething; + return didSomeLoading; } var scratchRay = new Ray(); From 211e5966b7d49ba54d889529169325cd46e4ee45 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 4 Feb 2019 17:45:50 +1100 Subject: [PATCH 124/131] Put terrain tweakables on Globe, doc updates. --- .../gallery/development/Terrain Tweaks.html | 20 ++++----- Source/Scene/Globe.js | 42 +++++++++++++++++++ Source/Scene/GlobeSurfaceTileProvider.js | 5 ++- Source/Scene/TileImagery.js | 4 +- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/Apps/Sandcastle/gallery/development/Terrain Tweaks.html b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html index 843b188a5f7..31248dce376 100644 --- a/Apps/Sandcastle/gallery/development/Terrain Tweaks.html +++ b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html @@ -72,11 +72,11 @@ var viewer = new Cesium.Viewer('cesiumContainer'); var viewModel = { - loadingDescendantLimit: viewer.scene.globe._surface.loadingDescendantLimit, - preloadAncestors: viewer.scene.globe._surface.preloadAncestors, - preloadSiblings: viewer.scene.globe._surface.preloadSiblings, - fillHighlightColor: Cesium.defined(viewer.scene.globe._surface._tileProvider.fillHighlightColor) ? viewer.scene.globe._surface._tileProvider.fillHighlightColor.toCssColorString() : 'rgba(255, 255, 0, 0.5)', - fillHighlightEnabled: Cesium.defined(viewer.scene.globe._surface._tileProvider.fillHighlightColor) + loadingDescendantLimit: viewer.scene.globe.loadingDescendantLimit, + preloadAncestors: viewer.scene.globe.preloadAncestors, + preloadSiblings: viewer.scene.globe.preloadSiblings, + fillHighlightColor: Cesium.defined(viewer.scene.globe.fillHighlightColor) ? viewer.scene.globe.fillHighlightColor.toCssColorString() : 'rgba(255, 255, 0, 0.5)', + fillHighlightEnabled: Cesium.defined(viewer.scene.globe.fillHighlightColor) }; Cesium.knockout.track(viewModel); @@ -85,20 +85,20 @@ Cesium.knockout.applyBindings(viewModel, toolbar); Cesium.knockout.getObservable(viewModel, 'loadingDescendantLimit').subscribe(function(newValue) { - viewer.scene.globe._surface.loadingDescendantLimit = parseInt(newValue, 10); + viewer.scene.globe.loadingDescendantLimit = parseInt(newValue, 10); }); Cesium.knockout.getObservable(viewModel, 'preloadAncestors').subscribe(function(newValue) { - viewer.scene.globe._surface.preloadAncestors = newValue; + viewer.scene.globe.preloadAncestors = newValue; }); Cesium.knockout.getObservable(viewModel, 'preloadSiblings').subscribe(function(newValue) { - viewer.scene.globe._surface.preloadSiblings = newValue; + viewer.scene.globe.preloadSiblings = newValue; }); function updateFillHighlight() { if (viewModel.fillHighlightEnabled) { - viewer.scene.globe._surface._tileProvider.fillHighlightColor = Cesium.Color.fromCssColorString(viewModel.fillHighlightColor); + viewer.scene.globe.fillHighlightColor = Cesium.Color.fromCssColorString(viewModel.fillHighlightColor); } else { - viewer.scene.globe._surface._tileProvider.fillHighlightColor = undefined; + viewer.scene.globe.fillHighlightColor = undefined; } } diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 46680e73626..1c2c9d03b57 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -129,6 +129,44 @@ define([ */ this.tileCacheSize = 100; + /** + * Gets or sets the number of loading descendant tiles that is considered "too many". + * If a tile has too many loading descendants, that tile will be loaded and rendered before any of + * its descendants are loaded and rendered. This means more feedback for the user that something + * is happening at the cost of a longer overall load time. Setting this to 0 will cause each + * tile level to be loaded successively, significantly increasing load time. Setting it to a large + * number (e.g. 1000) will minimize the number of tiles that are loaded but tend to make + * detail appear all at once after a long wait. + * @type {Number} + * @default 20 + */ + this.loadingDescendantLimit = 20; + + /** + * Gets or sets a value indicating whether the ancestors of rendered tiles should be preloaded. + * Setting this to true optimizes the zoom-out experience and provides more detail in + * newly-exposed areas when panning. The down side is that it requires loading more tiles. + * @type {Boolean} + * @default true + */ + this.preloadAncestors = true; + + /** + * Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded. + * Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even + * if they are culled. Setting this to true may provide a better panning experience at the + * cost of loading more tiles. + */ + this.preloadSiblings = false; + + /** + * The color to use to highlight terrain fill tiles. If undefined, fill tiles are not + * highlighted at all. The alpha value is used to alpha blend with the tile's + * actual color. Because terrain fill tiles do not represent the actual terrain surface, + * it may be useful in some applications to indicate visually that they are not to be trusted. + */ + this.fillHighlightColor = undefined; + /** * Enable lighting the globe with the sun as a light source. * @@ -713,6 +751,9 @@ define([ surface.maximumScreenSpaceError = this.maximumScreenSpaceError; surface.tileCacheSize = this.tileCacheSize; + surface.loadingDescendantLimit = this.loadingDescendantLimit; + surface.preloadAncestors = this.preloadAncestors; + surface.preloadSiblings = this.preloadSiblings; tileProvider.terrainProvider = this.terrainProvider; tileProvider.lightingFadeOutDistance = this.lightingFadeOutDistance; @@ -728,6 +769,7 @@ define([ tileProvider.hueShift = this.atmosphereHueShift; tileProvider.saturationShift = this.atmosphereSaturationShift; tileProvider.brightnessShift = this.atmosphereBrightnessShift; + tileProvider.fillHighlightColor = this.fillHighlightColor; surface.beginFrame(frameState); } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 958531c41d2..3a12468bdf3 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -143,9 +143,10 @@ define([ this.shadows = ShadowMode.RECEIVE_ONLY; /** - * The color to use to highlight fill tiles. If undefined, fill tiles are not + * The color to use to highlight terrain fill tiles. If undefined, fill tiles are not * highlighted at all. The alpha value is used to alpha blend with the tile's - * actual color. + * actual color. Because terrain fill tiles do not represent the actual terrain surface, + * it may be useful in some applications to indicate visually that they are not to be trusted. */ this.fillHighlightColor = undefined; diff --git a/Source/Scene/TileImagery.js b/Source/Scene/TileImagery.js index e1682f2952f..11817176aa9 100644 --- a/Source/Scene/TileImagery.js +++ b/Source/Scene/TileImagery.js @@ -43,7 +43,9 @@ define([ * * @param {Tile} tile The tile to which this instance belongs. * @param {FrameState} frameState The frameState. - * @param {Boolean} skipLoading True to skip loading, but synchronously process imagery that's already mostly ready to go. + * @param {Boolean} skipLoading True to skip loading, e.g. new requests, creating textures. This function will + * still synchronously process imagery that's already mostly ready to go, e.g. use textures + * already loaded on ancestor tiles. * @returns {Boolean} True if this instance is done loading; otherwise, false. */ TileImagery.prototype.processStateMachine = function(tile, frameState, skipLoading) { From a88c200fa0dda186aeb11834a3184ecf5da0f6b4 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 4 Feb 2019 22:25:26 +1100 Subject: [PATCH 125/131] TraversalDetails doc. --- Source/Scene/QuadtreePrimitive.js | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 165fc0a57ec..ed7901ffab4 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -593,9 +593,43 @@ define([ queue.push(tile); } + /** + * Tracks details of traversing a tile while selecting tiles for rendering. + * @alias TraversalDetails + * @constructor + * @private + */ function TraversalDetails() { + /** + * True if all selected (i.e. not culled or refined) tiles in this tile's subtree + * are renderable. If the subtree is renderable, we'll render it; no drama. + */ this.allAreRenderable = true; + + /** + * True if any tiles in this tile's subtree were rendered last frame. If any + * were, we must render the subtree rather than this tile, because rendering + * this tile would cause detail to vanish that was visible last frame, and + * that's no good. + */ this.anyWereRenderedLastFrame = false; + + /** + * Counts the number of selected tiles in this tile's subtree that are + * not yet ready to be rendered because they need more loading. Note that + * this value will _not_ necessarily be zero when + * {@link TraversalDetails#allAreRenderable} is true, for subtle reasons. + * When {@link TraversalDetails#allAreRenderable} and + * {@link TraversalDetails#anyWereRenderedLastFrame} are both false, we + * will render this tile instead of any tiles in its subtree and + * the `allAreRenderable` value for this tile will reflect only whether _this_ + * tile is renderable. The `notYetRenderableCount` value, however, will still + * reflect the total number of tiles that we are waiting on, including the + * ones that we're not rendering. `notYetRenderableCount` is only reset + * when a subtree is removed from the render queue because the + * `notYetRenderableCount` exceeds the + * {@link QuadtreePrimitive#loadingDescendantLimit}. + */ this.notYetRenderableCount = 0; } From 15e628ed8977a31cfbd13643987a167d437f8781 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 6 Feb 2019 15:16:19 +1100 Subject: [PATCH 126/131] Add missing @default and @type docs. --- Source/Scene/Globe.js | 4 ++++ Source/Scene/GlobeSurfaceTileProvider.js | 2 ++ Source/Scene/QuadtreePrimitive.js | 2 ++ 3 files changed, 8 insertions(+) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 1c2c9d03b57..9380dbeac86 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -156,6 +156,8 @@ define([ * Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even * if they are culled. Setting this to true may provide a better panning experience at the * cost of loading more tiles. + * @type {Boolean} + * @default false */ this.preloadSiblings = false; @@ -164,6 +166,8 @@ define([ * highlighted at all. The alpha value is used to alpha blend with the tile's * actual color. Because terrain fill tiles do not represent the actual terrain surface, * it may be useful in some applications to indicate visually that they are not to be trusted. + * @type {Color} + * @default undefined */ this.fillHighlightColor = undefined; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 3a12468bdf3..2f599e2b4ad 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -147,6 +147,8 @@ define([ * highlighted at all. The alpha value is used to alpha blend with the tile's * actual color. Because terrain fill tiles do not represent the actual terrain surface, * it may be useful in some applications to indicate visually that they are not to be trusted. + * @type {Color} + * @default undefined */ this.fillHighlightColor = undefined; diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index ed7901ffab4..5a67a80d781 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -169,6 +169,8 @@ define([ * Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even * if they are culled. Setting this to true may provide a better panning experience at the * cost of loading more tiles. + * @type {Boolean} + * @default false */ this.preloadSiblings = false; From 3a9b9ca942254018962829970fcb601a3b399657 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 6 Feb 2019 15:25:55 +1100 Subject: [PATCH 127/131] Don't use an array to track needed positions. --- Source/Scene/QuadtreePrimitive.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index 5a67a80d781..c5103a2321c 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -117,11 +117,12 @@ define([ this._lastTileIndex = 0; this._updateHeightsTimeSlice = 2.0; - // If a culled tile contains a cartographic positions in this list, it will be marked + // If a culled tile contains _cameraPositionCartographic or _cameraReferenceFrameOriginCartographic, it will be marked // TileSelectionResult.CULLED_BUT_NEEDED and added to the list of tiles to update heights, - // even though it is not rendered. The first position will be the position of the - // camera and the second will be the origin of the camera's reference frame. - this._neededPositions = [undefined, undefined]; + // even though it is not rendered. + // These are updated each frame in `selectTilesForRendering`. + this._cameraPositionCartographic = undefined; + this._cameraReferenceFrameOriginCartographic = undefined; /** * Gets or sets the maximum screen-space error, in pixels, that is allowed. @@ -565,9 +566,10 @@ define([ } var camera = frameState.camera; - primitive._neededPositions[0] = camera.positionCartographic; + + primitive._cameraPositionCartographic = camera.positionCartographic; var cameraFrameOrigin = Matrix4.getTranslation(camera.transform, cameraOriginScratch); - primitive._neededPositions[1] = primitive.tileProvider.tilingScheme.ellipsoid.cartesianToCartographic(cameraFrameOrigin, primitive._neededPositions[1]); + primitive._cameraReferenceFrameOriginCartographic = primitive.tileProvider.tilingScheme.ellipsoid.cartesianToCartographic(cameraFrameOrigin, primitive._cameraReferenceFrameOriginCartographic); // Traverse in depth-first, near-to-far order. for (i = 0, len = levelZeroTiles.length; i < len; ++i) { @@ -934,17 +936,9 @@ define([ } function containsNeededPosition(primitive, tile) { - var needed = primitive._neededPositions; var rectangle = tile.rectangle; - - for (var i = 0, len = needed.length; i < len; ++i) { - var position = needed[i]; - if (defined(position) && Rectangle.contains(rectangle, position)) { - return true; - } - } - - return false; + return (defined(primitive._cameraPositionCartographic) && Rectangle.contains(rectangle, primitive._cameraPositionCartographic)) || + (defined(primitive._cameraReferenceFrameOriginCartographic) && Rectangle.contains(rectangle, primitive._cameraReferenceFrameOriginCartographic)); } function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, ancestorMeetsSse, traversalDetails) { From 9ac2e0b3ffee96825ae0f06ef6ac87bd28cc009f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 6 Feb 2019 15:30:48 +1100 Subject: [PATCH 128/131] Remove height range clamping. --- Source/Scene/Globe.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 9380dbeac86..c2cbbc67e28 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -687,14 +687,7 @@ define([ return undefined; } - var height = ellipsoid.cartesianToCartographic(intersection, scratchGetHeightCartographic).height; - - // For low-detail tiles, large triangles often cut the the globe and appear to be at a much - // lower height than actually makes any sense. So clamp the height to the actual height range - // of the tile. - height = Math.max(height, tile.data.tileBoundingRegion.minimumHeight); - height = Math.min(height, tile.data.tileBoundingRegion.maximumHeight); - return height; + return ellipsoid.cartesianToCartographic(intersection, scratchGetHeightCartographic).height; }; /** From b774bd56dd53bb8a02b566279a4229536b2b8b49 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 7 Feb 2019 11:50:05 +1100 Subject: [PATCH 129/131] Possibly fix water mask destroyed crash. --- Source/Scene/TerrainFillMesh.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 83aca7a078e..e047b256899 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -704,6 +704,7 @@ define([ surfaceTile.processImagery(tile, tileProvider.terrainProvider, frameState, true); var oldTexture = fill.waterMaskTexture; + fill.waterMaskTexture = undefined; if (tileProvider.terrainProvider.hasWaterMask) { var waterSourceTile = surfaceTile._findAncestorTileWithTerrainData(tile); From 2f907d4fbc3c7a62878598316066e0d1292ccf0b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 8 Feb 2019 18:07:48 +1100 Subject: [PATCH 130/131] Make Google terrain include edge indices. --- .../Core/GoogleEarthEnterpriseTerrainData.js | 6 ++++- ...VerticesFromGoogleEarthEnterpriseBuffer.js | 23 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Source/Core/GoogleEarthEnterpriseTerrainData.js b/Source/Core/GoogleEarthEnterpriseTerrainData.js index 8fd4ce2ea11..08ce5604a26 100644 --- a/Source/Core/GoogleEarthEnterpriseTerrainData.js +++ b/Source/Core/GoogleEarthEnterpriseTerrainData.js @@ -204,7 +204,11 @@ define([ result.numberOfAttributes, result.orientedBoundingBox, TerrainEncoding.clone(result.encoding), - exaggeration); + exaggeration, + result.westIndicesSouthToNorth, + result.southIndicesEastToWest, + result.eastIndicesNorthToSouth, + result.northIndicesWestToEast); that._vertexCountWithoutSkirts = result.vertexCountWithoutSkirts; that._skirtIndex = result.skirtIndex; diff --git a/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js b/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js index cbde530e722..b86ed3fbd43 100644 --- a/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js +++ b/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js @@ -79,7 +79,11 @@ define([ occludeePointInScaledSpace : statistics.occludeePointInScaledSpace, encoding : statistics.encoding, vertexCountWithoutSkirts : statistics.vertexCountWithoutSkirts, - skirtIndex : statistics.skirtIndex + skirtIndex : statistics.skirtIndex, + westIndicesSouthToNorth : statistics.westIndicesSouthToNorth, + southIndicesEastToWest : statistics.southIndicesEastToWest, + eastIndicesNorthToSouth : statistics.eastIndicesNorthToSouth, + northIndicesWestToEast : statistics.northIndicesWestToEast }; } @@ -398,6 +402,17 @@ define([ bufferIndex = encoding.encode(vertices, bufferIndex, positions[k], uvs[k], heights[k], undefined, webMercatorTs[k]); } + var westIndicesSouthToNorth = westBorder.map(function(vertex) { return vertex.index; }).reverse(); + var southIndicesEastToWest = southBorder.map(function(vertex) { return vertex.index; }).reverse(); + var eastIndicesNorthToSouth = eastBorder.map(function(vertex) { return vertex.index; }).reverse(); + var northIndicesWestToEast = northBorder.map(function(vertex) { return vertex.index; }).reverse(); + + southIndicesEastToWest.unshift(eastIndicesNorthToSouth[eastIndicesNorthToSouth.length - 1]); + southIndicesEastToWest.push(westIndicesSouthToNorth[0]); + + northIndicesWestToEast.unshift(westIndicesSouthToNorth[westIndicesSouthToNorth.length - 1]); + northIndicesWestToEast.push(eastIndicesNorthToSouth[0]); + return { vertices : vertices, indices : new Uint16Array(indices), @@ -408,7 +423,11 @@ define([ orientedBoundingBox : orientedBoundingBox, occludeePointInScaledSpace : occludeePointInScaledSpace, vertexCountWithoutSkirts : vertexCountWithoutSkirts, - skirtIndex : skirtIndex + skirtIndex : skirtIndex, + westIndicesSouthToNorth : westIndicesSouthToNorth, + southIndicesEastToWest : southIndicesEastToWest, + eastIndicesNorthToSouth : eastIndicesNorthToSouth, + northIndicesWestToEast : northIndicesWestToEast }; } From 2bc0095cd9c84f380b8afdcfe57dca9de712010b Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Sat, 9 Feb 2019 22:01:50 +1100 Subject: [PATCH 131/131] Don't keep adding CULLED_BUT_NEEDED tiles to the load queue. We can stop once terrain is available. --- Source/Scene/QuadtreePrimitive.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index c5103a2321c..d923303b331 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -956,7 +956,10 @@ define([ if (containsNeededPosition(primitive, tile)) { // Load the tile(s) that contains the camera's position and // the origin of its reference frame with medium priority. - queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + // But we only need to load until the terrain is available, no need to load imagery. + if (!defined(tile.data) || !defined(tile.data.vertexArray)) { + queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState); + } var lastFrame = primitive._lastSelectionFrameNumber; var lastFrameSelectionResult = tile._lastSelectionResultFrame === lastFrame ? tile._lastSelectionResult : TileSelectionResult.NONE;