diff --git a/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html new file mode 100644 index 000000000000..fa158754e4af --- /dev/null +++ b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html @@ -0,0 +1,227 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.jpg b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.jpg new file mode 100644 index 000000000000..22d9d9be59b9 Binary files /dev/null and b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.jpg differ diff --git a/Apps/Sandcastle/gallery/3D Tiles Point Cloud Styling.html b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Styling.html new file mode 100644 index 000000000000..f4c43d24b4f5 --- /dev/null +++ b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Styling.html @@ -0,0 +1,231 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/3D Tiles Point Cloud Styling.jpg b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Styling.jpg new file mode 100644 index 000000000000..11512b80b98d Binary files /dev/null and b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Styling.jpg differ diff --git a/Apps/Sandcastle/gallery/3D Tiles.html b/Apps/Sandcastle/gallery/3D Tiles.html new file mode 100644 index 000000000000..39427cc36075 --- /dev/null +++ b/Apps/Sandcastle/gallery/3D Tiles.html @@ -0,0 +1,284 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ +
Shadows
+
Right click action:
+
Zoom to feature
+
Annotate
+
Middle click action:
+
Hide feature
+
+ + + diff --git a/Apps/Sandcastle/gallery/3D Tiles.jpg b/Apps/Sandcastle/gallery/3D Tiles.jpg new file mode 100644 index 000000000000..7d99bc035e2e Binary files /dev/null and b/Apps/Sandcastle/gallery/3D Tiles.jpg differ diff --git a/Apps/Sandcastle/gallery/development/3D Models Instancing.html b/Apps/Sandcastle/gallery/development/3D Models Instancing.html new file mode 100644 index 000000000000..3bbbd4656925 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/3D Models Instancing.html @@ -0,0 +1,255 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/development/3D Models Instancing.jpg b/Apps/Sandcastle/gallery/development/3D Models Instancing.jpg new file mode 100644 index 000000000000..34305b24dd90 Binary files /dev/null and b/Apps/Sandcastle/gallery/development/3D Models Instancing.jpg differ diff --git a/Apps/Sandcastle/gallery/development/Billboards Instancing.jpg b/Apps/Sandcastle/gallery/development/Billboards Instancing.jpg index a4f82ed55dc1..e01a4fe5cad5 100644 Binary files a/Apps/Sandcastle/gallery/development/Billboards Instancing.jpg and b/Apps/Sandcastle/gallery/development/Billboards Instancing.jpg differ diff --git a/Apps/Sandcastle/gallery/development/Shadows.html b/Apps/Sandcastle/gallery/development/Shadows.html index b5df78fc31c6..f5ae826cc56b 100644 --- a/Apps/Sandcastle/gallery/development/Shadows.html +++ b/Apps/Sandcastle/gallery/development/Shadows.html @@ -219,7 +219,7 @@ size : 1024, modelOptions : ['Wood Tower', 'Cesium Air', 'Cesium Man', 'Transparent Box', 'Shadow Tester', 'Shadow Tester 2', 'Shadow Tester 3', 'Shadow Tester 4', 'Shadow Tester Point'], model : 'Shadow Tester', - locationOptions : ['Exton', 'Everest', 'Pinnacle PA', 'Seneca Rocks', 'Half Dome'], + locationOptions : ['Exton', 'Everest', 'Pinnacle PA', 'Seneca Rocks', 'Half Dome', '3D Tiles'], location : 'Pinnacle PA', modelPositionOptions : ['Center', 'Ground', 'High', 'Higher', 'Space'], modelPosition : 'Center', @@ -302,6 +302,11 @@ 'Half Dome' : { 'centerLongitude' : -2.0862479628, 'centerLatitude' : 0.6587902522 + }, + '3D Tiles' : { + 'centerLongitude' : -1.31968, + 'centerLatitude' : 0.698874, + 'tileset' : '../../../Specs/Data/Cesium3DTiles/Tilesets/Tileset' } } }; @@ -569,6 +574,10 @@ createBoxRTC(position2); createSphere(position1); + if (Cesium.defined(location.tileset)) { + createTileset(location.tileset); + } + // Add a grid of models if (viewModel.grid) { var spacing = 0.00002; @@ -584,6 +593,12 @@ } } +function createTileset(url) { + scene.primitives.add(new Cesium.Cesium3DTileset({ + url : url + })); +} + function createModel(url, origin) { var modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(origin, new Cesium.HeadingPitchRoll()); diff --git a/CHANGES.md b/CHANGES.md index 3c77d74ee45a..8c47153664a6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,14 @@ Change Log * Added Sandcastle demo for ArcticDEM data. [#5224](https://github.com/AnalyticalGraphicsInc/cesium/issues/5224) * `CzmlDataSource` and `KmlDataSource` load functions now take an optional `query` object, which will append query parameters to all network requests. [#5419](https://github.com/AnalyticalGraphicsInc/cesium/pull/5419), [#5434](https://github.com/AnalyticalGraphicsInc/cesium/pull/5434) * Fixed geocoder bug so geocoder can accurately handle NSEW inputs [#5407] (https://github.com/AnalyticalGraphicsInc/cesium/pull/5407) +* Added support for [3D Tiles](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md) for streaming massive heterogeneous 3D geospatial datasets. The new Cesium APIs are: + * `Cesium3DTileset` + * `Cesium3DTileStyle`, `StyleExpression`, `Expression`, and `ConditionsExpression` + * `Cesium3DTile` + * `Cesium3DTileContent` + * `Cesium3DTileFeature` + * `Cesium3DTilesInspector`, `Cesium3DTilesInspectorViewModel`, and `viewerCesium3DTilesInspectorMixin` + * `Cesium3DTileColorBlendMode` * Added a Sandcastle demo for setting time with the Clock API [#5457](https://github.com/AnalyticalGraphicsInc/cesium/pull/5457); ### 1.34 - 2017-06-01 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9129e1bdf61d..0c983da4d75e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -79,6 +79,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu * [Guillaume Beraudo](https://github.com/gberaudo) * [EndPoint](https://www.endpoint.com/) * [Dmitry Kiselev](https://github.com/kiselev-dv) +* [Safe Software](https://www.safe.com) + * [Joel Depooter](https://github.com/JDepooter) * [Bentley Systems, Inc.](https://www.bentley.com) * [Paul Connelly](https://github.com/pmconne) * [Flightradar24 AB](https://www.flightradar24.com) diff --git a/LICENSE.md b/LICENSE.md index ea6ec67d48f5..3f9c51b5058f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -202,7 +202,9 @@ Copyright 2011-2017 Cesium Contributors See the License for the specific language governing permissions and limitations under the License. -Columbus View (Pat. Pend.) +Patent 9153063 + +Skipping Heuristics and Fusing (Pat. Pend.) Third-Party Code ================ @@ -448,6 +450,31 @@ https://github.com/richtr/NoSleep.js > Rich Tibbett > MIT license +### jsep + +https://github.com/soney/jsep + +> Copyright (c) 2013 Stephen Oney, http://jsep.from.so/ +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +> LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +> OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +> WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ### earcut https://github.com/mapbox/earcut diff --git a/Source/Core/DoublyLinkedList.js b/Source/Core/DoublyLinkedList.js new file mode 100644 index 000000000000..969222203a8b --- /dev/null +++ b/Source/Core/DoublyLinkedList.js @@ -0,0 +1,105 @@ +/*global define*/ +define([ + '../Core/defined', + '../Core/defineProperties' + ], function( + defined, + defineProperties) { + 'use strict'; + + /** + * @private + */ + function DoublyLinkedList() { + this.head = undefined; + this.tail = undefined; + this._length = 0; + } + + defineProperties(DoublyLinkedList.prototype, { + length : { + get : function() { + return this._length; + } + } + }); + + function DoublyLinkedListNode(item, previous, next) { + this.item = item; + this.previous = previous; + this.next = next; + } + + DoublyLinkedList.prototype.add = function(item) { + var node = new DoublyLinkedListNode(item, this.tail, undefined); + + if (defined(this.tail)) { + this.tail.next = node; + this.tail = node; + } else { + // Insert into empty linked list + this.head = node; + this.tail = node; + } + + ++this._length; + + return node; + }; + + function remove(list, node) { + if (defined(node.previous) && defined(node.next)) { + node.previous.next = node.next; + node.next.previous = node.previous; + } else if (defined(node.previous)) { + // Remove last node + node.previous.next = undefined; + list.tail = node.previous; + } else if (defined(node.next)) { + // Remove first node + node.next.previous = undefined; + list.head = node.next; + } else { + // Remove last node in the linked list + list.head = undefined; + list.tail = undefined; + } + + node.next = undefined; + node.previous = undefined; + } + + DoublyLinkedList.prototype.remove = function(node) { + if (!defined(node)) { + return; + } + + remove(this, node); + + --this._length; + }; + + DoublyLinkedList.prototype.splice = function(node, nextNode) { + if (node === nextNode) { + return; + } + + // Remove nextNode, then insert after node + remove(this, nextNode); + + var oldNodeNext = node.next; + node.next = nextNode; + + // nextNode is the new tail + if (this.tail === node) { + this.tail = nextNode; + } else { + oldNodeNext.previous = nextNode; + } + + nextNode.next = oldNodeNext; + nextNode.previous = node; + }; + + return DoublyLinkedList; +}); diff --git a/Source/Core/ManagedArray.js b/Source/Core/ManagedArray.js new file mode 100644 index 000000000000..ec172bec83f1 --- /dev/null +++ b/Source/Core/ManagedArray.js @@ -0,0 +1,145 @@ +/*global define*/ +define([ + './defaultValue', + './defineProperties', + './Check' + ], function( + defaultValue, + defineProperties, + Check) { + 'use strict'; + + /** + * A wrapper around arrays so that the internal length of the array can be manually managed. + * + * @alias ManagedArray + * @constructor + * @private + * + * @param {Number} [length=0] The initial length of the array. + */ + function ManagedArray(length) { + length = defaultValue(length, 0); + this._array = new Array(length); + this._length = length; + } + + defineProperties(ManagedArray.prototype, { + /** + * Gets or sets the length of the array. + * If the set length is greater than the length of the internal array, the internal array is resized. + * + * @type Number + */ + length : { + get : function() { + return this._length; + }, + set : function(length) { + this._length = length; + if (length > this._array.length) { + this._array.length = length; + } + } + }, + + /** + * Gets the internal array. + * + * @type Array + * @readonly + */ + values : { + get : function() { + return this._array; + } + } + }); + + /** + * Gets the element at an index. + * + * @param {Number} index The index to get. + */ + ManagedArray.prototype.get = function(index) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.lessThan('index', index, this._array.length); + //>>includeEnd('debug'); + + return this._array[index]; + }; + + /** + * Sets the element at an index. Resizes the array if index is greater than the length of the array. + * + * @param {Number} index The index to set. + * @param {*} value The value to set at index. + */ + ManagedArray.prototype.set = function(index, value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('index', index); + //>>includeEnd('debug'); + + if (index >= this.length) { + this.length = index + 1; + } + this._array[index] = value; + }; + + /** + * Push an element into the array. + */ + ManagedArray.prototype.push = function(element) { + var index = this.length++; + this._array[index] = element; + }; + + /** + * Pop an element from the array. + * + * @returns {*} The last element in the array. + */ + ManagedArray.prototype.pop = function() { + return this._array[--this.length]; + }; + + /** + * Resize the internal array if length > _array.length. + * + * @param {Number} length The length. + */ + ManagedArray.prototype.reserve = function(length) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals('length', length, 0); + //>>includeEnd('debug'); + + if (length > this._array.length) { + this._array.length = length; + } + }; + + /** + * Resize the array. + * + * @param {Number} length The length. + */ + ManagedArray.prototype.resize = function(length) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals('length', length, 0); + //>>includeEnd('debug'); + + this.length = length; + }; + + /** + * Trim the internal array to the specified length. Defaults to the current length. + * + * @param {Number} [length] The length. + */ + ManagedArray.prototype.trim = function(length) { + length = defaultValue(length, this.length); + this._array.length = length; + }; + + return ManagedArray; +}); diff --git a/Source/Core/PixelFormat.js b/Source/Core/PixelFormat.js index b7b2288f02f7..80f5bf07d4a6 100644 --- a/Source/Core/PixelFormat.js +++ b/Source/Core/PixelFormat.js @@ -249,7 +249,7 @@ define([ /** * @private */ - compressedTextureSizeInBytes : function(pixelFormat, width, height) { + compressedTextureSizeInBytes : function(pixelFormat, width, height) { switch (pixelFormat) { case PixelFormat.RGB_DXT1: case PixelFormat.RGBA_DXT1: diff --git a/Source/Core/Rectangle.js b/Source/Core/Rectangle.js index 44f1cec294ec..22c425ab209d 100644 --- a/Source/Core/Rectangle.js +++ b/Source/Core/Rectangle.js @@ -343,7 +343,7 @@ define([ }; /** - * Duplicates an Rectangle. + * Duplicates a Rectangle. * * @param {Rectangle} rectangle The rectangle to clone. * @param {Rectangle} [result] The object onto which to store the result, or undefined if a new instance should be created. @@ -426,7 +426,7 @@ define([ }; /** - * Checks an Rectangle's properties and throws if they are not in valid ranges. + * Checks a Rectangle's properties and throws if they are not in valid ranges. * * @param {Rectangle} rectangle The rectangle to validate * diff --git a/Source/Renderer/Pass.js b/Source/Renderer/Pass.js index 636d02a17e58..6691bc45f9f3 100644 --- a/Source/Renderer/Pass.js +++ b/Source/Renderer/Pass.js @@ -21,11 +21,12 @@ define([ ENVIRONMENT : 0, COMPUTE : 1, GLOBE : 2, - GROUND : 3, - OPAQUE : 4, - TRANSLUCENT : 5, - OVERLAY : 6, - NUMBER_OF_PASSES : 7 + CESIUM_3D_TILE : 3, + GROUND : 4, + OPAQUE : 5, + TRANSLUCENT : 6, + OVERLAY : 7, + NUMBER_OF_PASSES : 8 }; return freezeObject(Pass); diff --git a/Source/Scene/AttributeType.js b/Source/Scene/AttributeType.js new file mode 100644 index 000000000000..fb380f843b26 --- /dev/null +++ b/Source/Scene/AttributeType.js @@ -0,0 +1,74 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + 'use strict'; + + /** + * An enum describing the attribute type for glTF and 3D Tiles. + * + * @exports AttributeType + * + * @private + */ + var AttributeType = { + /** + * The attribute is a single component. + * + * @type {String} + * @constant + */ + SCALAR : 'SCALAR', + + /** + * The attribute is a two-component vector. + * + * @type {String} + * @constant + */ + VEC2 : 'VEC2', + + /** + * The attribute is a three-component vector. + * + * @type {String} + * @constant + */ + VEC3 : 'VEC3', + + /** + * The attribute is a four-component vector. + * + * @type {String} + * @constant + */ + VEC4 : 'VEC4', + + /** + * The attribute is a 2x2 matrix. + * + * @type {String} + * @constant + */ + MAT2 : 'MAT2', + + /** + * The attribute is a 3x3 matrix. + * + * @type {String} + * @constant + */ + MAT3 : 'MAT3', + + /** + * The attribute is a 4x4 matrix. + * + * @type {String} + * @constant + */ + MAT4 : 'MAT4' + }; + + return freezeObject(AttributeType); +}); diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js new file mode 100644 index 000000000000..21b279df17a3 --- /dev/null +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -0,0 +1,466 @@ +/*global define*/ +define([ + '../Core/Check', + '../Core/Color', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/deprecationWarning', + '../Core/destroyObject', + '../Core/DeveloperError', + '../Core/FeatureDetection', + '../Core/getAbsoluteUri', + '../Core/getBaseUri', + '../Core/getStringFromTypedArray', + '../Core/RequestType', + '../Core/RuntimeError', + './Cesium3DTileBatchTable', + './Cesium3DTileFeature', + './Cesium3DTileFeatureTable', + './getAttributeOrUniformBySemantic', + './Model' + ], function( + Check, + Color, + defaultValue, + defined, + defineProperties, + deprecationWarning, + destroyObject, + DeveloperError, + FeatureDetection, + getAbsoluteUri, + getBaseUri, + getStringFromTypedArray, + RequestType, + RuntimeError, + Cesium3DTileBatchTable, + Cesium3DTileFeature, + Cesium3DTileFeatureTable, + getAttributeOrUniformBySemantic, + Model) { + 'use strict'; + + // Bail out if the browser doesn't support typed arrays, to prevent the setup function + // from failing, since we won't be able to create a WebGL context anyway. + if (!FeatureDetection.supportsTypedArrays()) { + return {}; + } + + /** + * Represents the contents of a + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Batched3DModel/README.md|Batched 3D Model} + * tile in a {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles} tileset. + *

+ * Implements the {@link Cesium3DTileContent} interface. + *

+ * + * @alias Batched3DModel3DTileContent + * @constructor + * + * @private + */ + function Batched3DModel3DTileContent(tileset, tile, url, arrayBuffer, byteOffset) { + this._tileset = tileset; + this._tile = tile; + this._url = url; + this._model = undefined; + this._batchTable = undefined; + this._features = undefined; + + /** + * @inheritdoc Cesium3DTileContent#featurePropertiesDirty + */ + this.featurePropertiesDirty = false; + + initialize(this, arrayBuffer, byteOffset); + } + + // This can be overridden for testing purposes + Batched3DModel3DTileContent._deprecationWarning = deprecationWarning; + + defineProperties(Batched3DModel3DTileContent.prototype, { + /** + * @inheritdoc Cesium3DTileContent#featuresLength + */ + featuresLength : { + get : function() { + return this._batchTable.featuresLength; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#pointsLength + */ + pointsLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#trianglesLength + */ + trianglesLength : { + get : function() { + return this._model.trianglesLength; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#geometryByteLength + */ + geometryByteLength : { + get : function() { + return this._model.geometryByteLength; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#texturesByteLength + */ + texturesByteLength : { + get : function() { + return this._model.texturesByteLength; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTableByteLength + */ + batchTableByteLength : { + get : function() { + return this._batchTable.memorySizeInBytes; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#innerContents + */ + innerContents : { + get : function() { + return undefined; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#readyPromise + */ + readyPromise : { + get : function() { + return this._model.readyPromise; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tileset + */ + tileset : { + get : function() { + return this._tileset; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tile + */ + tile : { + get : function() { + return this._tile; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#url + */ + url: { + get: function() { + return this._url; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTable + */ + batchTable : { + get : function() { + return this._batchTable; + } + } + }); + + var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; + + function getBatchIdAttributeName(gltf) { + var batchIdAttributeName = getAttributeOrUniformBySemantic(gltf, '_BATCHID'); + if (!defined(batchIdAttributeName)) { + batchIdAttributeName = getAttributeOrUniformBySemantic(gltf, 'BATCHID'); + if (defined(batchIdAttributeName)) { + Batched3DModel3DTileContent._deprecationWarning('b3dm-legacy-batchid', 'The glTF in this b3dm uses the semantic `BATCHID`. Application-specific semantics should be prefixed with an underscore: `_BATCHID`.'); + } + } + return batchIdAttributeName; + } + + function getVertexShaderCallback(content) { + return function(vs) { + var batchTable = content._batchTable; + var gltf = content._model.gltf; + var batchIdAttributeName = getBatchIdAttributeName(gltf); + var callback = batchTable.getVertexShaderCallback(true, batchIdAttributeName); + return defined(callback) ? callback(vs) : vs; + }; + } + + function getPickVertexShaderCallback(content) { + return function(vs) { + var batchTable = content._batchTable; + var gltf = content._model.gltf; + var batchIdAttributeName = getBatchIdAttributeName(gltf); + var callback = batchTable.getPickVertexShaderCallback(batchIdAttributeName); + return defined(callback) ? callback(vs) : vs; + }; + } + + function getFragmentShaderCallback(content) { + return function(fs) { + var batchTable = content._batchTable; + var gltf = content._model.gltf; + var diffuseUniformName = getAttributeOrUniformBySemantic(gltf, '_3DTILESDIFFUSE'); + var callback = batchTable.getFragmentShaderCallback(true, diffuseUniformName); + return defined(callback) ? callback(fs) : fs; + }; + } + + function initialize(content, arrayBuffer, byteOffset) { + var tileset = content._tileset; + var tile = content._tile; + var basePath = getAbsoluteUri(getBaseUri(content._url, true)); + + var byteStart = defaultValue(byteOffset, 0); + byteOffset = byteStart; + + var uint8Array = new Uint8Array(arrayBuffer); + var view = new DataView(arrayBuffer); + byteOffset += sizeOfUint32; // Skip magic + + var version = view.getUint32(byteOffset, true); + if (version !== 1) { + throw new RuntimeError('Only Batched 3D Model version 1 is supported. Version ' + version + ' is not.'); + } + byteOffset += sizeOfUint32; + + var byteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var featureTableJsonByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var featureTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var batchTableJsonByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var batchTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var batchLength; + + // Legacy header #1: [batchLength] [batchTableByteLength] + // Legacy header #2: [batchTableJsonByteLength] [batchTableBinaryByteLength] [batchLength] + // Current header: [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength] + // If the header is in the first legacy format 'batchTableJsonByteLength' will be the start of the JSON string (a quotation mark) or the glTF magic. + // Accordingly its first byte will be either 0x22 or 0x67, and so the minimum uint32 expected is 0x22000000 = 570425344 = 570MB. It is unlikely that the feature table JSON will exceed this length. + // The check for the second legacy format is similar, except it checks 'batchTableBinaryByteLength' instead + if (batchTableJsonByteLength >= 570425344) { + // First legacy check + byteOffset -= sizeOfUint32 * 2; + batchLength = featureTableJsonByteLength; + batchTableJsonByteLength = featureTableBinaryByteLength; + batchTableBinaryByteLength = 0; + featureTableJsonByteLength = 0; + featureTableBinaryByteLength = 0; + deprecationWarning('b3dm-legacy-header', 'This b3dm header is using the legacy format [batchLength] [batchTableByteLength]. The new format is [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength] from https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Batched3DModel/README.md.'); + } else if (batchTableBinaryByteLength >= 570425344) { + // Second legacy check + byteOffset -= sizeOfUint32; + batchLength = batchTableJsonByteLength; + batchTableJsonByteLength = featureTableJsonByteLength; + batchTableBinaryByteLength = featureTableBinaryByteLength; + featureTableJsonByteLength = 0; + featureTableBinaryByteLength = 0; + deprecationWarning('b3dm-legacy-header', 'This b3dm header is using the legacy format [batchTableJsonByteLength] [batchTableBinaryByteLength] [batchLength]. The new format is [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength] from https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Batched3DModel/README.md.'); + } + + var featureTableJson; + if (featureTableJsonByteLength === 0) { + featureTableJson = { + BATCH_LENGTH : defaultValue(batchLength, 0) + }; + } else { + var featureTableString = getStringFromTypedArray(uint8Array, byteOffset, featureTableJsonByteLength); + featureTableJson = JSON.parse(featureTableString); + byteOffset += featureTableJsonByteLength; + } + + var featureTableBinary = new Uint8Array(arrayBuffer, byteOffset, featureTableBinaryByteLength); + byteOffset += featureTableBinaryByteLength; + + var featureTable = new Cesium3DTileFeatureTable(featureTableJson, featureTableBinary); + + batchLength = featureTable.getGlobalProperty('BATCH_LENGTH'); + featureTable.featuresLength = batchLength; + + var batchTableJson; + var batchTableBinary; + if (batchTableJsonByteLength > 0) { + // PERFORMANCE_IDEA: is it possible to allocate this on-demand? Perhaps keep the + // arraybuffer/string compressed in memory and then decompress it when it is first accessed. + // + // We could also make another request for it, but that would make the property set/get + // API async, and would double the number of numbers in some cases. + var batchTableString = getStringFromTypedArray(uint8Array, byteOffset, batchTableJsonByteLength); + batchTableJson = JSON.parse(batchTableString); + byteOffset += batchTableJsonByteLength; + + if (batchTableBinaryByteLength > 0) { + // Has a batch table binary + batchTableBinary = new Uint8Array(arrayBuffer, byteOffset, batchTableBinaryByteLength); + // Copy the batchTableBinary section and let the underlying ArrayBuffer be freed + batchTableBinary = new Uint8Array(batchTableBinary); + byteOffset += batchTableBinaryByteLength; + } + } + + var batchTable = new Cesium3DTileBatchTable(content, batchLength, batchTableJson, batchTableBinary); + content._batchTable = batchTable; + + var gltfByteLength = byteStart + byteLength - byteOffset; + if (gltfByteLength === 0) { + throw new RuntimeError('glTF byte length must be greater than 0.'); + } + var gltfView = new Uint8Array(arrayBuffer, byteOffset, gltfByteLength); + + var pickObject = { + content : content, + primitive : tileset + }; + + // PERFORMANCE_IDEA: patch the shader on demand, e.g., the first time show/color changes. + // The pick shader still needs to be patched. + content._model = new Model({ + gltf : gltfView, + cull : false, // The model is already culled by 3D Tiles + releaseGltfJson : true, // Models are unique and will not benefit from caching so save memory + basePath : basePath, + requestType : RequestType.TILES3D, + modelMatrix : tile.computedTransform, + upAxis : tileset._gltfUpAxis, + shadows: tileset.shadows, + debugWireframe: tileset.debugWireframe, + incrementallyLoadTextures : false, + vertexShaderLoaded : getVertexShaderCallback(content), + fragmentShaderLoaded : getFragmentShaderCallback(content), + uniformMapLoaded : batchTable.getUniformMapCallback(), + pickVertexShaderLoaded : getPickVertexShaderCallback(content), + pickFragmentShaderLoaded : batchTable.getPickFragmentShaderCallback(), + pickUniformMapLoaded : batchTable.getPickUniformMapCallback(), + addBatchIdToGeneratedShaders : (batchLength > 0), // If the batch table has values in it, generated shaders will need a batchId attribute + pickObject : pickObject + }); + } + + function createFeatures(content) { + var tileset = content._tileset; + var featuresLength = content.featuresLength; + if (!defined(content._features) && (featuresLength > 0)) { + var features = new Array(featuresLength); + for (var i = 0; i < featuresLength; ++i) { + features[i] = new Cesium3DTileFeature(tileset, content, i); + } + content._features = features; + } + } + + /** + * @inheritdoc Cesium3DTileContent#hasProperty + */ + Batched3DModel3DTileContent.prototype.hasProperty = function(batchId, name) { + return this._batchTable.hasProperty(batchId, name); + }; + + /** + * @inheritdoc Cesium3DTileContent#getFeature + */ + Batched3DModel3DTileContent.prototype.getFeature = function(batchId) { + //>>includeStart('debug', pragmas.debug); + var featuresLength = this.featuresLength; + if (!defined(batchId) || (batchId < 0) || (batchId >= featuresLength)) { + throw new DeveloperError('batchId is required and between zero and featuresLength - 1 (' + (featuresLength - 1) + ').'); + } + //>>includeEnd('debug'); + + createFeatures(this); + return this._features[batchId]; + }; + + /** + * @inheritdoc Cesium3DTileContent#applyDebugSettings + */ + Batched3DModel3DTileContent.prototype.applyDebugSettings = function(enabled, color) { + color = enabled ? color : Color.WHITE; + if (this.featuresLength === 0) { + this._model.color = color; + } else { + this._batchTable.setAllColor(color); + } + }; + + /** + * @inheritdoc Cesium3DTileContent#applyStyle + */ + Batched3DModel3DTileContent.prototype.applyStyle = function(frameState, style) { + this._batchTable.applyStyle(frameState, style); + }; + + /** + * @inheritdoc Cesium3DTileContent#update + */ + Batched3DModel3DTileContent.prototype.update = function(tileset, frameState) { + var commandStart = frameState.commandList.length; + + // In the PROCESSING state we may be calling update() to move forward + // the content's resource loading. In the READY state, it will + // actually generate commands. + this._batchTable.update(tileset, frameState); + this._model.modelMatrix = this._tile.computedTransform; + this._model.shadows = this._tileset.shadows; + this._model.debugWireframe = this._tileset.debugWireframe; + this._model.update(frameState); + + // If any commands were pushed, add derived commands + var commandEnd = frameState.commandList.length; + if ((commandStart < commandEnd) && frameState.passes.render) { + this._batchTable.addDerivedCommands(frameState, commandStart); + } + }; + + /** + * @inheritdoc Cesium3DTileContent#isDestroyed + */ + Batched3DModel3DTileContent.prototype.isDestroyed = function() { + return false; + }; + + /** + * @inheritdoc Cesium3DTileContent#destroy + */ + Batched3DModel3DTileContent.prototype.destroy = function() { + this._model = this._model && this._model.destroy(); + this._batchTable = this._batchTable && this._batchTable.destroy(); + return destroyObject(this); + }; + + return Batched3DModel3DTileContent; +}); diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js new file mode 100644 index 000000000000..4ab91851d49c --- /dev/null +++ b/Source/Scene/Cesium3DTile.js @@ -0,0 +1,1054 @@ +/*global define*/ +define([ + '../Core/BoundingSphere', + '../Core/Cartesian3', + '../Core/Color', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/getMagic', + '../Core/Intersect', + '../Core/joinUrls', + '../Core/JulianDate', + '../Core/loadArrayBuffer', + '../Core/Matrix3', + '../Core/Matrix4', + '../Core/Rectangle', + '../Core/Request', + '../Core/RequestScheduler', + '../Core/RequestState', + '../Core/RequestType', + '../Core/RuntimeError', + '../ThirdParty/when', + './Cesium3DTileChildrenVisibility', + './Cesium3DTileContentFactory', + './Cesium3DTileContentState', + './Cesium3DTileOptimizationHint', + './Cesium3DTileRefine', + './Empty3DTileContent', + './SceneMode', + './TileBoundingRegion', + './TileBoundingSphere', + './TileOrientedBoundingBox' + ], function( + BoundingSphere, + Cartesian3, + Color, + defaultValue, + defined, + defineProperties, + destroyObject, + getMagic, + Intersect, + joinUrls, + JulianDate, + loadArrayBuffer, + Matrix3, + Matrix4, + Rectangle, + Request, + RequestScheduler, + RequestState, + RequestType, + RuntimeError, + when, + Cesium3DTileChildrenVisibility, + Cesium3DTileContentFactory, + Cesium3DTileContentState, + Cesium3DTileOptimizationHint, + Cesium3DTileRefine, + Empty3DTileContent, + SceneMode, + TileBoundingRegion, + TileBoundingSphere, + TileOrientedBoundingBox) { + 'use strict'; + + /** + * A tile in a {@link Cesium3DTileset}. When a tile is first created, its content is not loaded; + * the content is loaded on-demand when needed based on the view. + *

+ * Do not construct this directly, instead access tiles through {@link Cesium3DTileset#tileVisible}. + *

+ * + * @alias Cesium3DTile + * @constructor + */ + function Cesium3DTile(tileset, basePath, header, parent) { + this._tileset = tileset; + this._header = header; + var contentHeader = header.content; + + /** + * The local transform of this tile + * @type {Matrix4} + */ + this.transform = defined(header.transform) ? Matrix4.unpack(header.transform) : Matrix4.clone(Matrix4.IDENTITY); + + var parentTransform = defined(parent) ? parent.computedTransform : tileset.modelMatrix; + var computedTransform = Matrix4.multiply(parentTransform, this.transform, new Matrix4()); + + /** + * The final computed transform of this tile + * @type {Matrix4} + * @readonly + */ + this.computedTransform = computedTransform; + + this._boundingVolume = this.createBoundingVolume(header.boundingVolume, computedTransform); + this._boundingVolume2D = undefined; + + var contentBoundingVolume; + + if (defined(contentHeader) && defined(contentHeader.boundingVolume)) { + // Non-leaf tiles may have a content bounding-volume, which is a tight-fit bounding volume + // around only the features in the tile. This box is useful for culling for rendering, + // but not for culling for traversing the tree since it does not guarantee spatial coherence, i.e., + // since it only bounds features in the tile, not the entire tile, children may be + // outside of this box. + contentBoundingVolume = this.createBoundingVolume(contentHeader.boundingVolume, computedTransform); + } + this._contentBoundingVolume = contentBoundingVolume; + this._contentBoundingVolume2D = undefined; + + var viewerRequestVolume; + if (defined(header.viewerRequestVolume)) { + viewerRequestVolume = this.createBoundingVolume(header.viewerRequestVolume, computedTransform); + } + this._viewerRequestVolume = viewerRequestVolume; + + /** + * The error, in meters, introduced if this tile is rendered and its children are not. + * This is used to compute screen space error, i.e., the error measured in pixels. + * + * @type {Number} + * @readonly + */ + this.geometricError = header.geometricError; + + if (!defined(this.geometricError)) { + throw new RuntimeError('geometricError must be defined'); + } + + var refine; + if (defined(header.refine)) { + refine = (header.refine === 'replace') ? Cesium3DTileRefine.REPLACE : Cesium3DTileRefine.ADD; + } else if (defined(parent)) { + // Inherit from parent tile if omitted. + refine = parent.refine; + } else { + refine = Cesium3DTileRefine.REPLACE; + } + + /** + * Specifies the type of refinment that is used when traversing this tile for rendering. + * + * @type {Cesium3DTileRefine} + * @readonly + * @private + */ + this.refine = refine; + + /** + * Gets the tile's children. + * + * @type {Cesium3DTile[]} + * @readonly + */ + this.children = []; + + /** + * This tile's parent or undefined if this tile is the root. + *

+ * When a tile's content points to an external tileset.json, the external tileset's + * root tile's parent is not undefined; instead, the parent references + * the tile (with its content pointing to an external tileset.json) as if the two tilesets were merged. + *

+ * + * @type {Cesium3DTile} + * @readonly + */ + this.parent = parent; + + var content; + var hasEmptyContent; + var contentState; + var contentUrl; + var serverKey; + + if (defined(contentHeader)) { + hasEmptyContent = false; + contentState = Cesium3DTileContentState.UNLOADED; + contentUrl = joinUrls(basePath, contentHeader.url); + serverKey = RequestScheduler.getServerKey(contentUrl); + } else { + content = new Empty3DTileContent(tileset, this); + hasEmptyContent = true; + contentState = Cesium3DTileContentState.READY; + } + + this._content = content; + this._contentUrl = contentUrl; + this._contentState = contentState; + this._contentReadyToProcessPromise = undefined; + this._contentReadyPromise = undefined; + this._expiredContent = undefined; + + this._serverKey = serverKey; + + /** + * When true, the tile has no content. + * + * @type {Boolean} + * @readonly + * + * @private + */ + this.hasEmptyContent = hasEmptyContent; + + /** + * When true, the tile's content is renderable. + *

+ * This is false until the tile's content is loaded. + *

+ * + * @type {Boolean} + * @readonly + * + * @private + */ + this.hasRenderableContent = false; + + /** + * When true, the tile's content points to an external tileset. + *

+ * This is false until the tile's content is loaded. + *

+ * + * @type {Boolean} + * @readonly + * + * @private + */ + this.hasTilesetContent = false; + + /** + * The corresponding node in the cache replacement list. + * + * @type {DoublyLinkedListNode} + * @readonly + * + * @private + */ + this.replacementNode = undefined; + + var expire = header.expire; + var expireDuration; + var expireDate; + if (defined(expire)) { + expireDuration = expire.duration; + if (defined(expire.date)) { + expireDate = JulianDate.fromIso8601(expire.date); + } + } + + /** + * The time in seconds after the tile's content is ready when the content expires and new content is requested. + * + * @type {Number} + */ + this.expireDuration = expireDuration; + + /** + * The date when the content expires and new content is requested. + * + * @type {JulianDate} + */ + this.expireDate = expireDate; + + /** + * Marks if the tile is selected this frame. + * + * @type {Boolean} + * + * @private + */ + this.selected = false; + + /** + * The time when a style was last applied to this tile. + * + * @type {Number} + * + * @private + */ + this.lastStyleTime = 0; + + /** + * Marks whether the tile's children bounds are fully contained within the tile's bounds + * + * @type {Cesium3DTileOptimizationHint} + * + * @private + */ + this._optimChildrenWithinParent = Cesium3DTileOptimizationHint.NOT_COMPUTED; + + // Members that are updated every frame for tree traversal and rendering optimizations: + this._distanceToCamera = 0; + this._visibilityPlaneMask = 0; + this._childrenVisibility = Cesium3DTileChildrenVisibility.VISIBLE; + this._lastSelectedFrameNumber = -1; + this._screenSpaceError = 0; + this._screenSpaceErrorComputedFrame = -1; + this._finalResolution = true; + this._depth = 0; + this._centerZDepth = 0; + this._stackLength = 0; + this._selectedFrame = -1; + this._selectionDepth = 0; + this._lastSelectionDepth = undefined; + this._requestedFrame = undefined; + this._lastVisitedFrame = undefined; + this._ancestorWithContent = undefined; + this._ancestorWithLoadedContent = undefined; + + this._debugBoundingVolume = undefined; + this._debugContentBoundingVolume = undefined; + this._debugViewerRequestVolume = undefined; + this._debugColor = Color.fromRandom({ alpha : 1.0 }); + this._debugColorizeTiles = false; + + this._commandsLength = 0; + + this._color = undefined; + this._colorDirty = false; + } + + defineProperties(Cesium3DTile.prototype, { + /** + * The tileset containing this tile. + * + * @memberof Cesium3DTile.prototype + * + * @type {Cesium3DTileset} + * @readonly + */ + tileset : { + get : function() { + return this._tileset; + } + }, + + /** + * The tile's content. This represents the actual tile's payload, + * not the content's metadata in tileset.json. + * + * @memberof Cesium3DTile.prototype + * + * @type {Cesium3DTileContent} + * @readonly + */ + content : { + get : function() { + return this._content; + } + }, + + /** + * Get the bounding volume of the tile's contents. This defaults to the + * tile's bounding volume when the content's bounding volume is + * undefined. + * + * @memberof Cesium3DTile.prototype + * + * @type {TileBoundingVolume} + * @readonly + * @private + */ + contentBoundingVolume : { + get : function() { + return defaultValue(this._contentBoundingVolume, this._boundingVolume); + } + }, + + /** + * Get the bounding sphere derived from the tile's bounding volume. + * + * @memberof Cesium3DTile.prototype + * + * @type {BoundingSphere} + * @readonly + */ + boundingSphere : { + get : function() { + return this._boundingVolume.boundingSphere; + } + }, + + /** + * Gets or sets the tile's highlight color. + * + * @memberof Cesium3DTile.prototype + * + * @type {Color} + * + * @default {@link Color.WHITE} + * + * @private + */ + color : { + get : function() { + if (!defined(this._color)) { + this._color = new Color(); + } + return Color.clone(this._color); + }, + set : function(value) { + this._color = Color.clone(value, this._color); + this._colorDirty = true; + } + }, + + /** + * Determines if the tile has available content to render. true if the tile's + * content is ready or if it has expired content that renders while new content loads; otherwise, + * false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} + * @readonly + * + * @private + */ + contentAvailable : { + get : function() { + return this.contentReady || (defined(this._expiredContent) && this._contentState !== Cesium3DTileContentState.FAILED); + } + }, + + /** + * Determines if the tile is ready to render. true if the tile + * is ready to render; otherwise, false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} + * @readonly + * + * @private + */ + contentReady : { + get : function() { + return this._contentState === Cesium3DTileContentState.READY; + } + }, + + /** + * Determines if the tile's content has not be requested. true if tile's + * content has not be requested; otherwise, false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} + * @readonly + * + * @private + */ + contentUnloaded : { + get : function() { + return this._contentState === Cesium3DTileContentState.UNLOADED; + } + }, + + /** + * Determines if the tile's content is expired. true if tile's + * content is expired; otherwise, false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} + * @readonly + * + * @private + */ + contentExpired : { + get : function() { + return this._contentState === Cesium3DTileContentState.EXPIRED; + } + }, + + /** + * Gets the promise that will be resolved when the tile's content is ready to process. + * This happens after the content is downloaded but before the content is ready + * to render. + *

+ * The promise remains undefined until the tile's content is requested. + *

+ * + * @type {Promise.} + * @readonly + * + * @private + */ + contentReadyToProcessPromise : { + get : function() { + if (defined(this._contentReadyToProcessPromise)) { + return this._contentReadyToProcessPromise.promise; + } + } + }, + + /** + * Gets the promise that will be resolved when the tile's content is ready to render. + *

+ * The promise remains undefined until the tile's content is requested. + *

+ * + * @type {Promise.} + * @readonly + * + * @private + */ + contentReadyPromise : { + get : function() { + if (defined(this._contentReadyPromise)) { + return this._contentReadyPromise.promise; + } + } + }, + + /** + * Returns the number of draw commands used by this tile. + * + * @readonly + * + * @private + */ + commandsLength : { + get : function() { + return this._commandsLength; + } + } + }); + + var scratchJulianDate = new JulianDate(); + + /** + * Update whether the tile has expired. + * + * @private + */ + Cesium3DTile.prototype.updateExpiration = function() { + if (defined(this.expireDate) && this.contentReady && !this.hasEmptyContent) { + var now = JulianDate.now(scratchJulianDate); + if (JulianDate.lessThan(this.expireDate, now)) { + this._contentState = Cesium3DTileContentState.EXPIRED; + this._expiredContent = this._content; + } + } + }; + + function updateExpireDate(tile) { + if (defined(tile.expireDuration)) { + var expireDurationDate = JulianDate.now(scratchJulianDate); + JulianDate.addSeconds(expireDurationDate, tile.expireDuration, expireDurationDate); + + if (defined(tile.expireDate)) { + if (JulianDate.lessThan(tile.expireDate, expireDurationDate)) { + JulianDate.clone(expireDurationDate, tile.expireDate); + } + } else { + tile.expireDate = JulianDate.clone(expireDurationDate); + } + } + } + + function getContentFailedFunction(tile) { + return function(error) { + tile._contentState = Cesium3DTileContentState.FAILED; + tile._contentReadyPromise.reject(error); + tile._contentReadyToProcessPromise.reject(error); + }; + } + + function createPriorityFunction(tile) { + return function() { + return tile._distanceToCamera; + }; + } + + /** + * Requests the tile's content. + *

+ * The request may not be made if the Cesium Request Scheduler can't prioritize it. + *

+ * + * @private + */ + Cesium3DTile.prototype.requestContent = function() { + var that = this; + var tileset = this._tileset; + + if (this.hasEmptyContent) { + return false; + } + + var url = this._contentUrl; + var expired = this.contentExpired; + if (expired) { + // Append a query parameter of the tile expiration date to prevent caching + var timestampQuery = '?expired=' + this.expireDate.toString(); + url = joinUrls(url, timestampQuery, false); + } + + var request = new Request({ + throttle : true, + throttleByServer : true, + type : RequestType.TILES3D, + priorityFunction : createPriorityFunction(this), + serverKey : this._serverKey + }); + + var promise = loadArrayBuffer(url, undefined, request); + + if (!defined(promise)) { + return false; + } + + var contentState = this._contentState; + this._contentState = Cesium3DTileContentState.LOADING; + this._contentReadyToProcessPromise = when.defer(); + this._contentReadyPromise = when.defer(); + + if (expired) { + this.expireDate = undefined; + } + + var contentFailedFunction = getContentFailedFunction(this); + + promise.then(function(arrayBuffer) { + if (that.isDestroyed()) { + // Tile is unloaded before the content finishes loading + contentFailedFunction(); + return; + } + var uint8Array = new Uint8Array(arrayBuffer); + var magic = getMagic(uint8Array); + var contentFactory = Cesium3DTileContentFactory[magic]; + var content; + + if (defined(contentFactory)) { + content = contentFactory(tileset, that, that._contentUrl, arrayBuffer, 0); + that.hasRenderableContent = true; + } else { + // The content may be json instead + content = Cesium3DTileContentFactory.json(tileset, that, that._contentUrl, arrayBuffer, 0); + that.hasTilesetContent = true; + } + + that._content = content; + that._contentState = Cesium3DTileContentState.PROCESSING; + that._contentReadyToProcessPromise.resolve(content); + + return content.readyPromise.then(function(content) { + if (that.isDestroyed()) { + // Tile is unloaded before the content finishes processing + contentFailedFunction(); + return; + } + updateExpireDate(that); + + // Refresh style for expired content + that.lastStyleTime = 0; + + that._contentState = Cesium3DTileContentState.READY; + that._contentReadyPromise.resolve(content); + }); + }).otherwise(function(error) { + if (request.state === RequestState.CANCELLED) { + // Cancelled due to low priority - try again later. + that._contentState = contentState; + --tileset.statistics.numberOfPendingRequests; + ++tileset.statistics.numberOfAttemptedRequests; + return; + } + contentFailedFunction(error); + }); + + return true; + }; + + /** + * Unloads the tile's content. + * + * @private + */ + Cesium3DTile.prototype.unloadContent = function() { + if (!this.hasRenderableContent) { + return; + } + + this._content = this._content && this._content.destroy(); + this._contentState = Cesium3DTileContentState.UNLOADED; + this._contentReadyToProcessPromise = undefined; + this._contentReadyPromise = undefined; + + this.replacementNode = undefined; + + this.lastStyleTime = 0; + + this._debugColorizeTiles = false; + + this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); + this._debugContentBoundingVolume = this._debugContentBoundingVolume && this._debugContentBoundingVolume.destroy(); + this._debugViewerRequestVolume = this._debugViewerRequestVolume && this._debugViewerRequestVolume.destroy(); + }; + + var scratchProjectedBoundingSphere = new BoundingSphere(); + + function getBoundingVolume(tile, frameState) { + if (frameState.mode !== SceneMode.SCENE3D && !defined(tile._boundingVolume2D)) { + var boundingSphere = tile._boundingVolume.boundingSphere; + var sphere = BoundingSphere.projectTo2D(boundingSphere, frameState.mapProjection, scratchProjectedBoundingSphere); + tile._boundingVolume2D = new TileBoundingSphere(sphere.center, sphere.radius); + } + + return frameState.mode !== SceneMode.SCENE3D ? tile._boundingVolume2D : tile._boundingVolume; + } + + function getContentBoundingVolume(tile, frameState) { + if (frameState.mode !== SceneMode.SCENE3D && !defined(tile._contentBoundingVolume2D)) { + var boundingSphere = tile._contentBoundingVolume.boundingSphere; + var sphere = BoundingSphere.projectTo2D(boundingSphere, frameState.mapProjection, scratchProjectedBoundingSphere); + tile._contentBoundingVolume2D = new TileBoundingSphere(sphere.center, sphere.radius); + } + return frameState.mode !== SceneMode.SCENE3D ? tile._contentBoundingVolume2D : tile._contentBoundingVolume; + } + + /** + * Determines whether the tile's bounding volume intersects the culling volume. + * + * @param {FrameState} frameState The frame state. + * @param {Number} parentVisibilityPlaneMask The parent's plane mask to speed up the visibility check. + * @returns {Number} A plane mask as described above in {@link CullingVolume#computeVisibilityWithPlaneMask}. + * + * @private + */ + Cesium3DTile.prototype.visibility = function(frameState, parentVisibilityPlaneMask) { + var cullingVolume = frameState.cullingVolume; + var boundingVolume = getBoundingVolume(this, frameState); + return cullingVolume.computeVisibilityWithPlaneMask(boundingVolume, parentVisibilityPlaneMask); + }; + + /** + * Assuming the tile's bounding volume intersects the culling volume, determines + * whether the tile's content's bounding volume intersects the culling volume. + * + * @param {FrameState} frameState The frame state. + * @returns {Intersect} The result of the intersection: the tile's content is completely outside, completely inside, or intersecting the culling volume. + * + * @private + */ + Cesium3DTile.prototype.contentVisibility = function(frameState) { + // Assumes the tile's bounding volume intersects the culling volume already, so + // just return Intersect.INSIDE if there is no content bounding volume. + if (!defined(this._contentBoundingVolume)) { + return Intersect.INSIDE; + } + + // PERFORMANCE_IDEA: is it possible to burn less CPU on this test since we know the + // tile's (not the content's) bounding volume intersects the culling volume? + var cullingVolume = frameState.cullingVolume; + var boundingVolume = getContentBoundingVolume(this, frameState); + return cullingVolume.computeVisibility(boundingVolume); + }; + + /** + * Computes the (potentially approximate) distance from the closest point of the tile's bounding volume to the camera. + * + * @param {FrameState} frameState The frame state. + * @returns {Number} The distance, in meters, or zero if the camera is inside the bounding volume. + * + * @private + */ + Cesium3DTile.prototype.distanceToTile = function(frameState) { + var boundingVolume = getBoundingVolume(this, frameState); + return boundingVolume.distanceToCamera(frameState); + }; + + var scratchToTileCenter = new Cartesian3(); + + /** + * Computes the distance from the center of the tile's bounding volume to the camera. + * + * @param {FrameState} frameState The frame state. + * @returns {Number} The distance, in meters, or zero if the camera is inside the bounding volume. + * + * @private + */ + Cesium3DTile.prototype.distanceToTileCenter = function(frameState) { + var tileBoundingVolume = getBoundingVolume(this, frameState); + var boundingVolume = tileBoundingVolume.boundingVolume; // Gets the underlying OrientedBoundingBox or BoundingSphere + var toCenter = Cartesian3.subtract(boundingVolume.center, frameState.camera.positionWC, scratchToTileCenter); + var distance = Cartesian3.magnitude(toCenter); + Cartesian3.divideByScalar(toCenter, distance, toCenter); + var dot = Cartesian3.dot(frameState.camera.directionWC, toCenter); + return distance * dot; + }; + + /** + * Checks if the camera is inside the viewer request volume. + * + * @param {FrameState} frameState The frame state. + * @returns {Boolean} Whether the camera is inside the volume. + * + * @private + */ + Cesium3DTile.prototype.insideViewerRequestVolume = function(frameState) { + var viewerRequestVolume = this._viewerRequestVolume; + return !defined(viewerRequestVolume) || (viewerRequestVolume.distanceToCamera(frameState) === 0.0); + }; + + var scratchMatrix = new Matrix3(); + var scratchScale = new Cartesian3(); + var scratchHalfAxes = new Matrix3(); + var scratchCenter = new Cartesian3(); + var scratchRectangle = new Rectangle(); + + function createBox(box, transform, result) { + var center = Cartesian3.fromElements(box[0], box[1], box[2], scratchCenter); + var halfAxes = Matrix3.fromArray(box, 3, scratchHalfAxes); + + // Find the transformed center and halfAxes + center = Matrix4.multiplyByPoint(transform, center, center); + var rotationScale = Matrix4.getRotation(transform, scratchMatrix); + halfAxes = Matrix3.multiply(rotationScale, halfAxes, halfAxes); + + if (defined(result)) { + result.update(center, halfAxes); + return result; + } + return new TileOrientedBoundingBox(center, halfAxes); + } + + function createRegion(region, result) { + var rectangleRegion = Rectangle.unpack(region, 0, scratchRectangle); + + if (defined(result)) { + // Don't update regions when the transform changes + return result; + } + return new TileBoundingRegion({ + rectangle : rectangleRegion, + minimumHeight : region[4], + maximumHeight : region[5] + }); + } + + function createSphere(sphere, transform, result) { + var center = Cartesian3.fromElements(sphere[0], sphere[1], sphere[2], scratchCenter); + var radius = sphere[3]; + + // Find the transformed center and radius + center = Matrix4.multiplyByPoint(transform, center, center); + var scale = Matrix4.getScale(transform, scratchScale); + var uniformScale = Cartesian3.maximumComponent(scale); + radius *= uniformScale; + + if (defined(result)) { + result.update(center, radius); + return result; + } + return new TileBoundingSphere(center, radius); + } + + /** + * Create a bounding volume from the tile's bounding volume header. + * + * @param {Object} boundingVolumeHeader The tile's bounding volume header. + * @param {Matrix4} transform The transform to apply to the bounding volume. + * @param {TileBoundingVolume} [result] The object onto which to store the result. + * + * @returns {TileBoundingVolume} The modified result parameter or a new TileBoundingVolume instance if none was provided. + * + * @private + */ + Cesium3DTile.prototype.createBoundingVolume = function(boundingVolumeHeader, transform, result) { + if (!defined(boundingVolumeHeader)) { + throw new RuntimeError('boundingVolume must be defined'); + } + if (defined(boundingVolumeHeader.box)) { + return createBox(boundingVolumeHeader.box, transform, result); + } + if (defined(boundingVolumeHeader.region)) { + return createRegion(boundingVolumeHeader.region, result); + } + if (defined(boundingVolumeHeader.sphere)) { + return createSphere(boundingVolumeHeader.sphere, transform, result); + } + throw new RuntimeError('boundingVolume must contain a sphere, region, or box'); + }; + + var scratchTransform = new Matrix4(); + + /** + * Update the tile's transform. The transform is applied to the tile's bounding volumes. + * + * @private + */ + Cesium3DTile.prototype.updateTransform = function(parentTransform) { + parentTransform = defaultValue(parentTransform, Matrix4.IDENTITY); + var computedTransform = Matrix4.multiply(parentTransform, this.transform, scratchTransform); + var transformChanged = !Matrix4.equals(computedTransform, this.computedTransform); + + if (!transformChanged) { + return; + } + + Matrix4.clone(computedTransform, this.computedTransform); + + // Update the bounding volumes + var header = this._header; + var content = this._header.content; + this._boundingVolume = this.createBoundingVolume(header.boundingVolume, computedTransform, this._boundingVolume); + if (defined(this._contentBoundingVolume)) { + this._contentBoundingVolume = this.createBoundingVolume(content.boundingVolume, computedTransform, this._contentBoundingVolume); + } + if (defined(this._viewerRequestVolume)) { + this._viewerRequestVolume = this.createBoundingVolume(header.viewerRequestVolume, computedTransform, this._viewerRequestVolume); + } + + // Destroy the debug bounding volumes. They will be generated fresh. + this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); + this._debugContentBoundingVolume = this._debugContentBoundingVolume && this._debugContentBoundingVolume.destroy(); + this._debugViewerRequestVolume = this._debugViewerRequestVolume && this._debugViewerRequestVolume.destroy(); + }; + + function applyDebugSettings(tile, tileset, frameState) { + var hasContentBoundingVolume = defined(tile._header.content) && defined(tile._header.content.boundingVolume); + + var showVolume = tileset.debugShowBoundingVolume || (tileset.debugShowContentBoundingVolume && !hasContentBoundingVolume); + if (showVolume) { + if (!defined(tile._debugBoundingVolume)) { + var color = tile._finalResolution ? (hasContentBoundingVolume ? Color.WHITE : Color.RED) : Color.YELLOW; + tile._debugBoundingVolume = tile._boundingVolume.createDebugVolume(color); + } + tile._debugBoundingVolume.update(frameState); + } else if (!showVolume && defined(tile._debugBoundingVolume)) { + tile._debugBoundingVolume = tile._debugBoundingVolume.destroy(); + } + + if (tileset.debugShowContentBoundingVolume && hasContentBoundingVolume) { + if (!defined(tile._debugContentBoundingVolume)) { + tile._debugContentBoundingVolume = tile._contentBoundingVolume.createDebugVolume(Color.BLUE); + } + tile._debugContentBoundingVolume.update(frameState); + } else if (!tileset.debugShowContentBoundingVolume && defined(tile._debugContentBoundingVolume)) { + tile._debugContentBoundingVolume = tile._debugContentBoundingVolume.destroy(); + } + + if (tileset.debugShowViewerRequestVolume && defined(tile._viewerRequestVolume)) { + if (!defined(tile._debugViewerRequestVolume)) { + tile._debugViewerRequestVolume = tile._viewerRequestVolume.createDebugVolume(Color.YELLOW); + } + tile._debugViewerRequestVolume.update(frameState); + } else if (!tileset.debugShowViewerRequestVolume && defined(tile._debugViewerRequestVolume)) { + tile._debugViewerRequestVolume = tile._debugViewerRequestVolume.destroy(); + } + + var debugColorizeTilesOn = tileset.debugColorizeTiles && !tile._debugColorizeTiles; + var debugColorizeTilesOff = !tileset.debugColorizeTiles && tile._debugColorizeTiles; + + if (debugColorizeTilesOn) { + tile._debugColorizeTiles = true; + tile.color = tile._debugColor; + } else if (debugColorizeTilesOff) { + tile._debugColorizeTiles = false; + tile.color = Color.WHITE; + } + + if (tile._colorDirty) { + tile._colorDirty = false; + tile._content.applyDebugSettings(true, tile._color); + } + + if (debugColorizeTilesOff) { + tileset.makeStyleDirty(); // Re-apply style now that colorize is switched off + } + } + + function updateContent(tile, tileset, frameState) { + var content = tile._content; + var expiredContent = tile._expiredContent; + + if (defined(expiredContent)) { + if (!tile.contentReady) { + // Render the expired content while the content loads + expiredContent.update(tileset, frameState); + return; + } + + // New content is ready, destroy expired content + tile._expiredContent.destroy(); + tile._expiredContent = undefined; + } + + content.update(tileset, frameState); + } + + /** + * Get the draw commands needed to render this tile. + * + * @private + */ + Cesium3DTile.prototype.update = function(tileset, frameState) { + var initCommandLength = frameState.commandList.length; + applyDebugSettings(this, tileset, frameState); + updateContent(this, tileset, frameState); + this._commandsLength = frameState.commandList.length - initCommandLength; + }; + + var scratchCommandList = []; + + /** + * Processes the tile's content, e.g., create WebGL resources, to move from the PROCESSING to READY state. + * + * @param {Cesium3DTileset} tileset The tileset containing this tile. + * @param {FrameState} frameState The frame state. + * + * @private + */ + Cesium3DTile.prototype.process = function(tileset, frameState) { + var savedCommandList = frameState.commandList; + frameState.commandList = scratchCommandList; + + this._content.update(tileset, frameState); + + scratchCommandList.length = 0; + frameState.commandList = savedCommandList; + }; + + /** + * @private + */ + Cesium3DTile.prototype.isDestroyed = function() { + return false; + }; + + /** + * @private + */ + Cesium3DTile.prototype.destroy = function() { + // For the interval between new content being requested and downloaded, expiredContent === content, so don't destroy twice + this._content = this._content && this._content.destroy(); + this._expiredContent = this._expiredContent && !this._expiredContent.isDestroyed() && this._expiredContent.destroy(); + this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); + this._debugContentBoundingVolume = this._debugContentBoundingVolume && this._debugContentBoundingVolume.destroy(); + this._debugViewerRequestVolume = this._debugViewerRequestVolume && this._debugViewerRequestVolume.destroy(); + return destroyObject(this); + }; + + return Cesium3DTile; +}); diff --git a/Source/Scene/Cesium3DTileBatchTable.js b/Source/Scene/Cesium3DTileBatchTable.js new file mode 100644 index 000000000000..69f20ac1be21 --- /dev/null +++ b/Source/Scene/Cesium3DTileBatchTable.js @@ -0,0 +1,1467 @@ +/*global define*/ +define([ + '../Core/arrayFill', + '../Core/Cartesian2', + '../Core/Cartesian4', + '../Core/Check', + '../Core/clone', + '../Core/Color', + '../Core/combine', + '../Core/ComponentDatatype', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/DeveloperError', + '../Core/Math', + '../Core/PixelFormat', + '../Core/RuntimeError', + '../Renderer/ContextLimits', + '../Renderer/DrawCommand', + '../Renderer/Pass', + '../Renderer/PixelDatatype', + '../Renderer/RenderState', + '../Renderer/Sampler', + '../Renderer/ShaderSource', + '../Renderer/Texture', + '../Renderer/TextureMagnificationFilter', + '../Renderer/TextureMinificationFilter', + './AttributeType', + './BlendingState', + './Cesium3DTileColorBlendMode', + './CullFace', + './getBinaryAccessor', + './StencilFunction', + './StencilOperation' + ], function( + arrayFill, + Cartesian2, + Cartesian4, + Check, + clone, + Color, + combine, + ComponentDatatype, + defaultValue, + defined, + defineProperties, + destroyObject, + DeveloperError, + CesiumMath, + PixelFormat, + RuntimeError, + ContextLimits, + DrawCommand, + Pass, + PixelDatatype, + RenderState, + Sampler, + ShaderSource, + Texture, + TextureMagnificationFilter, + TextureMinificationFilter, + AttributeType, + BlendingState, + Cesium3DTileColorBlendMode, + CullFace, + getBinaryAccessor, + StencilFunction, + StencilOperation) { + 'use strict'; + + /** + * @private + */ + function Cesium3DTileBatchTable(content, featuresLength, batchTableJson, batchTableBinary) { + /** + * @readonly + */ + this.featuresLength = featuresLength; + + this._translucentFeaturesLength = 0; // Number of features in the tile that are translucent + + /** + * @private + */ + this.batchTableJson = batchTableJson; + + /** + * @private + */ + this.batchTableBinary = batchTableBinary; + + var batchTableHierarchy; + var batchTableBinaryProperties; + if (defined(batchTableJson)) { + // Extract the hierarchy and remove it from the batch table json + batchTableHierarchy = batchTableJson.HIERARCHY; + if (defined(batchTableHierarchy)) { + delete batchTableJson.HIERARCHY; + batchTableHierarchy = initializeHierarchy(batchTableHierarchy, batchTableBinary); + } + // Get the binary properties + batchTableBinaryProperties = Cesium3DTileBatchTable.getBinaryProperties(featuresLength, batchTableJson, batchTableBinary); + } + + this._batchTableHierarchy = batchTableHierarchy; + this._batchTableBinaryProperties = batchTableBinaryProperties; + + // PERFORMANCE_IDEA: These parallel arrays probably generate cache misses in get/set color/show + // and use A LOT of memory. How can we use less memory? + this._showAlphaProperties = undefined; // [Show (0 or 255), Alpha (0 to 255)] property for each feature + this._batchValues = undefined; // Per-feature RGBA (A is based on the color's alpha and feature's show property) + + this._batchValuesDirty = false; + this._batchTexture = undefined; + this._defaultTexture = undefined; + + this._pickTexture = undefined; + this._pickIds = []; + + this._content = content; + + // Dimensions for batch and pick textures + var textureDimensions; + var textureStep; + + if (featuresLength > 0) { + // PERFORMANCE_IDEA: this can waste memory in the last row in the uncommon case + // when more than one row is needed (e.g., > 16K features in one tile) + var width = Math.min(featuresLength, ContextLimits.maximumTextureSize); + var height = Math.ceil(featuresLength / ContextLimits.maximumTextureSize); + var stepX = 1.0 / width; + var centerX = stepX * 0.5; + var stepY = 1.0 / height; + var centerY = stepY * 0.5; + + textureDimensions = new Cartesian2(width, height); + textureStep = new Cartesian4(stepX, centerX, stepY, centerY); + } + + this._textureDimensions = textureDimensions; + this._textureStep = textureStep; + } + + defineProperties(Cesium3DTileBatchTable.prototype, { + memorySizeInBytes : { + get : function() { + var memory = 0; + if (defined(this._pickTexture)) { + memory += this._pickTexture.sizeInBytes; + } + if (defined(this._batchTexture)) { + memory += this._batchTexture.sizeInBytes; + } + return memory; + } + } + }); + + function initializeHierarchy(json, binary) { + var i; + var classId; + var binaryAccessor; + + var instancesLength = json.instancesLength; + var classes = json.classes; + var classIds = json.classIds; + var parentCounts = json.parentCounts; + var parentIds = json.parentIds; + var parentIdsLength = instancesLength; + + if (defined(classIds.byteOffset)) { + classIds.componentType = defaultValue(classIds.componentType, ComponentDatatype.UNSIGNED_SHORT); + classIds.type = AttributeType.SCALAR; + binaryAccessor = getBinaryAccessor(classIds); + classIds = binaryAccessor.createArrayBufferView(binary.buffer, binary.byteOffset + classIds.byteOffset, instancesLength); + } + + var parentIndexes; + if (defined(parentCounts)) { + if (defined(parentCounts.byteOffset)) { + parentCounts.componentType = defaultValue(parentCounts.componentType, ComponentDatatype.UNSIGNED_SHORT); + parentCounts.type = AttributeType.SCALAR; + binaryAccessor = getBinaryAccessor(parentCounts); + parentCounts = binaryAccessor.createArrayBufferView(binary.buffer, binary.byteOffset + parentCounts.byteOffset, instancesLength); + } + parentIndexes = new Uint16Array(instancesLength); + parentIdsLength = 0; + for (i = 0; i < instancesLength; ++i) { + parentIndexes[i] = parentIdsLength; + parentIdsLength += parentCounts[i]; + } + } + + if (defined(parentIds) && defined(parentIds.byteOffset)) { + parentIds.componentType = defaultValue(parentIds.componentType, ComponentDatatype.UNSIGNED_SHORT); + parentIds.type = AttributeType.SCALAR; + binaryAccessor = getBinaryAccessor(parentIds); + parentIds = binaryAccessor.createArrayBufferView(binary.buffer, binary.byteOffset + parentIds.byteOffset, parentIdsLength); + } + + var classesLength = classes.length; + for (i = 0; i < classesLength; ++i) { + var classInstancesLength = classes[i].length; + var properties = classes[i].instances; + var binaryProperties = Cesium3DTileBatchTable.getBinaryProperties(classInstancesLength, properties, binary); + classes[i].instances = combine(binaryProperties, properties); + } + + var classCounts = arrayFill(new Array(classesLength), 0); + var classIndexes = new Uint16Array(instancesLength); + for (i = 0; i < instancesLength; ++i) { + classId = classIds[i]; + classIndexes[i] = classCounts[classId]; + ++classCounts[classId]; + } + + var hierarchy = { + classes : classes, + classIds : classIds, + classIndexes : classIndexes, + parentCounts : parentCounts, + parentIndexes : parentIndexes, + parentIds : parentIds + }; + + //>>includeStart('debug', pragmas.debug); + validateHierarchy(hierarchy); + //>>includeEnd('debug'); + + return hierarchy; + } + + //>>includeStart('debug', pragmas.debug); + var scratchValidateStack = []; + function validateHierarchy(hierarchy) { + var stack = scratchValidateStack; + stack.length = 0; + + var classIds = hierarchy.classIds; + var instancesLength = classIds.length; + + for (var i = 0; i < instancesLength; ++i) { + validateInstance(hierarchy, i, stack); + } + } + + function validateInstance(hierarchy, instanceIndex, stack) { + var parentCounts = hierarchy.parentCounts; + var parentIds = hierarchy.parentIds; + var parentIndexes = hierarchy.parentIndexes; + var classIds = hierarchy.classIds; + var instancesLength = classIds.length; + + if (!defined(parentIds)) { + // No need to validate if there are no parents + return; + } + + if (instanceIndex >= instancesLength) { + throw new DeveloperError('Parent index ' + instanceIndex + ' exceeds the total number of instances: ' + instancesLength); + } + if (stack.indexOf(instanceIndex) > -1) { + throw new DeveloperError('Circular dependency detected in the batch table hierarchy.'); + } + + stack.push(instanceIndex); + var parentCount = defined(parentCounts) ? parentCounts[instanceIndex] : 1; + var parentIndex = defined(parentCounts) ? parentIndexes[instanceIndex] : instanceIndex; + for (var i = 0; i < parentCount; ++i) { + var parentId = parentIds[parentIndex + i]; + // Stop the traversal when the instance has no parent (its parentId equals itself), else continue the traversal. + if (parentId !== instanceIndex) { + validateInstance(hierarchy, parentId, stack); + } + } + stack.pop(instanceIndex); + } + //>>includeEnd('debug'); + + Cesium3DTileBatchTable.getBinaryProperties = function(featuresLength, json, binary) { + var binaryProperties; + for (var name in json) { + if (json.hasOwnProperty(name)) { + var property = json[name]; + var byteOffset = property.byteOffset; + if (defined(byteOffset)) { + // This is a binary property + var componentType = property.componentType; + var type = property.type; + if (!defined(componentType)) { + throw new RuntimeError('componentType is required.'); + } + if (!defined(type)) { + throw new RuntimeError('type is required.'); + } + if (!defined(binary)) { + throw new RuntimeError('Property ' + name + ' requires a batch table binary.'); + } + + var binaryAccessor = getBinaryAccessor(property); + var componentCount = binaryAccessor.componentsPerAttribute; + var classType = binaryAccessor.classType; + var typedArray = binaryAccessor.createArrayBufferView(binary.buffer, binary.byteOffset + byteOffset, featuresLength); + + if (!defined(binaryProperties)) { + binaryProperties = {}; + } + + // Store any information needed to access the binary data, including the typed array, + // componentCount (e.g. a VEC4 would be 4), and the type used to pack and unpack (e.g. Cartesian4). + binaryProperties[name] = { + typedArray : typedArray, + componentCount : componentCount, + type : classType + }; + } + } + } + return binaryProperties; + }; + + function getByteLength(batchTable) { + var dimensions = batchTable._textureDimensions; + return (dimensions.x * dimensions.y) * 4; + } + + function getBatchValues(batchTable) { + if (!defined(batchTable._batchValues)) { + // Default batch texture to RGBA = 255: white highlight (RGB) and show/alpha = true/255 (A). + var byteLength = getByteLength(batchTable); + var bytes = new Uint8Array(byteLength); + arrayFill(bytes, 255); + batchTable._batchValues = bytes; + } + + return batchTable._batchValues; + } + + function getShowAlphaProperties(batchTable) { + if (!defined(batchTable._showAlphaProperties)) { + var byteLength = 2 * batchTable.featuresLength; + var bytes = new Uint8Array(byteLength); + // [Show = true, Alpha = 255] + arrayFill(bytes, 255); + batchTable._showAlphaProperties = bytes; + } + return batchTable._showAlphaProperties; + } + + function checkBatchId(batchId, featuresLength) { + if (!defined(batchId) || (batchId < 0) || (batchId > featuresLength)) { + throw new DeveloperError('batchId is required and between zero and featuresLength - 1 (' + featuresLength - + ').'); + } + } + + Cesium3DTileBatchTable.prototype.setShow = function(batchId, show) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + Check.typeOf.bool('show', show); + //>>includeEnd('debug'); + + if (show && !defined(this._showAlphaProperties)) { + // Avoid allocating since the default is show = true + return; + } + + var showAlphaProperties = getShowAlphaProperties(this); + var propertyOffset = batchId * 2; + + var newShow = show ? 255 : 0; + if (showAlphaProperties[propertyOffset] !== newShow) { + showAlphaProperties[propertyOffset] = newShow; + + var batchValues = getBatchValues(this); + + // Compute alpha used in the shader based on show and color.alpha properties + var offset = (batchId * 4) + 3; + batchValues[offset] = show ? showAlphaProperties[propertyOffset + 1] : 0; + + this._batchValuesDirty = true; + } + }; + + Cesium3DTileBatchTable.prototype.setAllShow = function(show) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bool('show', show); + //>>includeEnd('debug'); + + var featuresLength = this.featuresLength; + for (var i = 0; i < featuresLength; ++i) { + this.setShow(i, show); + } + }; + + Cesium3DTileBatchTable.prototype.getShow = function(batchId) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + //>>includeEnd('debug'); + + if (!defined(this._showAlphaProperties)) { + // Avoid allocating since the default is show = true + return true; + } + + var offset = batchId * 2; + return (this._showAlphaProperties[offset] === 255); + }; + + var scratchColorBytes = new Array(4); + + Cesium3DTileBatchTable.prototype.setColor = function(batchId, color) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + Check.typeOf.object('color', color); + //>>includeEnd('debug'); + + if (Color.equals(color, Color.WHITE) && !defined(this._batchValues)) { + // Avoid allocating since the default is white + return; + } + + var newColor = color.toBytes(scratchColorBytes); + var newAlpha = newColor[3]; + + var batchValues = getBatchValues(this); + var offset = batchId * 4; + + var showAlphaProperties = getShowAlphaProperties(this); + var propertyOffset = batchId * 2; + + if ((batchValues[offset] !== newColor[0]) || + (batchValues[offset + 1] !== newColor[1]) || + (batchValues[offset + 2] !== newColor[2]) || + (showAlphaProperties[propertyOffset + 1] !== newAlpha)) { + + batchValues[offset] = newColor[0]; + batchValues[offset + 1] = newColor[1]; + batchValues[offset + 2] = newColor[2]; + + var wasTranslucent = (showAlphaProperties[propertyOffset + 1] !== 255); + + // Compute alpha used in the shader based on show and color.alpha properties + var show = showAlphaProperties[propertyOffset] !== 0; + batchValues[offset + 3] = show ? newAlpha : 0; + showAlphaProperties[propertyOffset + 1] = newAlpha; + + // Track number of translucent features so we know if this tile needs + // opaque commands, translucent commands, or both for rendering. + var isTranslucent = (newAlpha !== 255); + if (isTranslucent && !wasTranslucent) { + ++this._translucentFeaturesLength; + } else if (!isTranslucent && wasTranslucent) { + --this._translucentFeaturesLength; + } + + this._batchValuesDirty = true; + } + }; + + Cesium3DTileBatchTable.prototype.setAllColor = function(color) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('color', color); + //>>includeEnd('debug'); + + var featuresLength = this.featuresLength; + for (var i = 0; i < featuresLength; ++i) { + this.setColor(i, color); + } + }; + + Cesium3DTileBatchTable.prototype.getColor = function(batchId, result) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + Check.typeOf.object('result', result); + //>>includeEnd('debug'); + + if (!defined(this._batchValues)) { + return Color.clone(Color.WHITE, result); + } + + var batchValues = this._batchValues; + var offset = batchId * 4; + + var showAlphaProperties = this._showAlphaProperties; + var propertyOffset = batchId * 2; + + return Color.fromBytes(batchValues[offset], + batchValues[offset + 1], + batchValues[offset + 2], + showAlphaProperties[propertyOffset + 1], + result); + }; + + var scratchColor = new Color(); + + Cesium3DTileBatchTable.prototype.applyStyle = function(frameState, style) { + if (!defined(style)) { + this.setAllColor(Color.WHITE); + this.setAllShow(true); + return; + } + + var content = this._content; + var length = this.featuresLength; + for (var i = 0; i < length; ++i) { + var feature = content.getFeature(i); + var color = style.color.evaluateColor(frameState, feature, scratchColor); + var show = style.show.evaluate(frameState, feature); + this.setColor(i, color); + this.setShow(i, show); + } + }; + + function getBinaryProperty(binaryProperty, index) { + var typedArray = binaryProperty.typedArray; + var componentCount = binaryProperty.componentCount; + if (componentCount === 1) { + return typedArray[index]; + } + return binaryProperty.type.unpack(typedArray, index * componentCount); + } + + function setBinaryProperty(binaryProperty, index, value) { + var typedArray = binaryProperty.typedArray; + var componentCount = binaryProperty.componentCount; + if (componentCount === 1) { + typedArray[index] = value; + } else { + binaryProperty.type.pack(value, typedArray, index * componentCount); + } + } + + // The size of this array equals the maximum instance count among all loaded tiles, which has the potential to be large. + var scratchVisited = []; + var scratchStack = []; + var marker = 0; + function traverseHierarchyMultipleParents(hierarchy, instanceIndex, endConditionCallback) { + var classIds = hierarchy.classIds; + var parentCounts = hierarchy.parentCounts; + var parentIds = hierarchy.parentIds; + var parentIndexes = hierarchy.parentIndexes; + var instancesLength = classIds.length; + + // Ignore instances that have already been visited. This occurs in diamond inheritance situations. + // Use a marker value to indicate that an instance has been visited, which increments with each run. + // This is more efficient than clearing the visited array every time. + var visited = scratchVisited; + visited.length = Math.max(visited.length, instancesLength); + var visitedMarker = ++marker; + + var stack = scratchStack; + stack.length = 0; + stack.push(instanceIndex); + + while (stack.length > 0) { + instanceIndex = stack.pop(); + if (visited[instanceIndex] === visitedMarker) { + // This instance has already been visited, stop traversal + continue; + } + visited[instanceIndex] = visitedMarker; + var result = endConditionCallback(hierarchy, instanceIndex); + if (defined(result)) { + // The end condition was met, stop the traversal and return the result + return result; + } + var parentCount = parentCounts[instanceIndex]; + var parentIndex = parentIndexes[instanceIndex]; + for (var i = 0; i < parentCount; ++i) { + var parentId = parentIds[parentIndex + i]; + // Stop the traversal when the instance has no parent (its parentId equals itself) + // else add the parent to the stack to continue the traversal. + if (parentId !== instanceIndex) { + stack.push(parentId); + } + } + } + } + + function traverseHierarchySingleParent(hierarchy, instanceIndex, endConditionCallback) { + var hasParent = true; + while (hasParent) { + var result = endConditionCallback(hierarchy, instanceIndex); + if (defined(result)) { + // The end condition was met, stop the traversal and return the result + return result; + } + var parentId = hierarchy.parentIds[instanceIndex]; + hasParent = parentId !== instanceIndex; + instanceIndex = parentId; + } + } + + function traverseHierarchy(hierarchy, instanceIndex, endConditionCallback) { + // Traverse over the hierarchy and process each instance with the endConditionCallback. + // When the endConditionCallback returns a value, the traversal stops and that value is returned. + var parentCounts = hierarchy.parentCounts; + var parentIds = hierarchy.parentIds; + if (!defined(parentIds)) { + return endConditionCallback(hierarchy, instanceIndex); + } else if (defined(parentCounts)) { + return traverseHierarchyMultipleParents(hierarchy, instanceIndex, endConditionCallback); + } + return traverseHierarchySingleParent(hierarchy, instanceIndex, endConditionCallback); + } + + function hasPropertyInHierarchy(batchTable, batchId, name) { + var hierarchy = batchTable._batchTableHierarchy; + var result = traverseHierarchy(hierarchy, batchId, function(hierarchy, instanceIndex) { + var classId = hierarchy.classIds[instanceIndex]; + var instances = hierarchy.classes[classId].instances; + if (defined(instances[name])) { + return true; + } + }); + return defined(result); + } + + function getPropertyNamesInHierarchy(batchTable, batchId, results) { + var hierarchy = batchTable._batchTableHierarchy; + traverseHierarchy(hierarchy, batchId, function(hierarchy, instanceIndex) { + var classId = hierarchy.classIds[instanceIndex]; + var instances = hierarchy.classes[classId].instances; + for (var name in instances) { + if (instances.hasOwnProperty(name)) { + if (results.indexOf(name) === -1) { + results.push(name); + } + } + } + }); + } + + function getHierarchyProperty(batchTable, batchId, name) { + var hierarchy = batchTable._batchTableHierarchy; + return traverseHierarchy(hierarchy, batchId, function(hierarchy, instanceIndex) { + var classId = hierarchy.classIds[instanceIndex]; + var instanceClass = hierarchy.classes[classId]; + var indexInClass = hierarchy.classIndexes[instanceIndex]; + var propertyValues = instanceClass.instances[name]; + if (defined(propertyValues)) { + if (defined(propertyValues.typedArray)) { + return getBinaryProperty(propertyValues, indexInClass); + } + return clone(propertyValues[indexInClass], true); + } + }); + } + + function setHierarchyProperty(batchTable, batchId, name, value) { + var hierarchy = batchTable._batchTableHierarchy; + var result = traverseHierarchy(hierarchy, batchId, function(hierarchy, instanceIndex) { + var classId = hierarchy.classIds[instanceIndex]; + var instanceClass = hierarchy.classes[classId]; + var indexInClass = hierarchy.classIndexes[instanceIndex]; + var propertyValues = instanceClass.instances[name]; + if (defined(propertyValues)) { + //>>includeStart('debug', pragmas.debug); + if (instanceIndex !== batchId) { + throw new DeveloperError('Inherited property "' + name + '" is read-only.'); + } + //>>includeEnd('debug'); + if (defined(propertyValues.typedArray)) { + setBinaryProperty(propertyValues, indexInClass, value); + } else { + propertyValues[indexInClass] = clone(value, true); + } + return true; + } + }); + return defined(result); + } + + Cesium3DTileBatchTable.prototype.isClass = function(batchId, className) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + Check.typeOf.string('className', className); + //>>includeEnd('debug'); + + // PERFORMANCE_IDEA : cache results in the ancestor classes to speed up this check if this area becomes a hotspot + var hierarchy = this._batchTableHierarchy; + if (!defined(hierarchy)) { + return false; + } + + // PERFORMANCE_IDEA : treat class names as integers for faster comparisons + var result = traverseHierarchy(hierarchy, batchId, function(hierarchy, instanceIndex) { + var classId = hierarchy.classIds[instanceIndex]; + var instanceClass = hierarchy.classes[classId]; + if (instanceClass.name === className) { + return true; + } + }); + return defined(result); + }; + + Cesium3DTileBatchTable.prototype.isExactClass = function(batchId, className) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('className', className); + //>>includeEnd('debug'); + + return (this.getExactClassName(batchId) === className); + }; + + Cesium3DTileBatchTable.prototype.getExactClassName = function(batchId) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + //>>includeEnd('debug'); + + var hierarchy = this._batchTableHierarchy; + if (!defined(hierarchy)) { + return undefined; + } + var classId = hierarchy.classIds[batchId]; + var instanceClass = hierarchy.classes[classId]; + return instanceClass.name; + }; + + Cesium3DTileBatchTable.prototype.hasProperty = function(batchId, name) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + Check.typeOf.string('name', name); + //>>includeEnd('debug'); + + var json = this.batchTableJson; + return (defined(json) && defined(json[name])) || (defined(this._batchTableHierarchy) && hasPropertyInHierarchy(this, batchId, name)); + }; + + Cesium3DTileBatchTable.prototype.getPropertyNames = function(batchId, results) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + //>>includeEnd('debug'); + + results = defined(results) ? results : []; + results.length = 0; + + var json = this.batchTableJson; + for (var name in json) { + if (json.hasOwnProperty(name)) { + results.push(name); + } + } + + if (defined(this._batchTableHierarchy)) { + getPropertyNamesInHierarchy(this, batchId, results); + } + + return results; + }; + + Cesium3DTileBatchTable.prototype.getProperty = function(batchId, name) { + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, this.featuresLength); + Check.typeOf.string('name', name); + //>>includeEnd('debug'); + + if (!defined(this.batchTableJson)) { + return undefined; + } + + if (defined(this._batchTableBinaryProperties)) { + var binaryProperty = this._batchTableBinaryProperties[name]; + if (defined(binaryProperty)) { + return getBinaryProperty(binaryProperty, batchId); + } + } + + var propertyValues = this.batchTableJson[name]; + if (defined(propertyValues)) { + return clone(propertyValues[batchId], true); + } + + if (defined(this._batchTableHierarchy)) { + var hierarchyProperty = getHierarchyProperty(this, batchId, name); + if (defined(hierarchyProperty)) { + return hierarchyProperty; + } + } + + return undefined; + }; + + Cesium3DTileBatchTable.prototype.setProperty = function(batchId, name, value) { + var featuresLength = this.featuresLength; + //>>includeStart('debug', pragmas.debug); + checkBatchId(batchId, featuresLength); + Check.typeOf.string('name', name); + //>>includeEnd('debug'); + + if (defined(this._batchTableBinaryProperties)) { + var binaryProperty = this._batchTableBinaryProperties[name]; + if (defined(binaryProperty)) { + setBinaryProperty(binaryProperty, batchId, value); + return; + } + } + + if (defined(this._batchTableHierarchy)) { + if (setHierarchyProperty(this, batchId, name, value)) { + return; + } + } + + if (!defined(this.batchTableJson)) { + // Tile payload did not have a batch table. Create one for new user-defined properties. + this.batchTableJson = {}; + } + + var propertyValues = this.batchTableJson[name]; + + if (!defined(propertyValues)) { + // Property does not exist. Create it. + this.batchTableJson[name] = new Array(featuresLength); + propertyValues = this.batchTableJson[name]; + } + + propertyValues[batchId] = clone(value, true); + }; + + function getGlslComputeSt(batchTable) { + // GLSL batchId is zero-based: [0, featuresLength - 1] + if (batchTable._textureDimensions.y === 1) { + return 'uniform vec4 tile_textureStep; \n' + + 'vec2 computeSt(float batchId) \n' + + '{ \n' + + ' float stepX = tile_textureStep.x; \n' + + ' float centerX = tile_textureStep.y; \n' + + ' return vec2(centerX + (batchId * stepX), 0.5); \n' + + '} \n'; + } + + return 'uniform vec4 tile_textureStep; \n' + + 'uniform vec2 tile_textureDimensions; \n' + + 'vec2 computeSt(float batchId) \n' + + '{ \n' + + ' float stepX = tile_textureStep.x; \n' + + ' float centerX = tile_textureStep.y; \n' + + ' float stepY = tile_textureStep.z; \n' + + ' float centerY = tile_textureStep.w; \n' + + ' float xId = mod(batchId, tile_textureDimensions.x); \n' + + ' float yId = floor(batchId / tile_textureDimensions.x); \n' + + ' return vec2(centerX + (xId * stepX), 1.0 - (centerY + (yId * stepY))); \n' + + '} \n'; + } + + Cesium3DTileBatchTable.prototype.getVertexShaderCallback = function(handleTranslucent, batchIdAttributeName) { + if (this.featuresLength === 0) { + return; + } + + var that = this; + return function(source) { + var renamedSource = ShaderSource.replaceMain(source, 'tile_main'); + var newMain; + + if (ContextLimits.maximumVertexTextureImageUnits > 0) { + // When VTF is supported, perform per-feature show/hide in the vertex shader + newMain = + 'uniform sampler2D tile_batchTexture; \n' + + 'uniform bool tile_translucentCommand; \n' + + 'varying vec4 tile_featureColor; \n' + + 'void main() \n' + + '{ \n' + + ' tile_main(); \n' + + ' vec2 st = computeSt(' + batchIdAttributeName + '); \n' + + ' vec4 featureProperties = texture2D(tile_batchTexture, st); \n' + + ' float show = ceil(featureProperties.a); \n' + // 0 - false, non-zeo - true + ' gl_Position *= show; \n'; // Per-feature show/hide + if (handleTranslucent) { + newMain += + ' bool isStyleTranslucent = (featureProperties.a != 1.0); \n' + + ' if (czm_pass == czm_passTranslucent) \n' + + ' { \n' + + ' if (!isStyleTranslucent && !tile_translucentCommand) \n' + // Do not render opaque features in the translucent pass + ' { \n' + + ' gl_Position *= 0.0; \n' + + ' } \n' + + ' } \n' + + ' else \n' + + ' { \n' + + ' if (isStyleTranslucent) \n' + // Do not render translucent features in the opaque pass + ' { \n' + + ' gl_Position *= 0.0; \n' + + ' } \n' + + ' } \n'; + } + newMain += + ' tile_featureColor = featureProperties; \n' + + '}'; + } else { + newMain = + 'varying vec2 tile_featureSt; \n' + + 'void main() \n' + + '{ \n' + + ' tile_main(); \n' + + ' tile_featureSt = computeSt(' + batchIdAttributeName + '); \n' + + '}'; + } + + return renamedSource + '\n' + getGlslComputeSt(that) + newMain; + }; + }; + + function getHighlightOnlyShader(source) { + source = ShaderSource.replaceMain(source, 'tile_main'); + return source + + 'void tile_color(vec4 tile_featureColor) \n' + + '{ \n' + + ' tile_main(); \n' + + ' gl_FragColor *= tile_featureColor; \n' + + '} \n'; + } + + function modifyDiffuse(source, diffuseUniformName) { + // If the glTF does not specify the _3DTILESDIFFUSE semantic, return a basic highlight shader. + // Otherwise if _3DTILESDIFFUSE is defined prefer the shader below that can switch the color mode at runtime. + if (!defined(diffuseUniformName)) { + return getHighlightOnlyShader(source); + } + + // Find the diffuse uniform. Examples matches: + // uniform vec3 u_diffuseColor; + // uniform sampler2D diffuseTexture; + var regex = new RegExp('uniform\\s+(vec[34]|sampler2D)\\s+' + diffuseUniformName + ';'); + var uniformMatch = source.match(regex); + + if (!defined(uniformMatch)) { + // Could not find uniform declaration of type vec3, vec4, or sampler2D + return getHighlightOnlyShader(source); + } + + var declaration = uniformMatch[0]; + var type = uniformMatch[1]; + + source = ShaderSource.replaceMain(source, 'tile_main'); + source = source.replace(declaration, ''); // Remove uniform declaration for now so the replace below doesn't affect it + + // If the tile color is white, use the source color. This implies the feature has not been styled. + // Highlight: tile_colorBlend is 0.0 and the source color is used + // Replace: tile_colorBlend is 1.0 and the tile color is used + // Mix: tile_colorBlend is between 0.0 and 1.0, causing the source color and tile color to mix + var finalDiffuseFunction = + 'vec4 tile_diffuse_final(vec4 sourceDiffuse, vec4 tileDiffuse) \n' + + '{ \n' + + ' vec4 blendDiffuse = mix(sourceDiffuse, tileDiffuse, tile_colorBlend); \n' + + ' vec4 diffuse = (tileDiffuse.rgb == vec3(1.0)) ? sourceDiffuse : blendDiffuse; \n' + + ' return vec4(diffuse.rgb, sourceDiffuse.a); \n' + + '} \n'; + + // The color blend mode is intended for the RGB channels so alpha is always just multiplied. + // gl_FragColor is multiplied by the tile color only when tile_colorBlend is 0.0 (highlight) + var applyHighlight = + ' gl_FragColor.a *= tile_featureColor.a; \n' + + ' float highlight = ceil(tile_colorBlend); \n' + + ' gl_FragColor.rgb *= mix(tile_featureColor.rgb, vec3(1.0), highlight); \n'; + + var setColor; + if (type === 'vec3' || type === 'vec4') { + var sourceDiffuse = (type === 'vec3') ? ('vec4(' + diffuseUniformName + ', 1.0)') : diffuseUniformName; + var replaceDiffuse = (type === 'vec3') ? 'tile_diffuse.xyz' : 'tile_diffuse'; + regex = new RegExp(diffuseUniformName, 'g'); + source = source.replace(regex, replaceDiffuse); + setColor = + ' vec4 source = ' + sourceDiffuse + '; \n' + + ' tile_diffuse = tile_diffuse_final(source, tile_featureColor); \n' + + ' tile_main(); \n'; + } else if (type === 'sampler2D') { + regex = new RegExp('texture2D\\(' + diffuseUniformName + '.*?\\)', 'g'); + source = source.replace(regex, 'tile_diffuse_final($&, tile_diffuse)'); + setColor = + ' tile_diffuse = tile_featureColor; \n' + + ' tile_main(); \n'; + } + + source = + 'uniform float tile_colorBlend; \n' + + 'vec4 tile_diffuse = vec4(1.0); \n' + + finalDiffuseFunction + + declaration + '\n' + + source + '\n' + + 'void tile_color(vec4 tile_featureColor) \n' + + '{ \n' + + setColor + + applyHighlight + + '} \n'; + + return source; + } + + Cesium3DTileBatchTable.prototype.getFragmentShaderCallback = function(handleTranslucent, diffuseUniformName) { + if (this.featuresLength === 0) { + return; + } + return function(source) { + source = modifyDiffuse(source, diffuseUniformName); + if (ContextLimits.maximumVertexTextureImageUnits > 0) { + // When VTF is supported, per-feature show/hide already happened in the fragment shader + source += + 'varying vec4 tile_featureColor; \n' + + 'void main() \n' + + '{ \n' + + ' tile_color(tile_featureColor); \n' + + '}'; + } else { + source += + 'uniform sampler2D tile_batchTexture; \n' + + 'uniform bool tile_translucentCommand; \n' + + 'varying vec2 tile_featureSt; \n' + + 'void main() \n' + + '{ \n' + + ' vec4 featureProperties = texture2D(tile_batchTexture, tile_featureSt); \n' + + ' if (featureProperties.a == 0.0) { \n' + // show: alpha == 0 - false, non-zeo - true + ' discard; \n' + + ' } \n'; + + if (handleTranslucent) { + source += + ' bool isStyleTranslucent = (featureProperties.a != 1.0); \n' + + ' if (czm_pass == czm_passTranslucent) \n' + + ' { \n' + + ' if (!isStyleTranslucent && !tile_translucentCommand) \n' + // Do not render opaque features in the translucent pass + ' { \n' + + ' discard; \n' + + ' } \n' + + ' } \n' + + ' else \n' + + ' { \n' + + ' if (isStyleTranslucent) \n' + // Do not render translucent features in the opaque pass + ' { \n' + + ' discard; \n' + + ' } \n' + + ' } \n'; + } + + source += + ' tile_color(featureProperties); \n' + + '} \n'; + } + return source; + }; + }; + + function getColorBlend(batchTable) { + var tileset = batchTable._content._tileset; + var colorBlendMode = tileset.colorBlendMode; + var colorBlendAmount = tileset.colorBlendAmount; + if (colorBlendMode === Cesium3DTileColorBlendMode.HIGHLIGHT) { + return 0.0; + } + if (colorBlendMode === Cesium3DTileColorBlendMode.REPLACE) { + return 1.0; + } + if (colorBlendMode === Cesium3DTileColorBlendMode.MIX) { + // The value 0.0 is reserved for highlight, so clamp to just above 0.0. + return CesiumMath.clamp(colorBlendAmount, CesiumMath.EPSILON4, 1.0); + } + //>>includeStart('debug', pragmas.debug); + throw new DeveloperError('Invalid color blend mode "' + colorBlendMode + '".'); + //>>includeEnd('debug'); + } + + Cesium3DTileBatchTable.prototype.getUniformMapCallback = function() { + if (this.featuresLength === 0) { + return; + } + + var that = this; + return function(uniformMap) { + var batchUniformMap = { + tile_batchTexture : function() { + // PERFORMANCE_IDEA: we could also use a custom shader that avoids the texture read. + return defaultValue(that._batchTexture, that._defaultTexture); + }, + tile_textureDimensions : function() { + return that._textureDimensions; + }, + tile_textureStep : function() { + return that._textureStep; + }, + tile_colorBlend : function() { + return getColorBlend(that); + } + }; + + return combine(uniformMap, batchUniformMap); + }; + }; + + Cesium3DTileBatchTable.prototype.getPickVertexShaderCallback = function(batchIdAttributeName) { + if (this.featuresLength === 0) { + return; + } + + var that = this; + return function(source) { + var renamedSource = ShaderSource.replaceMain(source, 'tile_main'); + var newMain; + + if (ContextLimits.maximumVertexTextureImageUnits > 0) { + // When VTF is supported, perform per-feature show/hide in the vertex shader + newMain = + 'uniform sampler2D tile_batchTexture; \n' + + 'varying vec2 tile_featureSt; \n' + + 'void main() \n' + + '{ \n' + + ' tile_main(); \n' + + ' vec2 st = computeSt(' + batchIdAttributeName + '); \n' + + ' vec4 featureProperties = texture2D(tile_batchTexture, st); \n' + + ' float show = ceil(featureProperties.a); \n' + // 0 - false, non-zero - true + ' gl_Position *= show; \n' + // Per-feature show/hide + ' tile_featureSt = st; \n' + + '}'; + } else { + newMain = + 'varying vec2 tile_featureSt; \n' + + 'void main() \n' + + '{ \n' + + ' tile_main(); \n' + + ' tile_featureSt = computeSt(' + batchIdAttributeName + '); \n' + + '}'; + } + + return renamedSource + '\n' + getGlslComputeSt(that) + newMain; + }; + }; + + Cesium3DTileBatchTable.prototype.getPickFragmentShaderCallback = function() { + if (this.featuresLength === 0) { + return; + } + + return function(source) { + var renamedSource = ShaderSource.replaceMain(source, 'tile_main'); + var newMain; + + // Pick shaders do not need to take into account per-feature color/alpha. + // (except when alpha is zero, which is treated as if show is false, so + // it does not write depth in the color or pick pass). + if (ContextLimits.maximumVertexTextureImageUnits > 0) { + // When VTF is supported, per-feature show/hide already happened in the fragment shader + newMain = + 'uniform sampler2D tile_pickTexture; \n' + + 'varying vec2 tile_featureSt; \n' + + 'void main() \n' + + '{ \n' + + ' tile_main(); \n' + + ' if (gl_FragColor.a == 0.0) { \n' + // per-feature show: alpha == 0 - false, non-zeo - true + ' discard; \n' + + ' } \n' + + ' gl_FragColor = texture2D(tile_pickTexture, tile_featureSt); \n' + + '}'; + } else { + newMain = + 'uniform sampler2D tile_pickTexture; \n' + + 'uniform sampler2D tile_batchTexture; \n' + + 'varying vec2 tile_featureSt; \n' + + 'void main() \n' + + '{ \n' + + ' vec4 featureProperties = texture2D(tile_batchTexture, tile_featureSt); \n' + + ' if (featureProperties.a == 0.0) { \n' + // per-feature show: alpha == 0 - false, non-zeo - true + ' discard; \n' + + ' } \n' + + ' tile_main(); \n' + + ' if (gl_FragColor.a == 0.0) { \n' + + ' discard; \n' + + ' } \n' + + ' gl_FragColor = texture2D(tile_pickTexture, tile_featureSt); \n' + + '}'; + } + + return renamedSource + '\n' + newMain; + }; + }; + + Cesium3DTileBatchTable.prototype.getPickUniformMapCallback = function() { + if (this.featuresLength === 0) { + return; + } + + var that = this; + return function(uniformMap) { + var batchUniformMap = { + tile_batchTexture : function() { + return defaultValue(that._batchTexture, that._defaultTexture); + }, + tile_textureDimensions : function() { + return that._textureDimensions; + }, + tile_textureStep : function() { + return that._textureStep; + }, + tile_pickTexture : function() { + return that._pickTexture; + } + }; + + return combine(batchUniformMap, uniformMap); + }; + }; + + /////////////////////////////////////////////////////////////////////////// + + var StyleCommandsNeeded = { + ALL_OPAQUE : 0, + ALL_TRANSLUCENT : 1, + OPAQUE_AND_TRANSLUCENT : 2 + }; + + Cesium3DTileBatchTable.prototype.addDerivedCommands = function(frameState, commandStart) { + var commandList = frameState.commandList; + var commandEnd = commandList.length; + var tile = this._content._tile; + var tileset = tile._tileset; + var bivariateVisibilityTest = tileset.skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer; + var styleCommandsNeeded = getStyleCommandsNeeded(this); + + for (var i = commandStart; i < commandEnd; ++i) { + var command = commandList[i]; + var derivedCommands = command.derivedCommands.tileset; + if (!defined(derivedCommands)) { + derivedCommands = {}; + command.derivedCommands.tileset = derivedCommands; + derivedCommands.originalCommand = deriveCommand(command); + } + + updateDerivedCommand(derivedCommands.originalCommand, command); + + if (styleCommandsNeeded !== StyleCommandsNeeded.ALL_OPAQUE) { + if (!defined(derivedCommands.translucent)) { + derivedCommands.translucent = deriveTranslucentCommand(derivedCommands.originalCommand); + } + updateDerivedCommand(derivedCommands.translucent, command); + } + + if (bivariateVisibilityTest) { + if (command.pass !== Pass.TRANSLUCENT) { + if (!defined(derivedCommands.zback)) { + derivedCommands.zback = deriveZBackfaceCommand(derivedCommands.originalCommand); + } + tileset._backfaceCommands.push(derivedCommands.zback); + } + if (!defined(derivedCommands.stencil) || tile._selectionDepth !== tile._lastSelectionDepth) { + derivedCommands.stencil = deriveStencilCommand(derivedCommands.originalCommand, tile._selectionDepth); + tile._lastSelectionDepth = tile._selectionDepth; + } + updateDerivedCommand(derivedCommands.stencil, command); + } + + var opaqueCommand = bivariateVisibilityTest ? derivedCommands.stencil : derivedCommands.originalCommand; + var translucentCommand = derivedCommands.translucent; + + // If the command was originally opaque: + // * If the styling applied to the tile is all opaque, use the original command + // (with one additional uniform needed for the shader). + // * If the styling is all translucent, use new (cached) derived commands (front + // and back faces) with a translucent render state. + // * If the styling causes both opaque and translucent features in this tile, + // then use both sets of commands. + if (command.pass !== Pass.TRANSLUCENT) { + if (styleCommandsNeeded === StyleCommandsNeeded.ALL_OPAQUE) { + commandList[i] = opaqueCommand; + } + if (styleCommandsNeeded === StyleCommandsNeeded.ALL_TRANSLUCENT) { + commandList[i] = translucentCommand; + } + if (styleCommandsNeeded === StyleCommandsNeeded.OPAQUE_AND_TRANSLUCENT) { + // PERFORMANCE_IDEA: if the tile has multiple commands, we do not know what features are in what + // commands so this case may be overkill. + commandList[i] = opaqueCommand; + commandList.push(translucentCommand); + } + } else { + // Command was originally translucent so no need to derive new commands; + // as of now, a style can't change an originally translucent feature to + // opaque since the style's alpha is modulated, not a replacement. When + // this changes, we need to derive new opaque commands here. + commandList[i] = opaqueCommand; + } + } + }; + + function updateDerivedCommand(derivedCommand, command) { + derivedCommand.castShadows = command.castShadows; + derivedCommand.receiveShadows = command.receiveShadows; + derivedCommand.primitiveType = command.primitiveType; + } + + function getStyleCommandsNeeded(batchTable) { + var translucentFeaturesLength = batchTable._translucentFeaturesLength; + + if (translucentFeaturesLength === 0) { + return StyleCommandsNeeded.ALL_OPAQUE; + } else if (translucentFeaturesLength === batchTable.featuresLength) { + return StyleCommandsNeeded.ALL_TRANSLUCENT; + } + + return StyleCommandsNeeded.OPAQUE_AND_TRANSLUCENT; + } + + function deriveCommand(command) { + var derivedCommand = DrawCommand.shallowClone(command); + + // Add a uniform to indicate if the original command was translucent so + // the shader knows not to cull vertices that were originally transparent + // even though their style is opaque. + var translucentCommand = (derivedCommand.pass === Pass.TRANSLUCENT); + + if (!translucentCommand) { + derivedCommand.pass = Pass.CESIUM_3D_TILE; + } + + derivedCommand.uniformMap = defined(derivedCommand.uniformMap) ? derivedCommand.uniformMap : {}; + derivedCommand.uniformMap.tile_translucentCommand = function() { + return translucentCommand; + }; + + return derivedCommand; + } + + function deriveTranslucentCommand(command) { + var derivedCommand = DrawCommand.shallowClone(command); + derivedCommand.pass = Pass.TRANSLUCENT; + derivedCommand.renderState = getTranslucentRenderState(command.renderState); + return derivedCommand; + } + + function deriveZBackfaceCommand(command) { + // Write just backface depth of unresolved tiles so resolved stenciled tiles do not appear in front + var derivedCommand = DrawCommand.shallowClone(command); + var rs = clone(derivedCommand.renderState, true); + rs.cull.enabled = true; + rs.cull.face = CullFace.FRONT; + derivedCommand.renderState = RenderState.fromCache(rs); + derivedCommand.castShadows = false; + derivedCommand.receiveShadows = false; + return derivedCommand; + } + + function deriveStencilCommand(command, reference) { + var derivedCommand = command; + if (command.renderState.depthMask) { // ignore if tile does not write depth (ex. translucent) + // Tiles only draw if their selection depth is >= the tile drawn already. They write their + // selection depth to the stencil buffer to prevent ancestor tiles from drawing on top + derivedCommand = DrawCommand.shallowClone(command); + var rs = clone(derivedCommand.renderState, true); + rs.stencilTest.enabled = true; + rs.stencilTest.reference = reference; + rs.stencilTest.frontFunction = StencilFunction.GREATER_OR_EQUAL; + rs.stencilTest.frontOperation.zPass = StencilOperation.REPLACE; + derivedCommand.renderState = RenderState.fromCache(rs); + } + return derivedCommand; + } + + function getTranslucentRenderState(renderState) { + var rs = clone(renderState, true); + rs.cull.enabled = false; + rs.depthTest.enabled = true; + rs.depthMask = false; + rs.blending = BlendingState.ALPHA_BLEND; + + return RenderState.fromCache(rs); + } + + /////////////////////////////////////////////////////////////////////////// + + function createTexture(batchTable, context, bytes) { + var dimensions = batchTable._textureDimensions; + return new Texture({ + context : context, + pixelFormat : PixelFormat.RGBA, + pixelDatatype : PixelDatatype.UNSIGNED_BYTE, + source : { + width : dimensions.x, + height : dimensions.y, + arrayBufferView : bytes + }, + sampler : new Sampler({ + minificationFilter : TextureMinificationFilter.NEAREST, + magnificationFilter : TextureMagnificationFilter.NEAREST + }) + }); + } + + function createPickTexture(batchTable, context) { + var featuresLength = batchTable.featuresLength; + if (!defined(batchTable._pickTexture) && (featuresLength > 0)) { + var pickIds = batchTable._pickIds; + var byteLength = getByteLength(batchTable); + var bytes = new Uint8Array(byteLength); + var content = batchTable._content; + + // PERFORMANCE_IDEA: we could skip the pick texture completely by allocating + // a continuous range of pickIds and then converting the base pickId + batchId + // to RGBA in the shader. The only consider is precision issues, which might + // not be an issue in WebGL 2. + for (var i = 0; i < featuresLength; ++i) { + var pickId = context.createPickId(content.getFeature(i)); + pickIds.push(pickId); + + var pickColor = pickId.color; + var offset = i * 4; + bytes[offset] = Color.floatToByte(pickColor.red); + bytes[offset + 1] = Color.floatToByte(pickColor.green); + bytes[offset + 2] = Color.floatToByte(pickColor.blue); + bytes[offset + 3] = Color.floatToByte(pickColor.alpha); + } + + batchTable._pickTexture = createTexture(batchTable, context, bytes); + content._tileset._statistics.batchTableByteLength += batchTable._pickTexture.sizeInBytes; + } + } + + function updateBatchTexture(batchTable) { + var dimensions = batchTable._textureDimensions; + // PERFORMANCE_IDEA: Instead of rewriting the entire texture, use fine-grained + // texture updates when less than, for example, 10%, of the values changed. Or + // even just optimize the common case when one feature show/color changed. + batchTable._batchTexture.copyFrom({ + width : dimensions.x, + height : dimensions.y, + arrayBufferView : batchTable._batchValues + }); + } + + Cesium3DTileBatchTable.prototype.update = function(tileset, frameState) { + var context = frameState.context; + this._defaultTexture = context.defaultTexture; + + if (frameState.passes.pick) { + // Create pick texture on-demand + createPickTexture(this, context); + } + + if (this._batchValuesDirty) { + this._batchValuesDirty = false; + + // Create batch texture on-demand + if (!defined(this._batchTexture)) { + this._batchTexture = createTexture(this, context, this._batchValues); + tileset._statistics.batchTableByteLength += this._batchTexture.sizeInBytes; + } + + updateBatchTexture(this); // Apply per-feature show/color updates + } + }; + + Cesium3DTileBatchTable.prototype.isDestroyed = function() { + return false; + }; + + Cesium3DTileBatchTable.prototype.destroy = function() { + this._batchTexture = this._batchTexture && this._batchTexture.destroy(); + this._pickTexture = this._pickTexture && this._pickTexture.destroy(); + + var pickIds = this._pickIds; + var length = pickIds.length; + for (var i = 0; i < length; ++i) { + pickIds[i].destroy(); + } + + return destroyObject(this); + }; + + return Cesium3DTileBatchTable; +}); diff --git a/Source/Scene/Cesium3DTileChildrenVisibility.js b/Source/Scene/Cesium3DTileChildrenVisibility.js new file mode 100644 index 000000000000..338fff0e205f --- /dev/null +++ b/Source/Scene/Cesium3DTileChildrenVisibility.js @@ -0,0 +1,20 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + 'use strict'; + + /** + * @private + */ + var Cesium3DTileChildrenVisibility = { + NONE : 0, // No children visible + VISIBLE : 1, // At least one child visible + IN_REQUEST_VOLUME : 2, // At least one child in viewer request volume + VISIBLE_IN_REQUEST_VOLUME : 4, // At least one child both visible and in viewer request volume + VISIBLE_NOT_IN_REQUEST_VOLUME : 8 // At least one child visible but not in viewer request volume + }; + + return freezeObject(Cesium3DTileChildrenVisibility); +}); diff --git a/Source/Scene/Cesium3DTileColorBlendMode.js b/Source/Scene/Cesium3DTileColorBlendMode.js new file mode 100644 index 000000000000..1c1d7c4fbb17 --- /dev/null +++ b/Source/Scene/Cesium3DTileColorBlendMode.js @@ -0,0 +1,57 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + 'use strict'; + + /** + * Defines how per-feature colors set from the Cesium API or declarative styling blend with the source colors from + * the original feature, e.g. glTF material or per-point color in the tile. + *

+ * When REPLACE or MIX are used and the source color is a glTF material, the technique must assign the + * _3DTILESDIFFUSE semantic to the diffuse color parameter. Otherwise only HIGHLIGHT is supported. + *

+ *

+     * "techniques": {
+     *   "technique0": {
+     *     "parameters": {
+     *       "diffuse": {
+     *         "semantic": "_3DTILESDIFFUSE",
+     *         "type": 35666
+     *       }
+     *     }
+     *   }
+     * }
+     * 
+ * + * @exports Cesium3DTileColorBlendMode + */ + var Cesium3DTileColorBlendMode = { + /** + * Multiplies the source color by the feature color. + * + * @type {Number} + * @constant + */ + HIGHLIGHT : 0, + + /** + * Replaces the source color with the feature color. + * + * @type {Number} + * @constant + */ + REPLACE : 1, + + /** + * Blends the source color and feature color together. + * + * @type {Number} + * @constant + */ + MIX : 2 + }; + + return freezeObject(Cesium3DTileColorBlendMode); +}); diff --git a/Source/Scene/Cesium3DTileContent.js b/Source/Scene/Cesium3DTileContent.js new file mode 100644 index 000000000000..f39056a7c2be --- /dev/null +++ b/Source/Scene/Cesium3DTileContent.js @@ -0,0 +1,346 @@ +/*global define*/ +define([ + '../Core/defineProperties', + '../Core/DeveloperError' + ], function( + defineProperties, + DeveloperError) { + 'use strict'; + + /** + * The content of a tile in a {@link Cesium3DTileset}. + *

+ * Derived classes of this interface provide access to individual features in the tile. + * Access derived objects through {@link Cesium3DTile#content}. + *

+ *

+ * This type describes an interface and is not intended to be instantiated directly. + *

+ * + * @alias Cesium3DTileContent + * @constructor + */ + function Cesium3DTileContent(tileset, tile, url, arrayBuffer, byteOffset) { + /** + * Gets or sets if any feature's property changed. Used to + * optimized applying a style when a feature's property changed. + *

+ * This is used to implement the Cesium3DTileContent interface, but is + * not part of the public Cesium API. + *

+ * + * @type {Boolean} + * + * @private + */ + this.featurePropertiesDirty = false; + } + + defineProperties(Cesium3DTileContent.prototype, { + /** + * Gets the number of features in the tile. + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Number} + * @readonly + */ + featuresLength : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the number of points in the tile. + *

+ * Only applicable for tiles with Point Cloud content. This is different than {@link Cesium3DTileContent#featuresLength} which + * equals the number of groups of points as distinguished by the BATCH_ID feature table semantic. + *

+ * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/PointCloud/README.md#batched-points} + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Number} + * @readonly + */ + pointsLength : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the number of triangles in the tile. + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Number} + * @readonly + */ + trianglesLength : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the tile's geometry memory in bytes. + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Number} + * @readonly + */ + geometryByteLength : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the tile's texture memory in bytes. + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Number} + * @readonly + */ + texturesByteLength : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the amount of memory used by the batch table textures, in bytes. + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Number} + * @readonly + */ + batchTableByteLength : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the array of {@link Cesium3DTileContent} objects that represent the + * content a composite's inner tiles, which can also be composites. + * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Composite/README.md} + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Array} + * @readonly + */ + innerContents : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the promise that will be resolved when the tile's content is ready to render. + * + * @memberof Cesium3DTileContent.prototype + * + * @type {Promise.} + * @readonly + */ + readyPromise : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the tileset for this tile. + * + * @type {Cesium3DTileset} + * @readonly + */ + tileset : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the tile containing this content. + * + * @type {Cesium3DTile} + * @readonly + */ + tile : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the url of the tile's content. + * @memberof Cesium3DTileContent.prototype + * + * @type {String} + * @readonly + */ + url : { + get : function() { + DeveloperError.throwInstantiationError(); + } + }, + + /** + * Gets the batch table for this content. + *

+ * This is used to implement the Cesium3DTileContent interface, but is + * not part of the public Cesium API. + *

+ * + * @type {Cesium3DTileBatchTable} + * @readonly + * + * @private + */ + batchTable : { + get : function() { + DeveloperError.throwInstantiationError(); + } + } + }); + + /** + * Determines if the tile's batch table has a property. If it does, each feature in + * the tile will have the property. + * + * @param {Number} batchId The batchId for the feature. + * @param {String} name The case-sensitive name of the property. + * @returns {Boolean} true if the property exists; otherwise, false. + */ + Cesium3DTileContent.prototype.hasProperty = function(batchId, name) { + DeveloperError.throwInstantiationError(); + }; + + /** + * Returns the {@link Cesium3DTileFeature} object for the feature with the + * given batchId. This object is used to get and modify the + * feature's properties. + *

+ * Features in a tile are ordered by batchId, an index used to retrieve their metadata from the batch table. + *

+ * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/TileFormats/BatchTable}. + * + * @param {Number} batchId The batchId for the feature. + * @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object. + * + * @exception {DeveloperError} batchId must be between zero and {@link Cesium3DTileContent#featuresLength} - 1. + */ + Cesium3DTileContent.prototype.getFeature = function(batchId) { + DeveloperError.throwInstantiationError(); + }; + + /** + * Called when {@link Cesium3DTileset#debugColorizeTiles} changes. + *

+ * This is used to implement the Cesium3DTileContent interface, but is + * not part of the public Cesium API. + *

+ * + * @param {Boolean} enabled Whether to enable or disable debug settings. + * @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object. + + * @private + */ + Cesium3DTileContent.prototype.applyDebugSettings = function(enabled, color) { + DeveloperError.throwInstantiationError(); + }; + + /** + * Apply a style to the content + *

+ * This is used to implement the Cesium3DTileContent interface, but is + * not part of the public Cesium API. + *

+ * + * @param {FrameSate} frameState The frame state. + * @param {Cesium3DTileStyle} style The style. + * + * @private + */ + Cesium3DTileContent.prototype.applyStyle = function(frameState, style) { + DeveloperError.throwInstantiationError(); + }; + + /** + * Called by the tile during tileset traversal to get the draw commands needed to render this content. + * When the tile's content is in the PROCESSING state, this creates WebGL resources to ultimately + * move to the READY state. + *

+ * This is used to implement the Cesium3DTileContent interface, but is + * not part of the public Cesium API. + *

+ * + * @param {Cesium3DTileset} tileset The tileset containing this tile. + * @param {FrameState} frameState The frame state. + * + * @private + */ + Cesium3DTileContent.prototype.update = function(tileset, frameState) { + DeveloperError.throwInstantiationError(); + }; + + /** + * Returns true if this object was destroyed; otherwise, false. + *

+ * If this object was destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. + *

+ * This is used to implement the Cesium3DTileContent interface, but is + * not part of the public Cesium API. + *

+ * + * @returns {Boolean} true if this object was destroyed; otherwise, false. + * + * @see Cesium3DTileContent#destroy + * + * @private + */ + Cesium3DTileContent.prototype.isDestroyed = function() { + DeveloperError.throwInstantiationError(); + }; + + /** + * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic + * release of WebGL resources, instead of relying on the garbage collector to destroy this object. + *

+ * Once an object is destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. Therefore, + * assign the return value (undefined) to the object as done in the example. + *

+ * This is used to implement the Cesium3DTileContent interface, but is + * not part of the public Cesium API. + *

+ * + * @returns {undefined} + * + * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. + * + * @example + * content = content && content.destroy(); + * + * @see Cesium3DTileContent#isDestroyed + * + * @private + */ + Cesium3DTileContent.prototype.destroy = function() { + DeveloperError.throwInstantiationError(); + }; + + return Cesium3DTileContent; +}); diff --git a/Source/Scene/Cesium3DTileContentFactory.js b/Source/Scene/Cesium3DTileContentFactory.js new file mode 100644 index 000000000000..c90d50b4917b --- /dev/null +++ b/Source/Scene/Cesium3DTileContentFactory.js @@ -0,0 +1,41 @@ +/*global define*/ +define([ + './Batched3DModel3DTileContent', + './Composite3DTileContent', + './Instanced3DModel3DTileContent', + './PointCloud3DTileContent', + './Tileset3DTileContent' + ], function( + Batched3DModel3DTileContent, + Composite3DTileContent, + Instanced3DModel3DTileContent, + PointCloud3DTileContent, + Tileset3DTileContent) { + 'use strict'; + + /** + * Maps a tile's magic field in its header to a new content object for the tile's payload. + * + * @private + */ + var Cesium3DTileContentFactory = { + b3dm : function(tileset, tile, url, arrayBuffer, byteOffset) { + return new Batched3DModel3DTileContent(tileset, tile, url, arrayBuffer, byteOffset); + }, + pnts : function(tileset, tile, url, arrayBuffer, byteOffset) { + return new PointCloud3DTileContent(tileset, tile, url, arrayBuffer, byteOffset); + }, + i3dm : function(tileset, tile, url, arrayBuffer, byteOffset) { + return new Instanced3DModel3DTileContent(tileset, tile, url, arrayBuffer, byteOffset); + }, + cmpt : function(tileset, tile, url, arrayBuffer, byteOffset) { + // Send in the factory in order to avoid a cyclical dependency + return new Composite3DTileContent(tileset, tile, url, arrayBuffer, byteOffset, Cesium3DTileContentFactory); + }, + json : function(tileset, tile, url, arrayBuffer, byteOffset) { + return new Tileset3DTileContent(tileset, tile, url, arrayBuffer, byteOffset); + } + }; + + return Cesium3DTileContentFactory; +}); diff --git a/Source/Scene/Cesium3DTileContentState.js b/Source/Scene/Cesium3DTileContentState.js new file mode 100644 index 000000000000..98fdad9a96bf --- /dev/null +++ b/Source/Scene/Cesium3DTileContentState.js @@ -0,0 +1,21 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + 'use strict'; + + /** + * @private + */ + var Cesium3DTileContentState = { + UNLOADED : 0, // Has never been requested + LOADING : 1, // Is waiting on a pending request + PROCESSING : 2, // Request received. Contents are being processed for rendering. Depending on the content, it might make its own requests for external data. + READY : 3, // Ready to render. + EXPIRED : 4, // Is expired and will be unloaded once new content is loaded. + FAILED : 5 // Request failed. + }; + + return freezeObject(Cesium3DTileContentState); +}); diff --git a/Source/Scene/Cesium3DTileFeature.js b/Source/Scene/Cesium3DTileFeature.js new file mode 100644 index 000000000000..5e03d3fc4cf8 --- /dev/null +++ b/Source/Scene/Cesium3DTileFeature.js @@ -0,0 +1,270 @@ +/*global define*/ +define([ + '../Core/Color', + '../Core/defined', + '../Core/defineProperties' + ], function( + Color, + defined, + defineProperties) { + 'use strict'; + + /** + * A feature of a {@link Cesium3DTileset}. + *

+ * Provides access to a feature's properties stored in the tile's batch table, as well + * as the ability to show/hide a feature and change its highlight color via + * {@link Cesium3DTileFeature#show} and {@link Cesium3DTileFeature#color}, respectively. + *

+ *

+ * Modifications to a Cesium3DTileFeature object have the lifetime of the tile's + * content. If the tile's content is unloaded, e.g., due to it going out of view and needing + * to free space in the cache for visible tiles, listen to the {@link Cesium3DTileset#tileUnload} event to save any + * modifications. Also listen to the {@link Cesium3DTileset#tileVisible} event to reapply any modifications. + *

+ *

+ * Do not construct this directly. Access it through {@link Cesium3DTileContent#getFeature} + * or picking using {@link Scene#pick} and {@link Scene#pickPosition}. + *

+ * + * @alias Cesium3DTileFeature + * @constructor + * + * @example + * // On mouse over, display all the properties for a feature in the console log. + * handler.setInputAction(function(movement) { + * var feature = scene.pick(movement.endPosition); + * if (feature instanceof Cesium.Cesium3DTileFeature) { + * var propertyNames = feature.getPropertyNames(); + * var length = propertyNames.length; + * for (var i = 0; i < length; ++i) { + * var propertyName = propertyNames[i]; + * console.log(propertyName + ': ' + feature.getProperty(propertyName)); + * } + * } + * }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + */ + function Cesium3DTileFeature(tileset, content, batchId) { + this._content = content; + this._batchId = batchId; + this._color = undefined; // for calling getColor + } + + defineProperties(Cesium3DTileFeature.prototype, { + /** + * Gets or sets if the feature will be shown. This is set for all features + * when a style's show is evaluated. + * + * @memberof Cesium3DTileFeature.prototype + * + * @type {Boolean} + * + * @default true + */ + show : { + get : function() { + return this._content.batchTable.getShow(this._batchId); + }, + set : function(value) { + this._content.batchTable.setShow(this._batchId, value); + } + }, + + /** + * Gets or sets the highlight color multiplied with the feature's color. When + * this is white, the feature's color is not changed. This is set for all features + * when a style's color is evaluated. + * + * @memberof Cesium3DTileFeature.prototype + * + * @type {Color} + * + * @default {@link Color.WHITE} + */ + color : { + get : function() { + if (!defined(this._color)) { + this._color = new Color(); + } + return this._content.batchTable.getColor(this._batchId, this._color); + }, + set : function(value) { + this._content.batchTable.setColor(this._batchId, value); + } + }, + + /** + * Gets the content of the tile containing the feature. + * + * @memberof Cesium3DTileFeature.prototype + * + * @type {Cesium3DTileContent} + * + * @readonly + * @private + */ + content : { + get : function() { + return this._content; + } + }, + + /** + * Gets the tileset containing the feature. + * + * @memberof Cesium3DTileFeature.prototype + * + * @type {Cesium3DTileset} + * + * @readonly + */ + tileset : { + get : function() { + return this._content.tileset; + } + }, + + /** + * All objects returned by {@link Scene#pick} have a primitive property. This returns + * the tileset containing the feature. + * + * @memberof Cesium3DTileFeature.prototype + * + * @type {Cesium3DTileset} + * + * @readonly + */ + primitive : { + get : function() { + return this._content.tileset; + } + } + }); + + /** + * Returns whether the feature contains this property. This includes properties from this feature's + * class and inherited classes when using a batch table hierarchy. + * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/TileFormats/BatchTable#batch-table-hierarchy} + * + * @param {String} name The case-sensitive name of the property. + * @returns {Boolean} Whether the feature contains this property. + */ + Cesium3DTileFeature.prototype.hasProperty = function(name) { + return this._content.batchTable.hasProperty(this._batchId, name); + }; + + /** + * Returns an array of property names for the feature. This includes properties from this feature's + * class and inherited classes when using a batch table hierarchy. + * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/TileFormats/BatchTable#batch-table-hierarchy} + * + * @param {String[]} results An array into which to store the results. + * @returns {String[]} The names of the feature's properties. + */ + Cesium3DTileFeature.prototype.getPropertyNames = function(results) { + return this._content.batchTable.getPropertyNames(this._batchId, results); + }; + + /** + * Returns a copy of the value of the feature's property with the given name. This includes properties from this feature's + * class and inherited classes when using a batch table hierarchy. + * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/TileFormats/BatchTable#batch-table-hierarchy} + * + * @param {String} name The case-sensitive name of the property. + * @returns {*} The value of the property or undefined if the property does not exist. + * + * @example + * // Display all the properties for a feature in the console log. + * var propertyNames = feature.getPropertyNames(); + * var length = propertyNames.length; + * for (var i = 0; i < length; ++i) { + * var propertyName = propertyNames[i]; + * console.log(propertyName + ': ' + feature.getProperty(propertyName)); + * } + */ + Cesium3DTileFeature.prototype.getProperty = function(name) { + return this._content.batchTable.getProperty(this._batchId, name); + }; + + /** + * Sets the value of the feature's property with the given name. + *

+ * If a property with the given name doesn't exist, it is created. + *

+ * + * @param {String} name The case-sensitive name of the property. + * @param {*} value The value of the property that will be copied. + * + * @exception {DeveloperError} Inherited batch table hierarchy property is read only. + * + * @example + * var height = feature.getProperty('Height'); // e.g., the height of a building + * + * @example + * var name = 'clicked'; + * if (feature.getProperty(name)) { + * console.log('already clicked'); + * } else { + * feature.setProperty(name, true); + * console.log('first click'); + * } + */ + Cesium3DTileFeature.prototype.setProperty = function(name, value) { + this._content.batchTable.setProperty(this._batchId, name, value); + + // PERFORMANCE_IDEA: Probably overkill, but maybe only mark the tile dirty if the + // property is in one of the style's expressions or - if it can be done quickly - + // if the new property value changed the result of an expression. + this._content.featurePropertiesDirty = true; + }; + + /** + * Returns whether the feature's class name equals className. Unlike {@link Cesium3DTileFeature#isClass} + * this function only checks the feature's exact class and not inherited classes. + *

+ * This function returns false if no batch table hierarchy is present. + *

+ * + * @param {String} className The name to check against. + * @returns {Boolean} Whether the feature's class name equals className + * + * @private + */ + Cesium3DTileFeature.prototype.isExactClass = function(className) { + return this._content.batchTable.isExactClass(this._batchId, className); + }; + + /** + * Returns whether the feature's class or any inherited classes are named className. + *

+ * This function returns false if no batch table hierarchy is present. + *

+ * + * @param {String} className The name to check against. + * @returns {Boolean} Whether the feature's class or inherited classes are named className + * + * @private + */ + Cesium3DTileFeature.prototype.isClass = function(className) { + return this._content.batchTable.isClass(this._batchId, className); + }; + + /** + * Returns the feature's class name. + *

+ * This function returns undefined if no batch table hierarchy is present. + *

+ * + * @returns {String} The feature's class name. + * + * @private + */ + Cesium3DTileFeature.prototype.getExactClassName = function() { + return this._content.batchTable.getExactClassName(this._batchId); + }; + + return Cesium3DTileFeature; +}); diff --git a/Source/Scene/Cesium3DTileFeatureTable.js b/Source/Scene/Cesium3DTileFeatureTable.js new file mode 100644 index 000000000000..ff6283cd18e7 --- /dev/null +++ b/Source/Scene/Cesium3DTileFeatureTable.js @@ -0,0 +1,93 @@ +/*global define*/ +define([ + '../Core/ComponentDatatype', + '../Core/defaultValue', + '../Core/defined' + ], function( + ComponentDatatype, + defaultValue, + defined) { + 'use strict'; + + /** + * @private + */ + function Cesium3DTileFeatureTable(featureTableJson, featureTableBinary) { + this.json = featureTableJson; + this.buffer = featureTableBinary; + this._cachedTypedArrays = {}; + this.featuresLength = 0; + } + + function getTypedArrayFromBinary(featureTable, semantic, componentType, componentLength, count, byteOffset) { + var cachedTypedArrays = featureTable._cachedTypedArrays; + var typedArray = cachedTypedArrays[semantic]; + if (!defined(typedArray)) { + typedArray = ComponentDatatype.createArrayBufferView(componentType, featureTable.buffer.buffer, featureTable.buffer.byteOffset + byteOffset, count * componentLength); + cachedTypedArrays[semantic] = typedArray; + } + return typedArray; + } + + function getTypedArrayFromArray(featureTable, semantic, componentType, array) { + var cachedTypedArrays = featureTable._cachedTypedArrays; + var typedArray = cachedTypedArrays[semantic]; + if (!defined(typedArray)) { + typedArray = ComponentDatatype.createTypedArray(componentType, array); + cachedTypedArrays[semantic] = typedArray; + } + return typedArray; + } + + Cesium3DTileFeatureTable.prototype.getGlobalProperty = function(semantic, componentType, componentLength) { + var jsonValue = this.json[semantic]; + if (!defined(jsonValue)) { + return undefined; + } + + if (defined(jsonValue.byteOffset)) { + componentType = defaultValue(componentType, ComponentDatatype.UNSIGNED_INT); + componentLength = defaultValue(componentLength, 1); + return getTypedArrayFromBinary(this, semantic, componentType, componentLength, 1, jsonValue.byteOffset); + } + + return jsonValue; + }; + + Cesium3DTileFeatureTable.prototype.getPropertyArray = function(semantic, componentType, componentLength) { + var jsonValue = this.json[semantic]; + if (!defined(jsonValue)) { + return undefined; + } + + if (defined(jsonValue.byteOffset)) { + if (defined(jsonValue.componentType)) { + componentType = ComponentDatatype.fromName(jsonValue.componentType); + } + return getTypedArrayFromBinary(this, semantic, componentType, componentLength, this.featuresLength, jsonValue.byteOffset); + } + + return getTypedArrayFromArray(this, semantic, componentType, jsonValue); + }; + + Cesium3DTileFeatureTable.prototype.getProperty = function(semantic, componentType, componentLength, featureId, result) { + var jsonValue = this.json[semantic]; + if (!defined(jsonValue)) { + return undefined; + } + + var typedArray = this.getPropertyArray(semantic, componentType, componentLength); + + if (componentLength === 1) { + return typedArray[featureId]; + } + + for (var i = 0; i < componentLength; ++i) { + result[i] = typedArray[componentLength * featureId + i]; + } + + return result; + }; + + return Cesium3DTileFeatureTable; +}); diff --git a/Source/Scene/Cesium3DTileOptimizationHint.js b/Source/Scene/Cesium3DTileOptimizationHint.js new file mode 100644 index 000000000000..b76479cbc6f3 --- /dev/null +++ b/Source/Scene/Cesium3DTileOptimizationHint.js @@ -0,0 +1,22 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + 'use strict'; + + /** + * Hint defining optimization support for a 3D tile + * + * @exports Cesium3DTileOptimizationHint + * + * @private + */ + var Cesium3DTileOptimizationHint = { + NOT_COMPUTED: -1, + USE_OPTIMIZATION: 1, + SKIP_OPTIMIZATION: 0 + }; + + return freezeObject(Cesium3DTileOptimizationHint); +}); diff --git a/Source/Scene/Cesium3DTileOptimizations.js b/Source/Scene/Cesium3DTileOptimizations.js new file mode 100644 index 000000000000..60b040574792 --- /dev/null +++ b/Source/Scene/Cesium3DTileOptimizations.js @@ -0,0 +1,106 @@ +/*global define*/ +define([ + '../Core/Cartesian3', + '../Core/Check', + './Cesium3DTileOptimizationHint', + './TileBoundingRegion', + './TileOrientedBoundingBox' + ], function( + Cartesian3, + Check, + Cesium3DTileOptimizationHint, + TileBoundingRegion, + TileOrientedBoundingBox) { + 'use strict'; + + /** + * Utility functions for computing optimization hints for a {@link Cesium3DTileset}. + * + * @exports Cesium3DTileOptimizations + * + * @private + */ + var Cesium3DTileOptimizations = {}; + + var scratchAxis = new Cartesian3(); + + /** + * Evaluates support for the childrenWithinParent optimization. This is used to more tightly cull tilesets if + * children bounds are fully contained within the parent. Currently, support for the optimization only works for + * oriented bounding boxes, so both the child and parent tile must be either a {@link TileOrientedBoundingBox} or + * {@link TileBoundingRegion}. The purpose of this check is to prevent use of a culling optimization when the child + * bounds exceed those of the parent. If the child bounds are greater, it is more likely that the optimization will + * waste CPU cycles. Bounding spheres are not supported for the reason that the child bounds can very often be + * partially outside of the parent bounds. + * + * @param {Cesium3DTile} tile The tile to check. + * @returns {Boolean} Whether the childrenWithinParent optimization is supported. + */ + Cesium3DTileOptimizations.checkChildrenWithinParent = function(tile) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('tile', tile); + //>>includeEnd('debug'); + + var children = tile.children; + var length = children.length; + + // Check if the parent has an oriented bounding box. + var boundingVolume = tile._boundingVolume; + if (boundingVolume instanceof TileOrientedBoundingBox || boundingVolume instanceof TileBoundingRegion) { + var orientedBoundingBox = boundingVolume._orientedBoundingBox; + tile._optimChildrenWithinParent = Cesium3DTileOptimizationHint.USE_OPTIMIZATION; + for (var i = 0; i < length; ++i) { + var child = children[i]; + + // Check if the child has an oriented bounding box. + var childBoundingVolume = child._boundingVolume; + if (!(childBoundingVolume instanceof TileOrientedBoundingBox || childBoundingVolume instanceof TileBoundingRegion)) { + // Do not support if the parent and child both do not have oriented bounding boxes. + tile._optimChildrenWithinParent = Cesium3DTileOptimizationHint.SKIP_OPTIMIZATION; + break; + } + + var childOrientedBoundingBox = childBoundingVolume._orientedBoundingBox; + + // Compute the axis from the parent to the child. + var axis = Cartesian3.subtract(childOrientedBoundingBox.center, orientedBoundingBox.center, scratchAxis); + var axisLength = Cartesian3.magnitude(axis); + Cartesian3.divideByScalar(axis, axisLength, axis); + + // Project the bounding box of the parent onto the axis. Because the axis is a ray from the parent + // to the child, the projection parameterized along the ray will be (+/- proj1). + var proj1 = Math.abs(orientedBoundingBox.halfAxes[0] * axis.x) + + Math.abs(orientedBoundingBox.halfAxes[1] * axis.y) + + Math.abs(orientedBoundingBox.halfAxes[2] * axis.z) + + Math.abs(orientedBoundingBox.halfAxes[3] * axis.x) + + Math.abs(orientedBoundingBox.halfAxes[4] * axis.y) + + Math.abs(orientedBoundingBox.halfAxes[5] * axis.z) + + Math.abs(orientedBoundingBox.halfAxes[6] * axis.x) + + Math.abs(orientedBoundingBox.halfAxes[7] * axis.y) + + Math.abs(orientedBoundingBox.halfAxes[8] * axis.z); + + // Project the bounding box of the child onto the axis. Because the axis is a ray from the parent + // to the child, the projection parameterized along the ray will be (+/- proj2) + axis.length. + var proj2 = Math.abs(childOrientedBoundingBox.halfAxes[0] * axis.x) + + Math.abs(childOrientedBoundingBox.halfAxes[1] * axis.y) + + Math.abs(childOrientedBoundingBox.halfAxes[2] * axis.z) + + Math.abs(childOrientedBoundingBox.halfAxes[3] * axis.x) + + Math.abs(childOrientedBoundingBox.halfAxes[4] * axis.y) + + Math.abs(childOrientedBoundingBox.halfAxes[5] * axis.z) + + Math.abs(childOrientedBoundingBox.halfAxes[6] * axis.x) + + Math.abs(childOrientedBoundingBox.halfAxes[7] * axis.y) + + Math.abs(childOrientedBoundingBox.halfAxes[8] * axis.z); + + // If the child extends the parent's bounds, the optimization is not valid and we skip it. + if (proj1 <= proj2 + axisLength) { + tile._optimChildrenWithinParent = Cesium3DTileOptimizationHint.SKIP_OPTIMIZATION; + break; + } + } + } + + return tile._optimChildrenWithinParent === Cesium3DTileOptimizationHint.USE_OPTIMIZATION; + }; + + return Cesium3DTileOptimizations; +}); diff --git a/Source/Scene/Cesium3DTileRefine.js b/Source/Scene/Cesium3DTileRefine.js new file mode 100644 index 000000000000..8b3773ba8a4f --- /dev/null +++ b/Source/Scene/Cesium3DTileRefine.js @@ -0,0 +1,38 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + 'use strict'; + + /** + * The refinement approach for a tile. + *

+ * See the {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/schema/tile.schema.json|tile schema} + * in the 3D Tiles spec. + *

+ * + * @exports Cesium3DTileRefine + * + * @private + */ + var Cesium3DTileRefine = { + /** + * Render this tile and, if it doesn't meet the screen space error, also refine to its children. + * + * @type {Number} + * @constant + */ + ADD : 0, + + /** + * Render this tile or, if it doesn't meet the screen space error, refine to its descendants instead. + * + * @type {Number} + * @constant + */ + REPLACE : 1 + }; + + return freezeObject(Cesium3DTileRefine); +}); diff --git a/Source/Scene/Cesium3DTileStyle.js b/Source/Scene/Cesium3DTileStyle.js new file mode 100644 index 000000000000..be427eb3f483 --- /dev/null +++ b/Source/Scene/Cesium3DTileStyle.js @@ -0,0 +1,442 @@ +/*global define*/ +define([ + '../Core/clone', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/DeveloperError', + '../Core/loadJson', + '../Core/RequestScheduler', + '../ThirdParty/when', + './ConditionsExpression', + './Expression' + ], function( + clone, + defaultValue, + defined, + defineProperties, + DeveloperError, + loadJson, + RequestScheduler, + when, + ConditionsExpression, + Expression) { + 'use strict'; + + var DEFAULT_JSON_COLOR_EXPRESSION = 'color("#ffffff")'; + var DEFAULT_JSON_BOOLEAN_EXPRESSION = true; + var DEFAULT_JSON_NUMBER_EXPRESSION = 1.0; + + /** + * A style that is applied to a {@link Cesium3DTileset}. + *

+ * Evaluates an expression defined using the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language}. + *

+ * + * @alias Cesium3DTileStyle + * @constructor + * + * @param {String|Object} [style] The url of a style or an object defining a style. + * + * @example + * tileset.style = new Cesium.Cesium3DTileStyle({ + * color : { + * conditions : [ + * ['${Height} >= 100', 'color("purple", 0.5)'], + * ['${Height} >= 50', 'color("red")'], + * ['true', 'color("blue")'] + * ] + * }, + * show : '${Height} > 0', + * meta : { + * description : '"Building id ${id} has height ${Height}."' + * } + * }); + * + * @example + * tileset.style = new Cesium.Cesium3DTileStyle({ + * color : 'vec4(${Temperature})', + * pointSize : '${Temperature} * 2.0' + * }); + * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language} + */ + function Cesium3DTileStyle(style) { + this._style = undefined; + this._ready = false; + this._color = undefined; + this._show = undefined; + this._pointSize = undefined; + this._meta = undefined; + + this._colorShaderFunction = undefined; + this._showShaderFunction = undefined; + this._pointSizeShaderFunction = undefined; + this._colorShaderFunctionReady = false; + this._showShaderFunctionReady = false; + this._pointSizeShaderFunctionReady = false; + + var promise; + if (typeof style === 'string') { + promise = loadJson(style); + } else { + promise = when.resolve(style); + } + + var that = this; + this._readyPromise = promise.then(function(styleJson) { + setup(that, styleJson); + return that; + }); + } + + function setup(that, styleJson) { + that._style = clone(styleJson, true); + + styleJson = defaultValue(styleJson, defaultValue.EMPTY_OBJECT); + + that._colorShaderFunctionReady = !defined(styleJson.color); + that._showShaderFunctionReady = !defined(styleJson.show); + that._pointSizeShaderFunctionReady = !defined(styleJson.pointSize); + + var colorExpression = defaultValue(styleJson.color, DEFAULT_JSON_COLOR_EXPRESSION); + var showExpression = defaultValue(styleJson.show, DEFAULT_JSON_BOOLEAN_EXPRESSION); + var pointSizeExpression = defaultValue(styleJson.pointSize, DEFAULT_JSON_NUMBER_EXPRESSION); + + var expressions = styleJson.expressions; + + var color; + if (typeof colorExpression === 'string') { + color = new Expression(colorExpression, expressions); + } else if (defined(colorExpression.conditions)) { + color = new ConditionsExpression(colorExpression, expressions); + } + + that._color = color; + + var show; + if (typeof showExpression === 'boolean') { + show = new Expression(String(showExpression), expressions); + } else if (typeof showExpression === 'string') { + show = new Expression(showExpression, expressions); + } else if (defined(showExpression.conditions)) { + show = new ConditionsExpression(showExpression, expressions); + } + + that._show = show; + + var pointSize; + if (typeof pointSizeExpression === 'number') { + pointSize = new Expression(String(pointSizeExpression), expressions); + } else if (typeof pointSizeExpression === 'string') { + pointSize = new Expression(pointSizeExpression, expressions); + } else if (defined(pointSizeExpression.conditions)) { + pointSize = new ConditionsExpression(pointSizeExpression, expressions); + } + + that._pointSize = pointSize; + + var meta = {}; + if (defined(styleJson.meta)) { + var metaJson = defaultValue(styleJson.meta, defaultValue.EMPTY_OBJECT); + for (var property in metaJson) { + if (metaJson.hasOwnProperty(property)) { + meta[property] = new Expression(metaJson[property], expressions); + } + } + } + + that._meta = meta; + + that._ready = true; + } + + defineProperties(Cesium3DTileStyle.prototype, { + /** + * Gets the object defining the style using the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language}. + * + * @memberof Cesium3DTileStyle.prototype + * + * @type {Object} + * @readonly + * + * @default undefined + * + * @exception {DeveloperError} The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true. + */ + style : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this._ready) { + throw new DeveloperError('The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._style; + } + }, + + /** + * When true, the style is ready and its expressions can be evaluated. When + * a style is constructed with an object, as opposed to a url, this is true immediately. + * + * @memberof Cesium3DTileStyle.prototype + * + * @type {Boolean} + * @readonly + * + * @default false + */ + ready : { + get : function() { + return this._ready; + } + }, + + /** + * Gets the promise that will be resolved when the the style is ready and its expressions can be evaluated. + * + * @memberof Cesium3DTileStyle.prototype + * + * @type {Promise.} + * @readonly + */ + readyPromise : { + get : function() { + return this._readyPromise; + } + }, + + /** + * Gets or sets the {@link StyleExpression} object used to evaluate the style's show property. + *

+ * The expression must return or convert to a Boolean. + *

+ * + * @memberof Cesium3DTileStyle.prototype + * + * @type {StyleExpression} + * + * @exception {DeveloperError} The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true. + * + * @example + * var style = new Cesium3DTileStyle({ + * show : '(regExp("^Chest").test(${County})) && (${YearBuilt} >= 1970)' + * }); + * style.show.evaluate(frameState, feature); // returns true or false depending on the feature's properties + * + * @example + * var style = new Cesium.Cesium3DTileStyle(); + * // Override show expression with a custom function + * style.show = { + * evaluate : function(frameState, feature) { + * return true; + * } + * }; + */ + show : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this._ready) { + throw new DeveloperError('The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._show; + }, + set : function(value) { + this._showShaderFunctionReady = false; + this._show = value; + } + }, + + /** + * Gets or sets the {@link StyleExpression} object used to evaluate the style's color property. + *

+ * The expression must return a Color. + *

+ * + * @memberof Cesium3DTileStyle.prototype + * + * @type {StyleExpression} + * + * @exception {DeveloperError} The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true. + * + * @example + * var style = new Cesium3DTileStyle({ + * color : '(${Temperature} > 90) ? color("red") : color("white")' + * }); + * style.color.evaluateColor(frameState, feature, result); // returns a Cesium.Color object + * + * @example + * var style = new Cesium.Cesium3DTileStyle(); + * // Override color expression with a custom function + * style.color = { + * evaluateColor : function(frameState, feature, result) { + * return Cesium.Color.clone(Cesium.Color.WHITE, result); + * } + * }; + */ + color : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this._ready) { + throw new DeveloperError('The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._color; + }, + set : function(value) { + this._colorShaderFunctionReady = false; + this._color = value; + } + }, + + /** + * Gets or sets the {@link StyleExpression} object used to evaluate the style's pointSize property. + *

+ * The expression must return or convert to a Number. + *

+ * + * @memberof Cesium3DTileStyle.prototype + * + * @type {StyleExpression} + * + * @exception {DeveloperError} The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true. + * + * @example + * var style = new Cesium3DTileStyle({ + * pointSize : '(${Temperature} > 90) ? 2.0 : 1.0' + * }); + * style.pointSize.evaluate(frameState, feature); // returns a Number + * + * @example + * var style = new Cesium.Cesium3DTileStyle(); + * // Override pointSize expression with a custom function + * style.pointSize = { + * evaluate : function(frameState, feature) { + * return 1.0; + * } + * }; + */ + pointSize : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this._ready) { + throw new DeveloperError('The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._pointSize; + }, + set : function(value) { + this._pointSizeShaderFunctionReady = false; + this._pointSize = value; + } + }, + + /** + * Gets or sets the object containing application-specific expression that can be explicitly + * evaluated, e.g., for display in a UI. + * + * @memberof Cesium3DTileStyle.prototype + * + * @type {StyleExpression} + * + * @exception {DeveloperError} The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true. + * + * @example + * var style = new Cesium3DTileStyle({ + * meta : { + * description : '"Building id ${id} has height ${Height}."' + * } + * }); + * style.meta.description.evaluate(frameState, feature); // returns a String with the substituted variables + */ + meta : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this._ready) { + throw new DeveloperError('The style is not loaded. Use Cesium3DTileStyle.readyPromise or wait for Cesium3DTileStyle.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._meta; + }, + set : function(value) { + this._meta = value; + } + } + }); + + /** + * Gets the color shader function for this style. + * + * @param {String} functionName Name to give to the generated function. + * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. + * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. + * + * @returns {String} The shader function. + * + * @private + */ + Cesium3DTileStyle.prototype.getColorShaderFunction = function(functionName, attributePrefix, shaderState) { + if (this._colorShaderFunctionReady) { + // Return the cached result, may be undefined + return this._colorShaderFunction; + } + + this._colorShaderFunctionReady = true; + this._colorShaderFunction = this.color.getShaderFunction(functionName, attributePrefix, shaderState, 'vec4'); + return this._colorShaderFunction; + }; + + /** + * Gets the show shader function for this style. + * + * @param {String} functionName Name to give to the generated function. + * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. + * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. + * + * @returns {String} The shader function. + * + * @private + */ + Cesium3DTileStyle.prototype.getShowShaderFunction = function(functionName, attributePrefix, shaderState) { + if (this._showShaderFunctionReady) { + // Return the cached result, may be undefined + return this._showShaderFunction; + } + + this._showShaderFunctionReady = true; + this._showShaderFunction = this.show.getShaderFunction(functionName, attributePrefix, shaderState, 'bool'); + return this._showShaderFunction; + }; + + /** + * Gets the pointSize shader function for this style. + * + * @param {String} functionName Name to give to the generated function. + * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. + * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. + * + * @returns {String} The shader function. + * + * @private + */ + Cesium3DTileStyle.prototype.getPointSizeShaderFunction = function(functionName, attributePrefix, shaderState) { + if (this._pointSizeShaderFunctionReady) { + // Return the cached result, may be undefined + return this._pointSizeShaderFunction; + } + + this._pointSizeShaderFunctionReady = true; + this._pointSizeShaderFunction = this.pointSize.getShaderFunction(functionName, attributePrefix, shaderState, 'float'); + return this._pointSizeShaderFunction; + }; + + return Cesium3DTileStyle; +}); diff --git a/Source/Scene/Cesium3DTileStyleEngine.js b/Source/Scene/Cesium3DTileStyleEngine.js new file mode 100644 index 000000000000..a74bb5e536dc --- /dev/null +++ b/Source/Scene/Cesium3DTileStyleEngine.js @@ -0,0 +1,83 @@ +/*global define*/ +define([ + '../Core/defined', + '../Core/defineProperties' + ], function( + defined, + defineProperties) { + 'use strict'; + + /** + * @private + */ + function Cesium3DTileStyleEngine() { + this._style = undefined; // The style provided by the user + this._styleDirty = false; // true when the style is reassigned + this._lastStyleTime = 0; // The "time" when the last style was assigned + } + + defineProperties(Cesium3DTileStyleEngine.prototype, { + style : { + get : function() { + return this._style; + }, + set : function(value) { + this._style = value; + this._styleDirty = true; + } + } + }); + + Cesium3DTileStyleEngine.prototype.makeDirty = function() { + this._styleDirty = true; + }; + + Cesium3DTileStyleEngine.prototype.applyStyle = function(tileset, frameState) { + if (!tileset.ready) { + return; + } + + if (defined(this._style) && !this._style.ready) { + return; + } + + var styleDirty = this._styleDirty; + + if (frameState.passes.render) { + // Don't reset until the color pass, e.g., for mouse-over picking + this._styleDirty = false; + } + + if (styleDirty) { + // Increase "time", so the style is applied to all visible tiles + ++this._lastStyleTime; + } + + var lastStyleTime = this._lastStyleTime; + var statistics = tileset._statistics; + + // If a new style was assigned, loop through all the visible tiles; otherwise, loop through + // only the tiles that are newly visible, i.e., they are visible this frame, but were not + // visible last frame. In many cases, the newly selected tiles list will be short or empty. + var tiles = styleDirty ? tileset._selectedTiles : tileset._selectedTilesToStyle; + // PERFORMANCE_IDEA: does mouse-over picking basically trash this? We need to style on + // pick, for example, because a feature's show may be false. + + var length = tiles.length; + for (var i = 0; i < length; ++i) { + var tile = tiles[i]; + if (tile.selected && (tile.lastStyleTime !== lastStyleTime)) { + // Apply the style to this tile if it wasn't already applied because: + // 1) the user assigned a new style to the tileset + // 2) this tile is now visible, but it wasn't visible when the style was first assigned + var content = tile.content; + tile.lastStyleTime = lastStyleTime; + content.applyStyle(frameState, this._style); + statistics.numberOfFeaturesStyled += content.featuresLength; + ++statistics.numberOfTilesStyled; + } + } + }; + + return Cesium3DTileStyleEngine; +}); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js new file mode 100644 index 000000000000..2e4ecc2973ef --- /dev/null +++ b/Source/Scene/Cesium3DTileset.js @@ -0,0 +1,1800 @@ +/*global define*/ +define([ + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Cartographic', + '../Core/Check', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/DeveloperError', + '../Core/DoublyLinkedList', + '../Core/Event', + '../Core/getBaseUri', + '../Core/getExtensionFromUri', + '../Core/Heap', + '../Core/isDataUri', + '../Core/joinUrls', + '../Core/JulianDate', + '../Core/loadJson', + '../Core/ManagedArray', + '../Core/Math', + '../Core/Matrix4', + '../Core/RuntimeError', + '../Renderer/ClearCommand', + '../Renderer/Pass', + '../ThirdParty/when', + './Axis', + './Cesium3DTile', + './Cesium3DTileColorBlendMode', + './Cesium3DTileOptimizations', + './Cesium3DTileRefine', + './Cesium3DTilesetStatistics', + './Cesium3DTilesetTraversal', + './Cesium3DTileStyleEngine', + './LabelCollection', + './SceneMode', + './ShadowMode', + './TileBoundingRegion', + './TileBoundingSphere', + './TileOrientedBoundingBox' + ], function( + Cartesian2, + Cartesian3, + Cartographic, + Check, + defaultValue, + defined, + defineProperties, + destroyObject, + DeveloperError, + DoublyLinkedList, + Event, + getBaseUri, + getExtensionFromUri, + Heap, + isDataUri, + joinUrls, + JulianDate, + loadJson, + ManagedArray, + CesiumMath, + Matrix4, + RuntimeError, + ClearCommand, + Pass, + when, + Axis, + Cesium3DTile, + Cesium3DTileColorBlendMode, + Cesium3DTileOptimizations, + Cesium3DTileRefine, + Cesium3DTilesetStatistics, + Cesium3DTilesetTraversal, + Cesium3DTileStyleEngine, + LabelCollection, + SceneMode, + ShadowMode, + TileBoundingRegion, + TileBoundingSphere, + TileOrientedBoundingBox) { + 'use strict'; + + /** + * A {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles tileset}, + * used for streaming massive heterogeneous 3D geospatial datasets. + * + * @alias Cesium3DTileset + * @constructor + * + * @param {Object} options Object with the following properties: + * @param {String} options.url The url to a tileset.json file or to a directory containing a tileset.json file. + * @param {Boolean} [options.show=true] Determines if the tileset will be shown. + * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] A 4x4 transformation matrix that transforms the tileset's root tile. + * @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the tileset casts or receives shadows from each light source. + * @param {Number} [options.maximumScreenSpaceError=16] The maximum screen space error used to drive level of detail refinement. + * @param {Number} [options.maximumMemoryUsage=512] The maximum amount of memory in MB that can be used by the tileset. + * @param {Boolean} [options.cullWithChildrenBounds=true] Optimization option. Whether to cull tiles using the union of their children bounding volumes. + * @param {Boolean} [options.dynamicScreenSpaceError=false] Optimization option. Reduce the screen space error for tiles that are further away from the camera. + * @param {Number} [options.dynamicScreenSpaceErrorDensity=0.00278] Density used to adjust the dynamic screen space error, similar to fog density. + * @param {Number} [options.dynamicScreenSpaceErrorFactor=4.0] A factor used to increase the computed dynamic screen space error. + * @param {Number} [options.dynamicScreenSpaceErrorHeightFalloff=0.25] A ratio of the tileset's height at which the density starts to falloff. + * @param {Boolean} [options.skipLevelOfDetail=true] Optimization option. Determines if level of detail skipping should be applied during the traversal. + * @param {Number} [options.baseScreenSpaceError=1024] When skipLevelOfDetail is true, the screen space error that must be reached before skipping levels of detail. + * @param {Number} [options.skipScreenSpaceErrorFactor=16] When skipLevelOfDetail is true, a multiplier defining the minimum screen space error to skip. Used in conjunction with skipLevels to determine which tiles to load. + * @param {Number} [options.skipLevels=1] When skipLevelOfDetail is true, a constant defining the minimum number of levels to skip when loading tiles. When it is 0, no levels are skipped. Used in conjunction with skipScreenSpaceErrorFactor to determine which tiles to load. + * @param {Boolean} [options.immediatelyLoadDesiredLevelOfDetail=false] When skipLevelOfDetail is true, only tiles that meet the maximum screen space error will ever be downloaded. Skipping factors are ignored and just the desired tiles are loaded. + * @param {Boolean} [options.loadSiblings=false] When skipLevelOfDetail is true, determines whether siblings of visible tiles are always downloaded during traversal. + * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. + * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. + * @param {Boolean} [options.debugWireframe=false] For debugging only. When true, render's each tile's content as a wireframe. + * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile. + * @param {Boolean} [options.debugShowContentBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile's content. + * @param {Boolean} [options.debugShowViewerRequestVolume=false] For debugging only. When true, renders the viewer request volume for each tile. + * @param {Boolean} [options.debugShowGeometricError=false] For debugging only. When true, draws labels to indicate the geometric error of each tile. + * @param {Boolean} [options.debugShowRenderingStatistics=false] For debugging only. When true, draws labels to indicate the number of commands, points, triangles and features for each tile. + * @param {Boolean} [options.debugShowMemoryUsage=false] For debugging only. When true, draws labels to indicate the texture and geometry memory in megabytes used by each tile. + * + * @exception {DeveloperError} The tileset must be 3D Tiles version 0.0 or 1.0. See {@link https://github.com/AnalyticalGraphicsInc/3d-tiles#spec-status} + * + * @example + * var tileset = scene.primitives.add(new Cesium.Cesium3DTileset({ + * url : 'http://localhost:8002/tilesets/Seattle' + * })); + * + * @example + * // Common setting for the skipLevelOfDetail optimization + * var tileset = scene.primitives.add(new Cesium.Cesium3DTileset({ + * url : 'http://localhost:8002/tilesets/Seattle', + * skipLevelOfDetail : true, + * baseScreenSpaceError : 1024, + * skipScreenSpaceErrorFactor : 16, + * skipLevels : 1, + * immediatelyLoadDesiredLevelOfDetail : false, + * loadSiblings : false, + * cullWithChildrenBounds : true + * })); + * + * @example + * // Common settings for the dynamicScreenSpaceError optimization + * var tileset = scene.primitives.add(new Cesium.Cesium3DTileset({ + * url : 'http://localhost:8002/tilesets/Seattle', + * dynamicScreenSpaceError : true, + * dynamicScreenSpaceErrorDensity : 0.00278, + * dynamicScreenSpaceErrorFactor : 4.0, + * dynamicScreenSpaceErrorHeightFalloff : 0.25 + * })); + * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles specification} + */ + function Cesium3DTileset(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + + var url = options.url; + + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('options.url', url); + //>>includeEnd('debug'); + + var tilesetUrl; + var basePath; + + if (getExtensionFromUri(url) === 'json') { + tilesetUrl = url; + basePath = getBaseUri(url, true); + } else if (isDataUri(url)) { + tilesetUrl = url; + basePath = ''; + } else { + basePath = url; + tilesetUrl = joinUrls(basePath, 'tileset.json'); + } + + this._url = url; + this._basePath = basePath; + this._tilesetUrl = tilesetUrl; + this._root = undefined; + this._asset = undefined; // Metadata for the entire tileset + this._properties = undefined; // Metadata for per-model/point/etc properties + this._geometricError = undefined; // Geometric error when the tree is not rendered at all + this._gltfUpAxis = undefined; + this._processingQueue = []; + this._selectedTiles = []; + this._requestedTiles = []; + this._desiredTiles = new ManagedArray(); + this._selectedTilesToStyle = []; + this._loadTimestamp = undefined; + this._timeSinceLoad = 0.0; + + var replacementList = new DoublyLinkedList(); + + // [head, sentinel) -> tiles that weren't selected this frame and may be replaced + // (sentinel, tail] -> tiles that were selected this frame + this._replacementList = replacementList; // Tiles with content loaded. For cache management. + this._replacementSentinel = replacementList.add(); + this._trimTiles = false; + + this._cullWithChildrenBounds = defaultValue(options.cullWithChildrenBounds, true); + + this._hasMixedContent = false; + + this._baseTraversal = new Cesium3DTilesetTraversal.BaseTraversal(); + this._skipTraversal = new Cesium3DTilesetTraversal.SkipTraversal({ + selectionHeuristic : selectionHeuristic + }); + + this._backfaceCommands = new ManagedArray(); + + this._maximumScreenSpaceError = defaultValue(options.maximumScreenSpaceError, 16); + this._maximumMemoryUsage = defaultValue(options.maximumMemoryUsage, 512); + + this._styleEngine = new Cesium3DTileStyleEngine(); + + this._modelMatrix = defined(options.modelMatrix) ? Matrix4.clone(options.modelMatrix) : Matrix4.clone(Matrix4.IDENTITY); + + this._statistics = new Cesium3DTilesetStatistics(); + this._statisticsLastColor = new Cesium3DTilesetStatistics(); + this._statisticsLastPick = new Cesium3DTilesetStatistics(); + + this._tilesLoaded = false; + + this._tileDebugLabels = undefined; + + this._readyPromise = when.defer(); + + /** + * Optimization option. Whether the tileset should refine based on a dynamic screen space error. Tiles that are further + * away will be rendered with lower detail than closer tiles. This improves performance by rendering fewer + * tiles and making less requests, but may result in a slight drop in visual quality for tiles in the distance. + * The algorithm is biased towards "street views" where the camera is close to the ground plane of the tileset and looking + * at the horizon. In addition results are more accurate for tightly fitting bounding volumes like box and region. + * + * @type {Boolean} + * @default false + */ + this.dynamicScreenSpaceError = defaultValue(options.dynamicScreenSpaceError, false); + + /** + * A scalar that determines the density used to adjust the dynamic screen space error, similar to {@link Fog}. Increasing this + * value has the effect of increasing the maximum screen space error for all tiles, but in a non-linear fashion. + * The error starts at 0.0 and increases exponentially until a midpoint is reached, and then approaches 1.0 asymptotically. + * This has the effect of keeping high detail in the closer tiles and lower detail in the further tiles, with all tiles + * beyond a certain distance all roughly having an error of 1.0. + *

+ * The dynamic error is in the range [0.0, 1.0) and is multiplied by dynamicScreenSpaceErrorFactor to produce the + * final dynamic error. This dynamic error is then subtracted from the tile's actual screen space error. + *

+ *

+ * Increasing dynamicScreenSpaceErrorDensity has the effect of moving the error midpoint closer to the camera. + * It is analogous to moving fog closer to the camera. + *

+ * + * @type {Number} + * @default 0.00278 + */ + this.dynamicScreenSpaceErrorDensity = 0.00278; + + /** + * A factor used to increase the screen space error of tiles for dynamic screen space error. As this value increases less tiles + * are requested for rendering and tiles in the distance will have lower detail. If set to zero, the feature will be disabled. + * + * @type {Number} + * @default 4.0 + */ + this.dynamicScreenSpaceErrorFactor = 4.0; + + /** + * A ratio of the tileset's height at which the density starts to falloff. If the camera is below this height the + * full computed density is applied, otherwise the density falls off. This has the effect of higher density at + * street level views. + *

+ * Valid values are between 0.0 and 1.0. + *

+ * + * @type {Number} + * @default 0.25 + */ + this.dynamicScreenSpaceErrorHeightFalloff = 0.25; + + this._dynamicScreenSpaceErrorComputedDensity = 0.0; // Updated based on the camera position and direction + + /** + * Determines whether the tileset casts or receives shadows from each light source. + *

+ * Enabling shadows has a performance impact. A tileset that casts shadows must be rendered twice, once from the camera and again from the light's point of view. + *

+ *

+ * Shadows are rendered only when {@link Viewer#shadows} is true. + *

+ * + * @type {ShadowMode} + * @default ShadowMode.ENABLED + */ + this.shadows = defaultValue(options.shadows, ShadowMode.ENABLED); + + /** + * Determines if the tileset will be shown. + * + * @type {Boolean} + * @default true + */ + this.show = defaultValue(options.show, true); + + /** + * Defines how per-feature colors set from the Cesium API or declarative styling blend with the source colors from + * the original feature, e.g. glTF material or per-point color in the tile. + * + * @type {Cesium3DTileColorBlendMode} + * @default Cesium3DTileColorBlendMode.HIGHLIGHT + */ + this.colorBlendMode = Cesium3DTileColorBlendMode.HIGHLIGHT; + + /** + * Defines the value used to linearly interpolate between the source color and feature color when the {@link Cesium3DTileset#colorBlendMode} is MIX. + * A value of 0.0 results in the source color while a value of 1.0 results in the feature color, with any value in-between + * resulting in a mix of the source color and feature color. + * + * @type {Number} + * @default 0.5 + */ + this.colorBlendAmount = 0.5; + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * Determines if only the tiles from last frame should be used for rendering. This + * effectively "freezes" the tileset to the previous frame so it is possible to zoom + * out and see what was rendered. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugFreezeFrame = defaultValue(options.debugFreezeFrame, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, assigns a random color to each tile. This is useful for visualizing + * what models belong to what tiles, especially with additive refinement where models + * from parent tiles may be interleaved with models from child tiles. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugColorizeTiles = defaultValue(options.debugColorizeTiles, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders each tile's content as a wireframe. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugWireframe = defaultValue(options.debugWireframe, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders the bounding volume for each visible tile. The bounding volume is + * white if the tile's content has an explicit bounding volume; otherwise, it + * is red. Tiles that are not at final resolution are yellow. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowBoundingVolume = defaultValue(options.debugShowBoundingVolume, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders a blue bounding volume for each tile's content. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowContentBoundingVolume = defaultValue(options.debugShowContentBoundingVolume, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders the viewer request volume for each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowViewerRequestVolume = defaultValue(options.debugShowViewerRequestVolume, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, draws labels to indicate the geometric error of each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowGeometricError = defaultValue(options.debugShowGeometricError, false); + + this._tileDebugLabels = undefined; + this.debugPickedTileLabelOnly = false; + this.debugPickedTile = undefined; + this.debugPickPosition = undefined; + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, draws labels to indicate the number of commands, points, triangles and features of each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowRenderingStatistics = defaultValue(options.debugShowRenderingStatistics, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, draws labels to indicate the geometry and texture memory usage of each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowMemoryUsage = defaultValue(options.debugShowMemoryUsage, false); + + /** + * The event fired to indicate progress of loading new tiles. This event is fired when a new tile + * is requested, when a requested tile is finished downloading, and when a downloaded tile has been + * processed and is ready to render. + *

+ * The number of pending tile requests, numberOfPendingRequests, and number of tiles + * processing, numberOfTilesProcessing are passed to the event listener. + *

+ *

+ * This event is fired at the end of the frame after the scene is rendered. + *

+ * + * @type {Event} + * @default new Event() + * + * @example + * tileset.loadProgress.addEventListener(function(numberOfPendingRequests, numberOfTilesProcessing) { + * if ((numberOfPendingRequests === 0) && (numberOfTilesProcessing === 0)) { + * console.log('Stopped loading'); + * return; + * } + * + * console.log('Loading: requests: ' + numberOfPendingRequests + ', processing: ' + numberOfTilesProcessing); + * }); + */ + this.loadProgress = new Event(); + + /** + * The event fired to indicate that all tiles that meet the screen space error this frame are loaded. The tileset + * is completely loaded for this view. + *

+ * This event is fired at the end of the frame after the scene is rendered. + *

+ * + * @type {Event} + * @default new Event() + * + * @example + * tileset.allTilesLoaded.addEventListener(function() { + * console.log('All tiles are loaded'); + * }); + * + * @see Cesium3DTileset#tilesLoaded + */ + this.allTilesLoaded = new Event(); + + /** + * The event fired to indicate that a tile's content was unloaded. + *

+ * The unloaded {@link Cesium3DTile} is passed to the event listener. + *

+ *

+ * This event is fired immediately before the tile's content is unloaded while the frame is being + * rendered so that the event listener has access to the tile's content. Do not create + * or modify Cesium entities or primitives during the event listener. + *

+ * + * @type {Event} + * @default new Event() + * + * @example + * tileset.tileUnload.addEventListener(function(tile) { + * console.log('A tile was unloaded from the cache.'); + * }); + * + * @see Cesium3DTileset#maximumMemoryUsage + * @see Cesium3DTileset#trimLoadedTiles + */ + this.tileUnload = new Event(); + + /** + * This event fires once for each visible tile in a frame. This can be used to manually + * style a tileset. + *

+ * The visible {@link Cesium3DTile} is passed to the event listener. + *

+ *

+ * This event is fired during the tileset traversal while the frame is being rendered + * so that updates to the tile take effect in the same frame. Do not create or modify + * Cesium entities or primitives during the event listener. + *

+ * + * @type {Event} + * @default new Event() + * + * @example + * tileset.tileVisible.addEventListener(function(tile) { + * if (tile.content instanceof Cesium.Batched3DModel3DTileContent) { + * console.log('A Batched 3D Model tile is visible.'); + * } + * }); + * + * @example + * // Apply a red style and then manually set random colors for every other feature when the tile becomes visible. + * tileset.style = new Cesium.Cesium3DTileStyle({ + * color : 'color("red")' + * }); + * tileset.tileVisible.addEventListener(function(tile) { + * var content = tile.content; + * var featuresLength = content.featuresLength; + * for (var i = 0; i < featuresLength; i+=2) { + * content.getFeature(i).color = Cesium.Color.fromRandom(); + * } + * }); + */ + this.tileVisible = new Event(); + + /** + * Optimization option. Determines if level of detail skipping should be applied during the traversal. + *

+ * The common strategy for replacement-refinement traversal is to store all levels of the tree in memory and require + * all children to be loaded before the parent can refine. With this optimization levels of the tree can be skipped + * entirely and children can be rendered alongside their parents. The tileset requires significantly less memory when + * using this optimization. + *

+ * + * @type {Boolean} + * @default true + */ + this.skipLevelOfDetail = defaultValue(options.skipLevelOfDetail, true); + + /** + * The screen space error that must be reached before skipping levels of detail. + *

+ * Only used when {@link Cesium3DTileset#skipLevelOfDetail} is true. + *

+ * + * @type {Number} + * @default 1024 + */ + this.baseScreenSpaceError = defaultValue(options.baseScreenSpaceError, 1024); + + /** + * Multiplier defining the minimum screen space error to skip. + * For example, if a tile has screen space error of 100, no tiles will be loaded unless they + * are leaves or have a screen space error <= 100 / skipScreenSpaceErrorFactor. + *

+ * Only used when {@link Cesium3DTileset#skipLevelOfDetail} is true. + *

+ * + * @type {Number} + * @default 16 + */ + this.skipScreenSpaceErrorFactor = defaultValue(options.skipScreenSpaceErrorFactor, 16); + + /** + * Constant defining the minimum number of levels to skip when loading tiles. When it is 0, no levels are skipped. + * For example, if a tile is level 1, no tiles will be loaded unless they are at level greater than 2. + *

+ * Only used when {@link Cesium3DTileset#skipLevelOfDetail} is true. + *

+ * + * @type {Number} + * @default 1 + */ + this.skipLevels = defaultValue(options.skipLevels, 1); + + /** + * When true, only tiles that meet the maximum screen space error will ever be downloaded. + * Skipping factors are ignored and just the desired tiles are loaded. + *

+ * Only used when {@link Cesium3DTileset#skipLevelOfDetail} is true. + *

+ * + * @type {Boolean} + * @default false + */ + this.immediatelyLoadDesiredLevelOfDetail = defaultValue(options.immediatelyLoadDesiredLevelOfDetail, false); + + /** + * Determines whether siblings of visible tiles are always downloaded during traversal. + * This may be useful for ensuring that tiles are already available when the viewer turns left/right. + *

+ * Only used when {@link Cesium3DTileset#skipLevelOfDetail} is true. + *

+ * + * @type {Boolean} + * @default false + */ + this.loadSiblings = defaultValue(options.loadSiblings, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * Determines if only the tiles from last frame should be used for rendering. This + * effectively "freezes" the tileset to the previous frame so it is possible to zoom + * out and see what was rendered. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugFreezeFrame = defaultValue(options.debugFreezeFrame, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, assigns a random color to each tile. This is useful for visualizing + * what features belong to what tiles, especially with additive refinement where features + * from parent tiles may be interleaved with features from child tiles. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugColorizeTiles = defaultValue(options.debugColorizeTiles, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders each tile's content as a wireframe. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugWireframe = defaultValue(options.debugWireframe, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders the bounding volume for each visible tile. The bounding volume is + * white if the tile has a content bounding volume; otherwise, it is red. Tiles that don't meet the + * screen space error and are still refining to their descendants are yellow. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowBoundingVolume = defaultValue(options.debugShowBoundingVolume, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders the bounding volume for each visible tile's content. The bounding volume is + * blue if the tile has a content bounding volume; otherwise it is red. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowContentBoundingVolume = defaultValue(options.debugShowContentBoundingVolume, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, renders the viewer request volume for each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowViewerRequestVolume = defaultValue(options.debugShowViewerRequestVolume, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, draws labels to indicate the geometric error of each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowGeometricError = defaultValue(options.debugShowGeometricError, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, draws labels to indicate the number of commands, points, triangles and features of each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowRenderingStatistics = defaultValue(options.debugShowRenderingStatistics, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * When true, draws labels to indicate the geometry and texture memory usage of each tile. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowMemoryUsage = defaultValue(options.debugShowMemoryUsage, false); + + var that = this; + + // We don't know the distance of the tileset until tileset.json is loaded, so use the default distance for now + loadJson(tilesetUrl).then(function(tilesetJson) { + that._root = that.loadTileset(tilesetUrl, tilesetJson); + var gltfUpAxis = defined(tilesetJson.asset.gltfUpAxis) ? Axis.fromName(tilesetJson.asset.gltfUpAxis) : Axis.Y; + that._asset = tilesetJson.asset; + that._properties = tilesetJson.properties; + that._geometricError = tilesetJson.geometricError; + that._gltfUpAxis = gltfUpAxis; + that._readyPromise.resolve(that); + }).otherwise(function(error) { + that._readyPromise.reject(error); + }); + } + + defineProperties(Cesium3DTileset.prototype, { + /** + * Gets the tileset's asset object property, which contains metadata about the tileset. + *

+ * See the {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/schema/asset.schema.json|asset schema} + * in the 3D Tiles spec for the full set of properties. + *

+ * + * @memberof Cesium3DTileset.prototype + * + * @type {Object} + * @readonly + * + * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true. + */ + asset : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._asset; + } + }, + + /** + * Gets the tileset's properties dictionary object, which contains metadata about per-feature properties. + *

+ * See the {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/schema/properties.schema.json|properties schema} + * in the 3D Tiles spec for the full set of properties. + *

+ * + * @memberof Cesium3DTileset.prototype + * + * @type {Object} + * @readonly + * + * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true. + * + * @example + * console.log('Maximum building height: ' + tileset.properties.height.maximum); + * console.log('Minimum building height: ' + tileset.properties.height.minimum); + * + * @see Cesium3DTileFeature#getProperty + * @see Cesium3DTileFeature#setProperty + */ + properties : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._properties; + } + }, + + /** + * When true, the tileset's root tile is loaded and the tileset is ready to render. + * This is set to true right before {@link Cesium3DTileset#readyPromise} is resolved. + * + * @memberof Cesium3DTileset.prototype + * + * @type {Boolean} + * @readonly + * + * @default false + */ + ready : { + get : function() { + return defined(this._root); + } + }, + + /** + * Gets the promise that will be resolved when the tileset's root tile is loaded and the tileset is ready to render. + *

+ * This promise is resolved at the end of the frame before the first frame the tileset is rendered in. + *

+ * + * @memberof Cesium3DTileset.prototype + * + * @type {Promise.} + * @readonly + * + * @example + * tileset.readyPromise.then(function(tileset) { + * // tile.properties is not defined until readyPromise resolves. + * var properties = tileset.properties; + * if (Cesium.defined(properties)) { + * for (var name in properties) { + * console.log(properties[name]); + * } + * } + * }); + */ + readyPromise : { + get : function() { + return this._readyPromise.promise; + } + }, + + /** + * When true, all tiles that meet the screen space error this frame are loaded. The tileset is + * completely loaded for this view. + * + * @memberof Cesium3DTileset.prototype + * + * @type {Boolean} + * @readonly + * + * @default false + * + * @see Cesium3DTileset#allTilesLoaded + */ + tilesLoaded : { + get : function() { + return this._tilesLoaded; + } + }, + + /** + * The url to a tileset.json file or to a directory containing a tileset.json file. + * + * @memberof Cesium3DTileset.prototype + * + * @type {String} + * @readonly + */ + url : { + get : function() { + return this._url; + } + }, + + /** + * The base path that non-absolute paths in tileset.json are relative to. + * + * @memberof Cesium3DTileset.prototype + * + * @type {String} + * @readonly + */ + basePath : { + get : function() { + return this._basePath; + } + }, + + /** + * The style, defined using the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language}, + * applied to each feature in the tileset. + *

+ * Assign undefined to remove the style, which will restore the visual + * appearance of the tileset to its default when no style was applied. + *

+ *

+ * The style is applied to a tile before the {@link Cesium3DTileset#tileVisible} + * event is raised, so code in tileVisible can manually set a feature's + * properties (e.g. color and show) after the style is applied. When + * a new style is assigned any manually set properties are overwritten. + *

+ * + * @memberof Cesium3DTileset.prototype + * + * @type {Cesium3DTileStyle} + * + * @default undefined + * + * @example + * tileset.style = new Cesium.Cesium3DTileStyle({ + * color : { + * conditions : [ + * ['${Height} >= 100', 'color("purple", 0.5)'], + * ['${Height} >= 50', 'color("red")'], + * ['true', 'color("blue")'] + * ] + * }, + * show : '${Height} > 0', + * meta : { + * description : '"Building id ${id} has height ${Height}."' + * } + * }); + * + * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language} + */ + style : { + get : function() { + return this._styleEngine.style; + }, + set : function(value) { + this._styleEngine.style = value; + } + }, + + /** + * The maximum screen space error used to drive level of detail refinement. This value helps determine when a tile + * refines to its descendants, and therefore plays a major role in balancing performance with visual quality. + *

+ * A tile's screen space error is roughly equivalent to the number of pixels wide that would be drawn if a sphere with a + * radius equal to the tile's geometric error were rendered at the tile's position. If this value exceeds + * maximumScreenSpaceError the tile refines to its descendants. + *

+ *

+ * Depending on the tileset, maximumScreenSpaceError may need to be tweaked to achieve the right balance. + * Higher values provide better performance but lower visual quality. + *

+ * + * @memberof Cesium3DTileset.prototype + * + * @type {Number} + * @default 16 + * + * @exception {DeveloperError} maximumScreenSpaceError must be greater than or equal to zero. + */ + maximumScreenSpaceError : { + get : function() { + return this._maximumScreenSpaceError; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals('maximumScreenSpaceError', value, 0); + //>>includeEnd('debug'); + + this._maximumScreenSpaceError = value; + } + }, + + /** + * The maximum amount of GPU memory (in MB) that may be used to cache tiles. This value is estimated from + * geometry, textures, and batch table textures of loaded tiles. For point clouds, this value also + * includes per-point metadata. + *

+ * Tiles not in view are unloaded to enforce this. + *

+ *

+ * If decreasing this value results in unloading tiles, the tiles are unloaded the next frame. + *

+ *

+ * If tiles sized more than maximumMemoryUsage are needed + * to meet the desired screen space error, determined by {@link Cesium3DTileset#maximumScreenSpaceError}, + * for the current view, then the memory usage of the tiles loaded will exceed + * maximumMemoryUsage. For example, if the maximum is 256 MB, but + * 300 MB of tiles are needed to meet the screen space error, then 300 MB of tiles may be loaded. When + * these tiles go out of view, they will be unloaded. + *

+ * + * @memberof Cesium3DTileset.prototype + * + * @type {Number} + * @default 512 + * + * @exception {DeveloperError} maximumMemoryUsage must be greater than or equal to zero. + * @see Cesium3DTileset#totalMemoryUsageInBytes + */ + maximumMemoryUsage : { + get : function() { + return this._maximumMemoryUsage; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals('value', value, 0); + //>>includeEnd('debug'); + + this._maximumMemoryUsage = value; + } + }, + + /** + * The tileset's bounding sphere. + * + * @memberof Cesium3DTileset.prototype + * + * @type {BoundingSphere} + * @readonly + * + * @exception {DeveloperError} The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true. + * + * @example + * var tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({ + * url : 'http://localhost:8002/tilesets/Seattle' + * })); + * + * tileset.readyPromise.then(function(tileset) { + * // Set the camera to view the newly added tileset + * viewer.camera.viewBoundingSphere(tileset.boundingSphere, new Cesium.HeadingPitchRange(0, -0.5, 0)); + * }); + */ + boundingSphere : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.'); + } + //>>includeEnd('debug'); + + return this._root.boundingSphere; + } + }, + + /** + * A 4x4 transformation matrix that transforms the entire tileset. + * + * @memberof Cesium3DTileset.prototype + * + * @type {Matrix4} + * @default Matrix4.IDENTITY + * + * @example + * // Adjust a tileset's height from the globe's surface. + * var heightOffset = 20.0; + * var boundingSphere = tileset.boundingSphere; + * var cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center); + * var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0); + * var offset = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, heightOffset); + * var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3()); + * tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation); + */ + modelMatrix : { + get : function() { + return this._modelMatrix; + }, + set : function(value) { + this._modelMatrix = Matrix4.clone(value, this._modelMatrix); + if (defined(this._root)) { + // Update the root transform right away instead of waiting for the next update loop. + // Useful, for example, when setting the modelMatrix and then having the camera view the tileset. + this._root.updateTransform(this._modelMatrix); + } + } + }, + + /** + * Returns the time, in milliseconds, since the tileset was loaded and first updated. + * + * @memberof Cesium3DTileset.prototype + * + * @type {Number} + * @readonly + */ + timeSinceLoad : { + get : function() { + return this._timeSinceLoad; + } + }, + + /** + * The total amount of GPU memory in bytes used by the tileset. This value is estimated from + * geometry, texture, and batch table textures of loaded tiles. For point clouds, this value also + * includes per-point metadata. + * + * @memberof Cesium3DTileset.prototype + * + * @type {Number} + * @readonly + * + * @see Cesium3DTileset#maximumMemoryUsage + */ + totalMemoryUsageInBytes : { + get : function() { + var statistics = this._statistics; + return statistics.texturesByteLength + statistics.geometryByteLength + statistics.batchTableByteLength; + } + }, + + /** + * @private + */ + styleEngine : { + get : function() { + return this._styleEngine; + } + }, + + /** + * @private + */ + statistics : { + get : function() { + return this._statistics; + } + } + }); + + /** + * Marks the tileset's {@link Cesium3DTileset#style} as dirty, which forces all + * features to re-evaluate the style in the next frame each is visible. + */ + Cesium3DTileset.prototype.makeStyleDirty = function() { + this._styleEngine.makeDirty(); + }; + + /** + * Loads the main tileset.json or a tileset.json referenced from a tile. + * + * @private + */ + Cesium3DTileset.prototype.loadTileset = function(tilesetUrl, tilesetJson, parentTile) { + var asset = tilesetJson.asset; + if (!defined(asset)) { + throw new RuntimeError('Tileset must have an asset property.'); + } + if (asset.version !== '0.0' && asset.version !== '1.0') { + throw new RuntimeError('The tileset must be 3D Tiles version 0.0 or 1.0. See https://github.com/AnalyticalGraphicsInc/3d-tiles#spec-status'); + } + + var statistics = this._statistics; + + // Append the tileset version to the basePath + var hasVersionQuery = /[?&]v=/.test(tilesetUrl); + if (!hasVersionQuery) { + var versionQuery = '?v=' + defaultValue(asset.tilesetVersion, '0.0'); + this._basePath = joinUrls(this._basePath, versionQuery); + tilesetUrl = joinUrls(tilesetUrl, versionQuery, false); + } + + // A tileset.json referenced from a tile may exist in a different directory than the root tileset. + // Get the basePath relative to the external tileset. + var basePath = getBaseUri(tilesetUrl, true); + var rootTile = new Cesium3DTile(this, basePath, tilesetJson.root, parentTile); + + // If there is a parentTile, add the root of the currently loading tileset + // to parentTile's children, and update its _depth. + if (defined(parentTile)) { + parentTile.children.push(rootTile); + rootTile._depth = parentTile._depth + 1; + } + + ++statistics.numberOfTilesTotal; + + var stack = []; + stack.push({ + header : tilesetJson.root, + tile3D : rootTile + }); + + while (stack.length > 0) { + var tile = stack.pop(); + var tile3D = tile.tile3D; + var children = tile.header.children; + if (defined(children)) { + var length = children.length; + for (var i = 0; i < length; ++i) { + var childHeader = children[i]; + var childTile = new Cesium3DTile(this, basePath, childHeader, tile3D); + tile3D.children.push(childTile); + childTile._depth = tile3D._depth + 1; + ++statistics.numberOfTilesTotal; + stack.push({ + header : childHeader, + tile3D : childTile + }); + } + } + + if (this._cullWithChildrenBounds) { + Cesium3DTileOptimizations.checkChildrenWithinParent(tile3D); + } + } + + return rootTile; + }; + + var scratchPositionNormal = new Cartesian3(); + var scratchCartographic = new Cartographic(); + var scratchMatrix = new Matrix4(); + var scratchCenter = new Cartesian3(); + var scratchPosition = new Cartesian3(); + var scratchDirection = new Cartesian3(); + + function updateDynamicScreenSpaceError(tileset, frameState) { + var up; + var direction; + var height; + var minimumHeight; + var maximumHeight; + + var camera = frameState.camera; + var root = tileset._root; + var tileBoundingVolume = root.contentBoundingVolume; + + if (tileBoundingVolume instanceof TileBoundingRegion) { + up = Cartesian3.normalize(camera.positionWC, scratchPositionNormal); + direction = camera.directionWC; + height = camera.positionCartographic.height; + minimumHeight = tileBoundingVolume.minimumHeight; + maximumHeight = tileBoundingVolume.maximumHeight; + } else { + // Transform camera position and direction into the local coordinate system of the tileset + var transformLocal = Matrix4.inverseTransformation(root.computedTransform, scratchMatrix); + var ellipsoid = frameState.mapProjection.ellipsoid; + var boundingVolume = tileBoundingVolume.boundingVolume; + var centerLocal = Matrix4.multiplyByPoint(transformLocal, boundingVolume.center, scratchCenter); + if (Cartesian3.magnitude(centerLocal) > ellipsoid.minimumRadius) { + // The tileset is defined in WGS84. Approximate the minimum and maximum height. + var centerCartographic = Cartographic.fromCartesian(centerLocal, ellipsoid, scratchCartographic); + up = Cartesian3.normalize(camera.positionWC, scratchPositionNormal); + direction = camera.directionWC; + height = camera.positionCartographic.height; + minimumHeight = 0.0; + maximumHeight = centerCartographic.height * 2.0; + } else { + // The tileset is defined in local coordinates (z-up) + var positionLocal = Matrix4.multiplyByPoint(transformLocal, camera.positionWC, scratchPosition); + up = Cartesian3.UNIT_Z; + direction = Matrix4.multiplyByPointAsVector(transformLocal, camera.directionWC, scratchDirection); + direction = Cartesian3.normalize(direction, direction); + height = positionLocal.z; + if (tileBoundingVolume instanceof TileOrientedBoundingBox) { + // Assuming z-up, the last component stores the half-height of the box + var boxHeight = root._header.boundingVolume.box[11]; + minimumHeight = centerLocal.z - boxHeight; + maximumHeight = centerLocal.z + boxHeight; + } else if (tileBoundingVolume instanceof TileBoundingSphere) { + var radius = boundingVolume.radius; + minimumHeight = centerLocal.z - radius; + maximumHeight = centerLocal.z + radius; + } + } + } + + // The range where the density starts to lessen. Start at the quarter height of the tileset. + var heightFalloff = tileset.dynamicScreenSpaceErrorHeightFalloff; + var heightClose = minimumHeight + (maximumHeight - minimumHeight) * heightFalloff; + var heightFar = maximumHeight; + + var t = CesiumMath.clamp((height - heightClose) / (heightFar - heightClose), 0.0, 1.0); + + // Increase density as the camera tilts towards the horizon + var dot = Math.abs(Cartesian3.dot(direction, up)); + var horizonFactor = 1.0 - dot; + + // Weaken the horizon factor as the camera height increases, implying the camera is further away from the tileset. + // The goal is to increase density for the "street view", not when viewing the tileset from a distance. + horizonFactor = horizonFactor * (1.0 - t); + + var density = tileset.dynamicScreenSpaceErrorDensity; + density *= horizonFactor; + + tileset._dynamicScreenSpaceErrorComputedDensity = density; + } + + function selectionHeuristic(tileset, ancestor, tile) { + var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; + var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; + + return (ancestor !== tile && !tile.hasEmptyContent && !tileset.immediatelyLoadDesiredLevelOfDetail) && + (tile._screenSpaceError < ancestor._screenSpaceError / skipScreenSpaceErrorFactor) && + (tile._depth > ancestor._depth + skipLevels); + } + + /////////////////////////////////////////////////////////////////////////// + + function requestContent(tileset, tile) { + if (tile.hasEmptyContent) { + return; + } + + var statistics = tileset._statistics; + var expired = tile.contentExpired; + var requested = tile.requestContent(); + + if (!requested) { + ++statistics.numberOfAttemptedRequests; + return; + } + + if (expired) { + if (tile.hasRenderableContent) { + statistics.decrementLoadCounts(tile.content); + --tileset._statistics.numberOfTilesWithContentReady; + } else if (tile.hasTilesetContent) { + destroySubtree(tileset, tile); + } + } + + ++statistics.numberOfPendingRequests; + + var removeFunction = removeFromProcessingQueue(tileset, tile); + tile.contentReadyToProcessPromise.then(addToProcessingQueue(tileset, tile)); + tile.contentReadyPromise.then(removeFunction).otherwise(removeFunction); + } + + function requestTiles(tileset, outOfCore) { + if (!outOfCore) { + return; + } + var requestedTiles = tileset._requestedTiles; + var length = requestedTiles.length; + for (var i = 0; i < length; ++i) { + requestContent(tileset, requestedTiles[i]); + } + } + + function addToProcessingQueue(tileset, tile) { + return function() { + tileset._processingQueue.push(tile); + + --tileset._statistics.numberOfPendingRequests; + ++tileset._statistics.numberOfTilesProcessing; + }; + } + + function removeFromProcessingQueue(tileset, tile) { + return function() { + var index = tileset._processingQueue.indexOf(tile); + if (index === -1) { + // Not in processing queue + // For example, when a url request fails and the ready promise is rejected + --tileset._statistics.numberOfPendingRequests; + return; + } + + // Remove from processing queue + tileset._processingQueue.splice(index, 1); + --tileset._statistics.numberOfTilesProcessing; + + if (tile.hasRenderableContent) { + // RESEARCH_IDEA: ability to unload tiles (without content) for an + // external tileset when all the tiles are unloaded. + tileset._statistics.incrementLoadCounts(tile.content); + ++tileset._statistics.numberOfTilesWithContentReady; + + // Add to the tile cache. Previously expired tiles are already in the cache. + if (!defined(tile.replacementNode)) { + tile.replacementNode = tileset._replacementList.add(tile); + } + } + }; + } + + function processTiles(tileset, frameState) { + var tiles = tileset._processingQueue; + var length = tiles.length; + + // Process tiles in the PROCESSING state so they will eventually move to the READY state. + // Traverse backwards in case a tile is removed as a result of calling process() + for (var i = length - 1; i >= 0; --i) { + tiles[i].process(tileset, frameState); + } + } + + /////////////////////////////////////////////////////////////////////////// + + var scratchCartesian = new Cartesian3(); + + var stringOptions = { + maximumFractionDigits : 3 + }; + + function formatMemoryString(memorySizeInBytes) { + var memoryInMegabytes = memorySizeInBytes / 1048576; + if (memoryInMegabytes < 1.0) { + return memoryInMegabytes.toLocaleString(undefined, stringOptions); + } + return Math.round(memoryInMegabytes).toLocaleString(); + } + + function computeTileLabelPosition(tile) { + var boundingVolume = tile._boundingVolume.boundingVolume; + var halfAxes = boundingVolume.halfAxes; + var radius = boundingVolume.radius; + + var position = Cartesian3.clone(boundingVolume.center, scratchCartesian); + if (defined(halfAxes)) { + position.x += 0.75 * (halfAxes[0] + halfAxes[3] + halfAxes[6]); + position.y += 0.75 * (halfAxes[1] + halfAxes[4] + halfAxes[7]); + position.z += 0.75 * (halfAxes[2] + halfAxes[5] + halfAxes[8]); + } else if (defined(radius)) { + var normal = Cartesian3.normalize(boundingVolume.center, scratchCartesian); + normal = Cartesian3.multiplyByScalar(normal, 0.75 * radius, scratchCartesian); + position = Cartesian3.add(normal, boundingVolume.center, scratchCartesian); + } + return position; + } + + function addTileDebugLabel(tile, tileset, position) { + var labelString = ''; + var attributes = 0; + + if (tileset.debugShowGeometricError) { + labelString += '\nGeometric error: ' + tile.geometricError; + attributes++; + } + + if (tileset.debugShowRenderingStatistics) { + labelString += '\nCommands: ' + tile.commandsLength; + attributes++; + + // Don't display number of points or triangles if 0. + var numberOfPoints = tile.content.pointsLength; + if (numberOfPoints > 0) { + labelString += '\nPoints: ' + tile.content.pointsLength; + attributes++; + } + + var numberOfTriangles = tile.content.trianglesLength; + if (numberOfTriangles > 0) { + labelString += '\nTriangles: ' + tile.content.trianglesLength; + attributes++; + } + + labelString += '\nFeatures: ' + tile.content.featuresLength; + attributes ++; + } + + if (tileset.debugShowMemoryUsage) { + labelString += '\nTexture Memory: ' + formatMemoryString(tile.content.texturesByteLength); + labelString += '\nGeometry Memory: ' + formatMemoryString(tile.content.geometryByteLength); + attributes += 2; + } + + var newLabel = { + text : labelString.substring(1), + position : position, + font : (19-attributes) + 'px sans-serif', + showBackground : true, + disableDepthTestDistance : Number.POSITIVE_INFINITY + }; + + return tileset._tileDebugLabels.add(newLabel); + } + + function updateTileDebugLabels(tileset, frameState) { + var selectedTiles = tileset._selectedTiles; + var length = selectedTiles.length; + tileset._tileDebugLabels.removeAll(); + + if (tileset.debugPickedTileLabelOnly) { + if (defined(tileset.debugPickedTile)) { + var position = (defined(tileset.debugPickPosition)) ? tileset.debugPickPosition : computeTileLabelPosition(tileset.debugPickedTile); + var label = addTileDebugLabel(tileset.debugPickedTile, tileset, position); + label.pixelOffset = new Cartesian2(15, -15); // Offset to avoid picking the label. + } + } else { + for (var i = 0; i < length; ++i) { + var tile = selectedTiles[i]; + addTileDebugLabel(tile, tileset, computeTileLabelPosition(tile)); + } + } + tileset._tileDebugLabels.update(frameState); + } + + var stencilClearCommand = new ClearCommand({ + stencil : 0, + pass : Pass.CESIUM_3D_TILE + }); + + function updateTiles(tileset, frameState) { + tileset._styleEngine.applyStyle(tileset, frameState); + + var statistics = tileset._statistics; + var commandList = frameState.commandList; + var numberOfInitialCommands = commandList.length; + var selectedTiles = tileset._selectedTiles; + var length = selectedTiles.length; + var tileVisible = tileset.tileVisible; + var i; + + var bivariateVisibilityTest = tileset.skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && length > 0; + + tileset._backfaceCommands.length = 0; + + if (bivariateVisibilityTest) { + commandList.push(stencilClearCommand); + } + + var lengthBeforeUpdate = commandList.length; + for (i = 0; i < length; ++i) { + var tile = selectedTiles[i]; + // tiles may get unloaded and destroyed between selection and update + if (tile.selected) { + // Raise the tileVisible event before update in case the tileVisible event + // handler makes changes that update needs to apply to WebGL resources + tileVisible.raiseEvent(tile); + tile.update(tileset, frameState); + statistics.incrementSelectionCounts(tile.content); + ++statistics.selected; + } + } + var lengthAfterUpdate = commandList.length; + + tileset._backfaceCommands.trim(); + + if (bivariateVisibilityTest) { + /** + * Consider 'effective leaf' tiles as selected tiles that have no selected descendants. They may have children, + * but they are currently our effective leaves because they do not have selected descendants. These tiles + * are those where with tile._finalResolution === true. + * Let 'unresolved' tiles be those with tile._finalResolution === false. + * + * 1. Render just the backfaces of unresolved tiles in order to lay down z + * 2. Render all frontfaces wherever tile._selectionDepth > stencilBuffer. + * Replace stencilBuffer with tile._selectionDepth, when passing the z test. + * Because children are always drawn before ancestors {@link Cesium3DTilesetTraversal#traverseAndSelect}, + * this effectively draws children first and does not draw ancestors if a descendant has already + * been drawn at that pixel. + * Step 1 prevents child tiles from appearing on top when they are truly behind ancestor content. + * If they are behind the backfaces of the ancestor, then they will not be drawn. + * + * NOTE: Step 2 sometimes causes visual artifacts when backfacing child content has some faces that + * partially face the camera and are inside of the ancestor content. Because they are inside, they will + * not be culled by the depth writes in Step 1, and because they partially face the camera, the stencil tests + * will draw them on top of the ancestor content. + * + * NOTE: Because we always render backfaces of unresolved tiles, if the camera is looking at the backfaces + * of an object, they will always be drawn while loading, even if backface culling is enabled. + */ + + var backfaceCommands = tileset._backfaceCommands.values; + var addedCommandsLength = (lengthAfterUpdate - lengthBeforeUpdate); + var backfaceCommandsLength = backfaceCommands.length; + + commandList.length += backfaceCommands.length; + + // copy commands to the back of the commandList + for (i = addedCommandsLength - 1; i >= 0; --i) { + commandList[lengthBeforeUpdate + backfaceCommandsLength + i] = commandList[lengthBeforeUpdate + i]; + } + + // move backface commands to the front of the commandList + for (i = 0; i < backfaceCommandsLength; ++i) { + commandList[lengthBeforeUpdate + i] = backfaceCommands[i]; + } + } + + // Number of commands added by each update above + statistics.numberOfCommands = (commandList.length - numberOfInitialCommands); + + if (tileset.debugShowGeometricError || tileset.debugShowRenderingStatistics || tileset.debugShowMemoryUsage) { + if (!defined(tileset._tileDebugLabels)) { + tileset._tileDebugLabels = new LabelCollection(); + } + updateTileDebugLabels(tileset, frameState); + } else { + tileset._tileDebugLabels = tileset._tileDebugLabels && tileset._tileDebugLabels.destroy(); + } + } + + function destroySubtree(tileset, tile) { + var root = tile; + var statistics = tileset._statistics; + var stack = scratchStack; + stack.push(tile); + while (stack.length > 0) { + tile = stack.pop(); + var children = tile.children; + var length = children.length; + for (var i = 0; i < length; ++i) { + stack.push(children[i]); + } + if (tile !== root) { + unloadTileFromCache(tileset, tile); + tile.destroy(); + --statistics.numberOfTilesTotal; + } + } + root.children = []; + } + + function unloadTileFromCache(tileset, tile) { + var node = tile.replacementNode; + if (!defined(node)) { + return; + } + + var statistics = tileset._statistics; + var replacementList = tileset._replacementList; + var tileUnload = tileset.tileUnload; + + tileUnload.raiseEvent(tile); + replacementList.remove(node); + statistics.decrementLoadCounts(tile.content); + --statistics.numberOfTilesWithContentReady; + } + + function unloadTiles(tileset) { + var trimTiles = tileset._trimTiles; + tileset._trimTiles = false; + + var replacementList = tileset._replacementList; + + var totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; + var maximumMemoryUsageInBytes = tileset._maximumMemoryUsage * 1024 * 1024; + + // Traverse the list only to the sentinel since tiles/nodes to the + // right of the sentinel were used this frame. + // + // The sub-list to the left of the sentinel is ordered from LRU to MRU. + var sentinel = tileset._replacementSentinel; + var node = replacementList.head; + while ((node !== sentinel) && ((totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { + var tile = node.item; + node = node.next; + unloadTileFromCache(tileset, tile); + tile.unloadContent(); + totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; + } + } + + /** + * Unloads all tiles that weren't selected the previous frame. This can be used to + * explicitly manage the tile cache and reduce the total number of tiles loaded below + * {@link Cesium3DTileset#maximumMemoryUsage}. + *

+ * Tile unloads occur at the next frame to keep all the WebGL delete calls + * within the render loop. + *

+ */ + Cesium3DTileset.prototype.trimLoadedTiles = function() { + // Defer to next frame so WebGL delete calls happen inside the render loop + this._trimTiles = true; + }; + + /////////////////////////////////////////////////////////////////////////// + + function raiseLoadProgressEvent(tileset, frameState) { + var statistics = tileset._statistics; + var statisticsLast = tileset._statisticsLastColor; + var numberOfPendingRequests = statistics.numberOfPendingRequests; + var numberOfTilesProcessing = statistics.numberOfTilesProcessing; + var lastNumberOfPendingRequest = statisticsLast.numberOfPendingRequests; + var lastNumberOfTilesProcessing = statisticsLast.numberOfTilesProcessing; + + var progressChanged = (numberOfPendingRequests !== lastNumberOfPendingRequest) || (numberOfTilesProcessing !== lastNumberOfTilesProcessing); + + if (progressChanged) { + frameState.afterRender.push(function() { + tileset.loadProgress.raiseEvent(numberOfPendingRequests, numberOfTilesProcessing); + }); + } + + tileset._tilesLoaded = (statistics.numberOfPendingRequests === 0) && (statistics.numberOfTilesProcessing === 0) && (statistics.numberOfAttemptedRequests === 0); + + if (progressChanged && tileset._tilesLoaded) { + frameState.afterRender.push(function() { + tileset.allTilesLoaded.raiseEvent(); + }); + } + } + + /////////////////////////////////////////////////////////////////////////// + + /** + * Called when {@link Viewer} or {@link CesiumWidget} render the scene to + * get the draw commands needed to render this primitive. + *

+ * Do not call this function directly. This is documented just to + * list the exceptions that may be propagated when the scene is rendered: + *

+ */ + Cesium3DTileset.prototype.update = function(frameState) { + if (frameState.mode === SceneMode.MORPHING) { + return; + } + + if (!this.show || !this.ready) { + return; + } + + if (!defined(this._loadTimestamp)) { + this._loadTimestamp = JulianDate.clone(frameState.time); + } + + this._timeSinceLoad = Math.max(JulianDate.secondsDifference(frameState.time, this._loadTimestamp) * 1000, 0.0); + + // Do not do out-of-core operations (new content requests, cache removal, + // process new tiles) during the pick pass. + var passes = frameState.passes; + var isPick = (passes.pick && !passes.render); + var outOfCore = !isPick; + + var statistics = this._statistics; + statistics.clear(); + + if (outOfCore) { + processTiles(this, frameState); + } + + if (this.dynamicScreenSpaceError) { + updateDynamicScreenSpaceError(this, frameState); + } + + Cesium3DTilesetTraversal.selectTiles(this, frameState, outOfCore); + requestTiles(this, outOfCore); + updateTiles(this, frameState); + + if (outOfCore) { + unloadTiles(this); + } + + // Events are raised (added to the afterRender queue) here since promises + // may resolve outside of the update loop that then raise events, e.g., + // model's readyPromise. + raiseLoadProgressEvent(this, frameState); + + // Update last statistics + var statisticsLast = isPick ? this._statisticsLastPick : this._statisticsLastColor; + Cesium3DTilesetStatistics.clone(statistics, statisticsLast); + }; + + /** + * Returns true if this object was destroyed; otherwise, false. + *

+ * If this object was destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. + * + * @returns {Boolean} true if this object was destroyed; otherwise, false. + * + * @see Cesium3DTileset#destroy + */ + Cesium3DTileset.prototype.isDestroyed = function() { + return false; + }; + + var scratchStack = []; + + /** + * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic + * release of WebGL resources, instead of relying on the garbage collector to destroy this object. + *

+ * Once an object is destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. Therefore, + * assign the return value (undefined) to the object as done in the example. + * + * @returns {undefined} + * + * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. + * + * @example + * tileset = tileset && tileset.destroy(); + * + * @see Cesium3DTileset#isDestroyed + */ + Cesium3DTileset.prototype.destroy = function() { + // Destroy debug labels + this._tileDebugLabels = this._tileDebugLabels && this._tileDebugLabels.destroy(); + + // Traverse the tree and destroy all tiles + if (defined(this._root)) { + var stack = scratchStack; + stack.push(this._root); + + while (stack.length > 0) { + var tile = stack.pop(); + tile.destroy(); + + var children = tile.children; + var length = children.length; + for (var i = 0; i < length; ++i) { + stack.push(children[i]); + } + } + } + + this._root = undefined; + return destroyObject(this); + }; + + return Cesium3DTileset; +}); diff --git a/Source/Scene/Cesium3DTilesetStatistics.js b/Source/Scene/Cesium3DTilesetStatistics.js new file mode 100644 index 000000000000..85442f395427 --- /dev/null +++ b/Source/Scene/Cesium3DTilesetStatistics.js @@ -0,0 +1,117 @@ +/*global define*/ +define([ + '../Core/defined' + ], function( + defined) { + 'use strict'; + + /** + * @private + */ + function Cesium3DTilesetStatistics() { + // Rendering statistics + this.selected = 0; + this.visited = 0; + // Loading statistics + this.numberOfCommands = 0; + this.numberOfAttemptedRequests = 0; + this.numberOfPendingRequests = 0; + this.numberOfTilesProcessing = 0; + this.numberOfTilesWithContentReady = 0; // Number of tiles with content loaded, does not include empty tiles + this.numberOfTilesTotal = 0; // Number of tiles in tileset.json (and other tileset.json files as they are loaded) + // Features statistics + this.numberOfFeaturesSelected = 0; // Number of features rendered + this.numberOfFeaturesLoaded = 0; // Number of features in memory + this.numberOfPointsSelected = 0; + this.numberOfPointsLoaded = 0; + this.numberOfTrianglesSelected = 0; + // Styling statistics + this.numberOfTilesStyled = 0; + this.numberOfFeaturesStyled = 0; + // Optimization statistics + this.numberOfTilesCulledWithChildrenUnion = 0; + // Memory statistics + this.geometryByteLength = 0; + this.texturesByteLength = 0; + this.batchTableByteLength = 0; + } + + Cesium3DTilesetStatistics.prototype.clear = function() { + this.selected = 0; + this.visited = 0; + this.numberOfCommands = 0; + this.numberOfAttemptedRequests = 0; + this.numberOfFeaturesSelected = 0; + this.numberOfPointsSelected = 0; + this.numberOfTrianglesSelected = 0; + this.numberOfTilesStyled = 0; + this.numberOfFeaturesStyled = 0; + this.numberOfTilesCulledWithChildrenUnion = 0; + }; + + function updatePointAndFeatureCounts(statistics, content, decrement, load) { + var contents = content.innerContents; + var pointsLength = content.pointsLength; + var trianglesLength = content.trianglesLength; + var featuresLength = content.featuresLength; + var geometryByteLength = content.geometryByteLength; + var texturesByteLength = content.texturesByteLength; + var batchTableByteLength = content.batchTableByteLength; + + if (load) { + statistics.numberOfFeaturesLoaded += decrement ? -featuresLength : featuresLength; + statistics.numberOfPointsLoaded += decrement ? -pointsLength : pointsLength; + statistics.geometryByteLength += decrement ? -geometryByteLength : geometryByteLength; + statistics.texturesByteLength += decrement ? -texturesByteLength : texturesByteLength; + statistics.batchTableByteLength += decrement ? -batchTableByteLength : batchTableByteLength; + } else { + statistics.numberOfFeaturesSelected += decrement ? -featuresLength : featuresLength; + statistics.numberOfPointsSelected += decrement ? -pointsLength : pointsLength; + statistics.numberOfTrianglesSelected += decrement ? -trianglesLength : trianglesLength; + } + + if (defined(contents)) { + var length = contents.length; + for (var i = 0; i < length; ++i) { + updatePointAndFeatureCounts(statistics, contents[i], decrement, load); + } + } + } + + Cesium3DTilesetStatistics.prototype.incrementSelectionCounts = function(content) { + updatePointAndFeatureCounts(this, content, false, false); + }; + + Cesium3DTilesetStatistics.prototype.incrementLoadCounts = function(content) { + updatePointAndFeatureCounts(this, content, false, true); + }; + + Cesium3DTilesetStatistics.prototype.decrementLoadCounts = function(content) { + updatePointAndFeatureCounts(this, content, true, true); + }; + + Cesium3DTilesetStatistics.clone = function(statistics, result) { + result.selected = statistics.selected; + result.visited = statistics.visited; + result.numberOfCommands = statistics.numberOfCommands; + result.selected = statistics.selected; + result.numberOfAttemptedRequests = statistics.numberOfAttemptedRequests; + result.numberOfPendingRequests = statistics.numberOfPendingRequests; + result.numberOfTilesProcessing = statistics.numberOfTilesProcessing; + result.numberOfTilesWithContentReady = statistics.numberOfTilesWithContentReady; + result.numberOfTilesTotal = statistics.numberOfTilesTotal; + result.numberOfFeaturesSelected = statistics.numberOfFeaturesSelected; + result.numberOfFeaturesLoaded = statistics.numberOfFeaturesLoaded; + result.numberOfPointsSelected = statistics.numberOfPointsSelected; + result.numberOfPointsLoaded = statistics.numberOfPointsLoaded; + result.numberOfTrianglesSelected = statistics.numberOfTrianglesSelected; + result.numberOfTilesStyled = statistics.numberOfTilesStyled; + result.numberOfFeaturesStyled = statistics.numberOfFeaturesStyled; + result.numberOfTilesCulledWithChildrenUnion = statistics.numberOfTilesCulledWithChildrenUnion; + result.geometryByteLength = statistics.geometryByteLength; + result.texturesByteLength = statistics.texturesByteLength; + result.batchTableByteLength = statistics.batchTableByteLength; + }; + + return Cesium3DTilesetStatistics; +}); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js new file mode 100644 index 000000000000..6722b3bf368f --- /dev/null +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -0,0 +1,839 @@ +/*global define*/ +define([ + '../Core/defined', + '../Core/freezeObject', + '../Core/Intersect', + '../Core/ManagedArray', + '../Core/Math', + './Cesium3DTileChildrenVisibility', + './Cesium3DTileRefine', + './CullingVolume', + './OrthographicFrustum', + './SceneMode' + ], function( + defined, + freezeObject, + Intersect, + ManagedArray, + CesiumMath, + Cesium3DTileChildrenVisibility, + Cesium3DTileRefine, + CullingVolume, + OrthographicFrustum, + SceneMode) { + 'use strict'; + + /** + * @private + */ + var Cesium3DTilesetTraversal = {}; + + function selectTiles(tileset, frameState, outOfCore) { + if (tileset.debugFreezeFrame) { + return; + } + + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + + tileset._desiredTiles.length = 0; + tileset._selectedTiles.length = 0; + tileset._requestedTiles.length = 0; + tileset._selectedTilesToStyle.length = 0; + tileset._hasMixedContent = false; + + // Move sentinel node to the tail so, at the start of the frame, all tiles + // may be potentially replaced. Tiles are moved to the right of the sentinel + // when they are selected so they will not be replaced. + var replacementList = tileset._replacementList; + replacementList.splice(replacementList.tail, tileset._replacementSentinel); + + var root = tileset._root; + root.updateTransform(tileset._modelMatrix); + + if (!root.insideViewerRequestVolume(frameState)) { + return; + } + + root._distanceToCamera = root.distanceToTile(frameState); + + if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= maximumScreenSpaceError) { + // The SSE of not rendering the tree is small enough that the tree does not need to be rendered + return; + } + + root._visibilityPlaneMask = root.visibility(frameState, CullingVolume.MASK_INDETERMINATE); + if (root._visibilityPlaneMask === CullingVolume.MASK_OUTSIDE) { + return; + } + + loadTile(tileset, root, frameState); + + if (!tileset.skipLevelOfDetail) { + // just execute base traversal and add tiles to _desiredTiles + tileset._baseTraversal.execute(tileset, root, maximumScreenSpaceError, frameState, outOfCore); + var leaves = tileset._baseTraversal.leaves; + var length = leaves.length; + for (var i = 0; i < length; ++i) { + tileset._desiredTiles.push(leaves.get(i)); + } + } else { + if (tileset.immediatelyLoadDesiredLevelOfDetail) { + tileset._skipTraversal.execute(tileset, root, frameState, outOfCore); + } else { + // leaves of the base traversal is where we start the skip traversal + tileset._baseTraversal.leaves = tileset._skipTraversal.queue1; + + // load and select tiles without skipping up to tileset.baseScreenSpaceError + tileset._baseTraversal.execute(tileset, root, tileset.baseScreenSpaceError, frameState, outOfCore); + + // skip traversal starts from a prepopulated queue from the base traversal + tileset._skipTraversal.execute(tileset, undefined, frameState, outOfCore); + } + } + + // mark tiles for selection or their nearest loaded ancestor + markLoadedTilesForSelection(tileset, frameState, outOfCore); + + // sort selected tiles by distance to camera and call selectTile on each + // set tile._selectionDepth on all tiles + traverseAndSelect(tileset, root, frameState); + + tileset._desiredTiles.trim(); + } + + var descendantStack = []; + + function markLoadedTilesForSelection(tileset, frameState, outOfCore) { + var tiles = tileset._desiredTiles; + var length = tiles.length; + for (var i = 0; i < length; ++i) { + var original = tiles.get(i); + + if (hasAdditiveContent(original)) { + original.selected = true; + original._selectedFrame = frameState.frameNumber; + continue; + } + + var loadedTile = original._ancestorWithLoadedContent; + if (original.hasRenderableContent && original.contentAvailable) { + loadedTile = original; + } + + if (defined(loadedTile)) { + loadedTile.selected = true; + loadedTile._selectedFrame = frameState.frameNumber; + } else { + // if no ancestors are ready, traverse down and select ready tiles to minimize empty regions + descendantStack.push(original); + while (descendantStack.length > 0) { + var tile = descendantStack.pop(); + var children = tile.children; + var childrenLength = children.length; + for (var j = 0; j < childrenLength; ++j) { + var child = children[j]; + touch(tileset, child, outOfCore); + if (child.contentAvailable) { + child.selected = true; + child._finalResolution = true; + child._selectedFrame = frameState.frameNumber; + } + if (child._depth - original._depth < 2) { // prevent traversing too far + if (!child.contentAvailable || child.refine === Cesium3DTileRefine.ADD) { + descendantStack.push(child); + } + } + } + } + } + } + } + + var scratchStack = []; + var scratchStack2 = []; + + /** + * Traverse the tree while tiles are visible and check if their selected frame is the current frame. + * If so, add it to a selection queue. + * Tiles are sorted near to far so we can take advantage of early Z. + * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. + * + * The reason for the preorder traversal is so that tiles can easily be marked with their + * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. + * This property is important for use in the stencil test because we want to render deeper tiles on top of their + * ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer. + * + * We want to select children before their ancestors because there is no guarantee on the relationship between + * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top + * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. @see {@link updateTiles} + * + * NOTE: this will no longer work when there is a chain of selected tiles that is longer than the size of the + * stencil buffer (usually 8 bits). In other words, the subset of the tree containing only selected tiles must be + * no deeper than 255. It is very, very unlikely this will cause a problem. + */ + function traverseAndSelect(tileset, root, frameState) { + var stack = scratchStack; + var ancestorStack = scratchStack2; + + var lastAncestor; + stack.push(root); + while (stack.length > 0 || ancestorStack.length > 0) { + if (ancestorStack.length > 0) { + var waitingTile = ancestorStack[ancestorStack.length - 1]; + if (waitingTile._stackLength === stack.length) { + ancestorStack.pop(); + if (waitingTile === lastAncestor) { + waitingTile._finalResolution = true; + } + selectTile(tileset, waitingTile, frameState); + continue; + } + } + + var tile = stack.pop(); + if (!defined(tile) || !isVisited(tile, frameState)) { + continue; + } + + var shouldSelect = tile.selected && tile._selectedFrame === frameState.frameNumber && tile.hasRenderableContent; + + var children = tile.children; + var childrenLength = children.length; + + children.sort(sortChildrenByDistanceToCamera); + + if (shouldSelect) { + if (tile.refine === Cesium3DTileRefine.ADD) { + tile._finalResolution = true; + selectTile(tileset, tile, frameState); + } else { + tile._selectionDepth = ancestorStack.length; + + if (tile._selectionDepth > 0) { + tileset._hasMixedContent = true; + } + + lastAncestor = tile; + + if (childrenLength === 0) { + tile._finalResolution = true; + selectTile(tileset, tile, frameState); + continue; + } + + ancestorStack.push(tile); + tile._stackLength = stack.length; + } + } + + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + stack.push(child); + } + } + } + + function selectTile(tileset, tile, frameState) { + // There may also be a tight box around just the tile's contents, e.g., for a city, we may be + // zoomed into a neighborhood and can cull the skyscrapers in the root tile. + if (tile.contentAvailable && ( + (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || + (tile.contentVisibility(frameState) !== Intersect.OUTSIDE) + )) { + tileset._selectedTiles.push(tile); + + var tileContent = tile.content; + if (tileContent.featurePropertiesDirty) { + // A feature's property in this tile changed, the tile needs to be re-styled. + tileContent.featurePropertiesDirty = false; + tile.lastStyleTime = 0; // Force applying the style to this tile + tileset._selectedTilesToStyle.push(tile); + } else if ((tile._lastSelectedFrameNumber !== frameState.frameNumber - 1)) { + // Tile is newly selected; it is selected this frame, but was not selected last frame. + tileset._selectedTilesToStyle.push(tile); + } + tile._lastSelectedFrameNumber = frameState.frameNumber; + } + } + + // PERFORMANCE_IDEA: is it worth exploiting frame-to-frame coherence in the sort, i.e., the + // list of children are probably fully or mostly sorted unless the camera moved significantly? + function sortChildrenByDistanceToCamera(a, b) { + // Sort by farthest child first since this is going on a stack + if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { + return b._centerZDepth - a._centerZDepth; + } + + return b._distanceToCamera - a._distanceToCamera; + } + + var emptyArray = freezeObject([]); + + function BaseTraversal() { + this.tileset = undefined; + this.frameState = undefined; + this.outOfCore = undefined; + this.stack = new ManagedArray(); + this.leaves = new ManagedArray(); + this.baseScreenSpaceError = undefined; + this.internalDFS = new InternalBaseTraversal(); + } + + BaseTraversal.prototype.execute = function(tileset, root, baseScreenSpaceError, frameState, outOfCore) { + this.tileset = tileset; + this.frameState = frameState; + this.outOfCore = outOfCore; + this.leaves.length = 0; + this.baseScreenSpaceError = Math.max(baseScreenSpaceError, this.tileset._maximumScreenSpaceError); + this.internalDFS.tileset = this.tileset; + this.internalDFS.frameState = this.frameState; + this.internalDFS.outOfCore = this.outOfCore; + this.internalDFS.baseScreenSpaceError = this.baseScreenSpaceError; + depthFirstSearch(root, this); + }; + + BaseTraversal.prototype.visitStart = function(tile) { + if (!isVisited(tile, this.frameState)) { + visitTile(this.tileset, tile, this.frameState, this.outOfCore); + } + }; + + BaseTraversal.prototype.visitEnd = function(tile) { + tile._lastVisitedFrame = this.frameState.frameNumber; + }; + + BaseTraversal.prototype.getChildren = function(tile) { + var tileset = this.tileset; + var outOfCore = this.outOfCore; + var frameState = this.frameState; + if (!baseUpdateAndCheckChildren(tileset, tile, this.baseScreenSpaceError, frameState)) { + return emptyArray; + } + + var children = tile.children; + var childrenLength = children.length; + var allReady = true; + var replacementWithContent = tile.refine === Cesium3DTileRefine.REPLACE && tile.hasRenderableContent; + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + loadTile(tileset, child, frameState); + touch(tileset, child, outOfCore); + + // content cannot be replaced until all of the nearest descendants with content are all loaded + if (replacementWithContent) { + if (!child.hasEmptyContent) { + allReady = allReady && child.contentAvailable; + } else { + allReady = allReady && this.internalDFS.execute(child); + } + } + } + + if (allReady) { + return children; + } + + return emptyArray; + }; + + function baseUpdateAndCheckChildren(tileset, tile, baseScreenSpaceError, frameState) { + if (hasAdditiveContent(tile)) { + tileset._desiredTiles.push(tile); + } + + // Stop traversal on the subtree since it will be destroyed + if (tile.hasTilesetContent && tile.contentExpired) { + return false; + } + + // stop traversal when we've attained the desired level of error + if (tile._screenSpaceError <= baseScreenSpaceError && !tile.hasTilesetContent) { + // update children so the leaf handler can check if any are visible for the children union bound optimization + updateChildren(tile, frameState); + return false; + } + + var childrenVisibility = updateChildren(tile, frameState); + var showAdditive = tile.refine === Cesium3DTileRefine.ADD; + var showReplacement = tile.refine === Cesium3DTileRefine.REPLACE && (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME) !== 0; + + return showAdditive || showReplacement || tile.hasTilesetContent || !defined(tile._ancestorWithContent); + } + + BaseTraversal.prototype.shouldVisit = function(tile) { + return isVisible(tile._visibilityPlaneMask); + }; + + BaseTraversal.prototype.leafHandler = function(tile) { + // if skipLevelOfDetail is off, leaves of the base traversal get pushed to tileset._desiredTiles. additive tiles have already been pushed + if (this.tileset.skipLevelOfDetail || !hasAdditiveContent(tile)) { + if (tile.refine === Cesium3DTileRefine.REPLACE && !childrenAreVisible(tile)) { + ++this.tileset._statistics.numberOfTilesCulledWithChildrenUnion; + return; + } + this.leaves.push(tile); + } + }; + + function InternalBaseTraversal() { + this.tileset = undefined; + this.frameState = undefined; + this.outOfCore = undefined; + this.baseScreenSpaceError = undefined; + this.stack = new ManagedArray(); + this.allLoaded = undefined; + } + + InternalBaseTraversal.prototype.execute = function(root) { + this.allLoaded = true; + depthFirstSearch(root, this); + return this.allLoaded; + }; + + InternalBaseTraversal.prototype.visitStart = function(tile) { + if (!isVisited(tile, this.frameState)) { + visitTile(this.tileset, tile, this.frameState, this.outOfCore); + } + }; + + InternalBaseTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; + + // Continue traversing until we have renderable content. We want the first descendants with content of the root to load + InternalBaseTraversal.prototype.shouldVisit = function(tile) { + return !tile.hasRenderableContent && isVisible(tile._visibilityPlaneMask); + }; + + InternalBaseTraversal.prototype.getChildren = function(tile) { + var tileset = this.tileset; + var frameState = this.frameState; + var outOfCore = this.outOfCore; + + if (!baseUpdateAndCheckChildren(tileset, tile, this.baseScreenSpaceError, frameState)) { + return emptyArray; + } + + var children = tile.children; + var childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + loadTile(tileset, child, frameState); + touch(tileset, child, outOfCore); + if (!tile.contentAvailable) { + this.allLoaded = false; + } + } + return children; + }; + + InternalBaseTraversal.prototype.updateAndCheckChildren = BaseTraversal.prototype.updateAndCheckChildren; + + function SkipTraversal(options) { + this.tileset = undefined; + this.frameState = undefined; + this.outOfCore = undefined; + this.queue1 = new ManagedArray(); + this.queue2 = new ManagedArray(); + this.internalDFS = new InternalSkipTraversal(options.selectionHeuristic); + this.maxChildrenLength = 0; + this.scratchQueue = new ManagedArray(); + } + + SkipTraversal.prototype.execute = function(tileset, root, frameState, outOfCore) { + this.tileset = tileset; + this.frameState = frameState; + this.outOfCore = outOfCore; + this.internalDFS.frameState = frameState; + this.internalDFS.outOfCore = outOfCore; + + this.maxChildrenLength = 0; + breadthFirstSearch(root, this); + this.queue1.length = 0; + this.queue2.length = 0; + this.scratchQueue.length = 0; + this.scratchQueue.trim(this.maxChildrenLength); + }; + + SkipTraversal.prototype.visitStart = function(tile) { + if (!isVisited(tile, this.frameState)) { + visitTile(this.tileset, tile, this.frameState, this.outOfCore); + } + }; + + SkipTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; + + SkipTraversal.prototype.getChildren = function(tile) { + this.scratchQueue.length = 0; + this.internalDFS.execute(tile, this.scratchQueue); + this.maxChildrenLength = Math.max(this.maxChildrenLength, this.scratchQueue.length); + return this.scratchQueue; + }; + + SkipTraversal.prototype.leafHandler = function(tile) { + // additive tiles have already been pushed + if (!hasAdditiveContent(tile) && !isVisited(tile, this.frameState)) { + this.tileset._desiredTiles.push(tile); + } + }; + + function InternalSkipTraversal(selectionHeuristic) { + this.selectionHeuristic = selectionHeuristic; + this.tileset = undefined; + this.frameState = undefined; + this.outOfCore = undefined; + this.root = undefined; + this.queue = undefined; + this.stack = new ManagedArray(); + } + + InternalSkipTraversal.prototype.execute = function(root, queue) { + this.tileset = root._tileset; + this.root = root; + this.queue = queue; + depthFirstSearch(root, this); + }; + + InternalSkipTraversal.prototype.visitStart = function(tile) { + if (!isVisited(tile, this.frameState)) { + visitTile(this.tileset, tile, this.frameState, this.outOfCore); + } + }; + + InternalSkipTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; + + InternalSkipTraversal.prototype.getChildren = function(tile) { + var tileset = this.tileset; + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + + // Stop traversal on the subtree since it will be destroyed + if (tile.hasTilesetContent && tile.contentExpired) { + return emptyArray; + } + + if (!tile.hasTilesetContent) { + if (hasAdditiveContent(tile)) { + tileset._desiredTiles.push(tile); + } + + // stop traversal when we've attained the desired level of error + if (tile._screenSpaceError <= maximumScreenSpaceError) { + updateChildren(tile, this.frameState); + return emptyArray; + } + + // if we have reached the skipping threshold without any loaded ancestors, return empty so this tile is loaded + if ( + (!tile.hasEmptyContent && tile.contentUnloaded) && + defined(tile._ancestorWithLoadedContent) && + this.selectionHeuristic(tileset, tile._ancestorWithLoadedContent, tile)) { + updateChildren(tile, this.frameState); + return emptyArray; + } + } + + var childrenVisibility = updateChildren(tile, this.frameState); + var showAdditive = tile.refine === Cesium3DTileRefine.ADD && tile._screenSpaceError > maximumScreenSpaceError; + var showReplacement = tile.refine === Cesium3DTileRefine.REPLACE && (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME) !== 0; + + // at least one child is visible, but is not in request volume. the parent must be selected + if (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_NOT_IN_REQUEST_VOLUME && tile.refine === Cesium3DTileRefine.REPLACE) { + this.tileset._desiredTiles.push(tile); + } + + if (showAdditive || showReplacement || tile.hasTilesetContent) { + var children = tile.children; + var childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + touch(tileset, children[i], this.outOfCore); + } + return children; + } + + return emptyArray; + }; + + InternalSkipTraversal.prototype.shouldVisit = function(tile) { + var maximumScreenSpaceError = this.tileset._maximumScreenSpaceError; + var parent = tile.parent; + if (!defined(parent)) { + return isVisible(tile._visibilityPlaneMask); + } + var showAdditive = parent.refine === Cesium3DTileRefine.ADD && parent._screenSpaceError > maximumScreenSpaceError; + + return isVisible(tile._visibilityPlaneMask) && (!showAdditive || getScreenSpaceError(this.tileset, parent.geometricError, tile, this.frameState) > maximumScreenSpaceError); + }; + + InternalSkipTraversal.prototype.leafHandler = function(tile) { + if (tile !== this.root) { + if (tile.refine === Cesium3DTileRefine.REPLACE && !childrenAreVisible(tile)) { + ++this.tileset._statistics.numberOfTilesCulledWithChildrenUnion; + return; + } + if (!tile.hasEmptyContent) { + if (this.tileset.loadSiblings) { + var parent = tile.parent; + var tiles = parent.children; + var length = tiles.length; + for (var i = 0; i < length; ++i) { + loadTile(this.tileset, tiles[i], this.frameState); + touch(this.tileset, tiles[i], this.outOfCore); + } + } else { + loadTile(this.tileset, tile, this.frameState); + touch(this.tileset, tile, this.outOfCore); + } + } + this.queue.push(tile); + } else if (!hasAdditiveContent(tile)) { + // additive tiles have already been pushed + this.tileset._desiredTiles.push(tile); + } + }; + + function updateChildren(tile, frameState) { + if (isVisited(tile, frameState)) { + return tile._childrenVisibility; + } + + var children = tile.children; + + updateTransforms(children, tile.computedTransform); + computeDistanceToCamera(children, frameState); + + return computeChildrenVisibility(tile, frameState); + } + + function isVisited(tile, frameState) { + // because the leaves of one tree traversal are the root of the subsequent traversal, avoid double visitation + return tile._lastVisitedFrame === frameState.frameNumber; + } + + function visitTile(tileset, tile, frameState, outOfCore) { + ++tileset._statistics.visited; + tile.selected = false; + tile._finalResolution = false; + computeSSE(tile, frameState); + touch(tileset, tile, outOfCore); + tile.updateExpiration(); + tile._ancestorWithContent = undefined; + tile._ancestorWithLoadedContent = undefined; + var parent = tile.parent; + if (defined(parent)) { + var replace = parent.refine === Cesium3DTileRefine.REPLACE; + tile._ancestorWithContent = (replace && parent.hasRenderableContent) ? parent : parent._ancestorWithContent; + tile._ancestorWithLoadedContent = (replace && parent.hasRenderableContent && parent.contentAvailable) ? parent : parent._ancestorWithLoadedContent; + } + } + + function touch(tileset, tile, outOfCore) { + if (!outOfCore) { + return; + } + var node = tile.replacementNode; + if (defined(node)) { + tileset._replacementList.splice(tileset._replacementSentinel, node); + } + } + + function computeSSE(tile, frameState) { + if (tile._screenSpaceErrorComputedFrame !== frameState.frameNumber) { + tile._screenSpaceErrorComputedFrame = frameState.frameNumber; + tile._screenSpaceError = getScreenSpaceError(tile._tileset, tile.geometricError, tile, frameState); + } + } + + function loadTile(tileset, tile, frameState) { + if ((tile.contentUnloaded || tile.contentExpired) && tile._requestedFrame !== frameState.frameNumber) { + tile._requestedFrame = frameState.frameNumber; + tileset._requestedTiles.push(tile); + } + } + + function computeChildrenVisibility(tile, frameState) { + var flag = Cesium3DTileChildrenVisibility.NONE; + var children = tile.children; + var childrenLength = children.length; + var visibilityPlaneMask = tile._visibilityPlaneMask; + for (var k = 0; k < childrenLength; ++k) { + var child = children[k]; + + var visibilityMask = child.visibility(frameState, visibilityPlaneMask); + + if (isVisible(visibilityMask)) { + flag |= Cesium3DTileChildrenVisibility.VISIBLE; + } + + if (!child.insideViewerRequestVolume(frameState)) { + if (isVisible(visibilityMask)) { + flag |= Cesium3DTileChildrenVisibility.VISIBLE_NOT_IN_REQUEST_VOLUME; + } + visibilityMask = CullingVolume.MASK_OUTSIDE; + } else { + flag |= Cesium3DTileChildrenVisibility.IN_REQUEST_VOLUME; + if (isVisible(visibilityMask)) { + flag |= Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME; + } + } + + child._visibilityPlaneMask = visibilityMask; + } + + tile._childrenVisibility = flag; + + return flag; + } + + function getScreenSpaceError(tileset, geometricError, tile, frameState) { + if (geometricError === 0.0) { + // Leaf tiles do not have any error so save the computation + return 0.0; + } + + // Avoid divide by zero when viewer is inside the tile + var camera = frameState.camera; + var frustum = camera.frustum; + var context = frameState.context; + var height = context.drawingBufferHeight; + + var error; + if (frameState.mode === SceneMode.SCENE2D || frustum instanceof OrthographicFrustum) { + if (defined(frustum._offCenterFrustum)) { + frustum = frustum._offCenterFrustum; + } + var width = context.drawingBufferWidth; + var pixelSize = Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) / Math.max(width, height); + error = geometricError / pixelSize; + } else { + var distance = Math.max(tile._distanceToCamera, CesiumMath.EPSILON7); + var sseDenominator = camera.frustum.sseDenominator; + error = (geometricError * height) / (distance * sseDenominator); + + if (tileset.dynamicScreenSpaceError) { + var density = tileset._dynamicScreenSpaceErrorComputedDensity; + var factor = tileset.dynamicScreenSpaceErrorFactor; + var dynamicError = CesiumMath.fog(distance, density) * factor; + error -= dynamicError; + } + } + + return error; + } + + function computeDistanceToCamera(children, frameState) { + var length = children.length; + for (var i = 0; i < length; ++i) { + var child = children[i]; + child._distanceToCamera = child.distanceToTile(frameState); + child._centerZDepth = child.distanceToTileCenter(frameState); + } + } + + function updateTransforms(children, parentTransform) { + var length = children.length; + for (var i = 0; i < length; ++i) { + var child = children[i]; + child.updateTransform(parentTransform); + } + } + + function isVisible(visibilityPlaneMask) { + return visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + } + + function childrenAreVisible(tile) { + // optimization does not apply for additive refinement + return tile.refine === Cesium3DTileRefine.ADD || tile.children.length === 0 || tile._childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE !== 0; + } + + function hasAdditiveContent(tile) { + return tile.refine === Cesium3DTileRefine.ADD && tile.hasRenderableContent; + } + + function depthFirstSearch(root, options) { + var stack = options.stack; + + if (defined(root) && (!defined(options.shouldVisit) || options.shouldVisit(root))) { + stack.push(root); + } + + var maxLength = 0; + while (stack.length > 0) { + maxLength = Math.max(maxLength, stack.length); + + var tile = stack.pop(); + options.visitStart(tile); + var children = options.getChildren(tile); + var isNativeArray = !defined(children.get); + var length = children.length; + for (var i = 0; i < length; ++i) { + var child = isNativeArray ? children[i] : children.get(i); + + if (!defined(options.shouldVisit) || options.shouldVisit(child)) { + stack.push(child); + } + } + + if (length === 0 && defined(options.leafHandler)) { + options.leafHandler(tile); + } + options.visitEnd(tile); + } + + stack.trim(maxLength); + } + + function breadthFirstSearch(root, options) { + var queue1 = options.queue1; + var queue2 = options.queue2; + + if (defined(root) && (!defined(options.shouldVisit) || options.shouldVisit(root))) { + queue1.push(root); + } + + var maxLength = 0; + while (queue1.length > 0) { + var length = queue1.length; + maxLength = Math.max(maxLength, length); + + for (var i = 0; i < length; ++i) { + var tile = queue1.get(i); + options.visitStart(tile); + var children = options.getChildren(tile); + var isNativeArray = !defined(children.get); + var childrenLength = children.length; + for (var j = 0; j < childrenLength; ++j) { + var child = isNativeArray ? children[j] : children.get(j); + + if (!defined(options.shouldVisit) || options.shouldVisit(child)) { + queue2.push(child); + } + } + + if (childrenLength === 0 && defined(options.leafHandler)) { + options.leafHandler(tile); + } + options.visitEnd(tile); + } + + queue1.length = 0; + var temp = queue1; + queue1 = queue2; + queue2 = temp; + options.queue1 = queue1; + options.queue2 = queue2; + } + + queue1.length = 0; + queue2.length = 0; + + queue1.trim(maxLength); + queue2.trim(maxLength); + } + + Cesium3DTilesetTraversal.selectTiles = selectTiles; + + Cesium3DTilesetTraversal.BaseTraversal = BaseTraversal; + + Cesium3DTilesetTraversal.SkipTraversal = SkipTraversal; + + return Cesium3DTilesetTraversal; +}); diff --git a/Source/Scene/Composite3DTileContent.js b/Source/Scene/Composite3DTileContent.js new file mode 100644 index 000000000000..39b0cf0ab799 --- /dev/null +++ b/Source/Scene/Composite3DTileContent.js @@ -0,0 +1,310 @@ +/*global define*/ +define([ + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/FeatureDetection', + '../Core/getMagic', + '../Core/RuntimeError', + '../ThirdParty/when' + ], function( + defaultValue, + defined, + defineProperties, + destroyObject, + FeatureDetection, + getMagic, + RuntimeError, + when) { + 'use strict'; + + // Bail out if the browser doesn't support typed arrays, to prevent the setup function + // from failing, since we won't be able to create a WebGL context anyway. + if (!FeatureDetection.supportsTypedArrays()) { + return {}; + } + + /** + * Represents the contents of a + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Composite/README.md|Composite} + * tile in a {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles} tileset. + *

+ * Implements the {@link Cesium3DTileContent} interface. + *

+ * + * @alias Composite3DTileContent + * @constructor + * + * @private + */ + function Composite3DTileContent(tileset, tile, url, arrayBuffer, byteOffset, factory) { + this._tileset = tileset; + this._tile = tile; + this._url = url; + this._contents = []; + this._readyPromise = when.defer(); + + initialize(this, arrayBuffer, byteOffset, factory); + } + + defineProperties(Composite3DTileContent.prototype, { + /** + * @inheritdoc Cesium3DTileContent#featurePropertiesDirty + */ + featurePropertiesDirty : { + get : function() { + var contents = this._contents; + var length = contents.length; + for (var i = 0; i < length; ++i) { + if (contents[i].featurePropertiesDirty) { + return true; + } + } + + return false; + }, + set : function(value) { + var contents = this._contents; + var length = contents.length; + for (var i = 0; i < length; ++i) { + contents[i].featurePropertiesDirty = value; + } + } + }, + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns 0. Instead call featuresLength for a tile in the composite. + */ + featuresLength : { + get : function() { + return 0; + } + }, + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns 0. Instead call pointsLength for a tile in the composite. + */ + pointsLength : { + get : function() { + return 0; + } + }, + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns 0. Instead call trianglesLength for a tile in the composite. + */ + trianglesLength : { + get : function() { + return 0; + } + }, + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns 0. Instead call geometryByteLength for a tile in the composite. + */ + geometryByteLength : { + get : function() { + return 0; + } + }, + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns 0. Instead call texturesByteLength for a tile in the composite. + */ + texturesByteLength : { + get : function() { + return 0; + } + }, + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns 0. Instead call batchTableByteLength for a tile in the composite. + */ + batchTableByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#innerContents + */ + innerContents : { + get : function() { + return this._contents; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#readyPromise + */ + readyPromise : { + get : function() { + return this._readyPromise.promise; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tileset + */ + tileset : { + get : function() { + return this._tileset; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tile + */ + tile : { + get : function() { + return this._tile; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#url + */ + url : { + get : function() { + return this._url; + } + }, + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns undefined. Instead call batchTable for a tile in the composite. + */ + batchTable : { + get : function() { + return undefined; + } + } + }); + + var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; + + function initialize(content, arrayBuffer, byteOffset, factory) { + byteOffset = defaultValue(byteOffset, 0); + + var uint8Array = new Uint8Array(arrayBuffer); + var view = new DataView(arrayBuffer); + byteOffset += sizeOfUint32; // Skip magic + + var version = view.getUint32(byteOffset, true); + if (version !== 1) { + throw new RuntimeError('Only Composite Tile version 1 is supported. Version ' + version + ' is not.'); + } + byteOffset += sizeOfUint32; + + // Skip byteLength + byteOffset += sizeOfUint32; + + var tilesLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var contentPromises = []; + + for (var i = 0; i < tilesLength; ++i) { + var tileType = getMagic(uint8Array, byteOffset); + + // Tile byte length is stored after magic and version + var tileByteLength = view.getUint32(byteOffset + sizeOfUint32 * 2, true); + + var contentFactory = factory[tileType]; + + if (defined(contentFactory)) { + var innerContent = contentFactory(content._tileset, content._tile, content._url, arrayBuffer, byteOffset); + content._contents.push(innerContent); + contentPromises.push(innerContent.readyPromise); + } else { + throw new RuntimeError('Unknown tile content type, ' + tileType + ', inside Composite tile'); + } + + byteOffset += tileByteLength; + } + + when.all(contentPromises).then(function() { + content._readyPromise.resolve(content); + }).otherwise(function(error) { + content._readyPromise.reject(error); + }); + } + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns false. Instead call hasProperty for a tile in the composite. + */ + Composite3DTileContent.prototype.hasProperty = function(batchId, name) { + return false; + }; + + /** + * Part of the {@link Cesium3DTileContent} interface. Composite3DTileContent + * always returns undefined. Instead call getFeature for a tile in the composite. + */ + Composite3DTileContent.prototype.getFeature = function(batchId) { + return undefined; + }; + + /** + * @inheritdoc Cesium3DTileContent#applyDebugSettings + */ + Composite3DTileContent.prototype.applyDebugSettings = function(enabled, color) { + var contents = this._contents; + var length = contents.length; + for (var i = 0; i < length; ++i) { + contents[i].applyDebugSettings(enabled, color); + } + }; + + /** + * @inheritdoc Cesium3DTileContent#applyStyle + */ + Composite3DTileContent.prototype.applyStyle = function(frameState, style) { + var contents = this._contents; + var length = contents.length; + for (var i = 0; i < length; ++i) { + contents[i].applyStyle(frameState, style); + } + }; + + /** + * @inheritdoc Cesium3DTileContent#update + */ + Composite3DTileContent.prototype.update = function(tileset, frameState) { + var contents = this._contents; + var length = contents.length; + for (var i = 0; i < length; ++i) { + contents[i].update(tileset, frameState); + } + }; + + /** + * @inheritdoc Cesium3DTileContent#isDestroyed + */ + Composite3DTileContent.prototype.isDestroyed = function() { + return false; + }; + + /** + * @inheritdoc Cesium3DTileContent#destroy + */ + Composite3DTileContent.prototype.destroy = function() { + var contents = this._contents; + var length = contents.length; + for (var i = 0; i < length; ++i) { + contents[i].destroy(); + } + return destroyObject(this); + }; + + return Composite3DTileContent; +}); diff --git a/Source/Scene/ConditionsExpression.js b/Source/Scene/ConditionsExpression.js new file mode 100644 index 000000000000..42b945164bf9 --- /dev/null +++ b/Source/Scene/ConditionsExpression.js @@ -0,0 +1,188 @@ +/*global define*/ +define([ + '../Core/clone', + '../Core/defined', + '../Core/defineProperties', + './Expression' + ], function( + clone, + defined, + defineProperties, + Expression) { + 'use strict'; + + /** + * An expression for a style applied to a {@link Cesium3DTileset}. + *

+ * Evaluates a conditions expression defined using the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language}. + *

+ *

+ * Implements the {@link StyleExpression} interface. + *

+ * + * @alias ConditionsExpression + * @constructor + * + * @param {Object} [conditionsExpression] The conditions expression defined using the 3D Tiles Styling language. + * @param {Object} [expressions] Additional expressions defined in the style. + * + * @example + * var expression = new Cesium.ConditionsExpression({ + * conditions : [ + * ['${Area} > 10, 'color("#FF0000")'], + * ['${id} !== "1"', 'color("#00FF00")'], + * ['true', 'color("#FFFFFF")'] + * ] + * }); + * expression.evaluateColor(frameState, feature, result); // returns a Cesium.Color object + */ + function ConditionsExpression(conditionsExpression, expressions) { + this._conditionsExpression = clone(conditionsExpression, true); + this._conditions = conditionsExpression.conditions; + this._runtimeConditions = undefined; + + setRuntime(this, expressions); + } + + defineProperties(ConditionsExpression.prototype, { + /** + * Gets the conditions expression defined in the 3D Tiles Styling language. + * + * @memberof ConditionsExpression.prototype + * + * @type {Object} + * @readonly + * + * @default undefined + */ + conditionsExpression : { + get : function() { + return this._conditionsExpression; + } + } + }); + + function Statement(condition, expression) { + this.condition = condition; + this.expression = expression; + } + + function setRuntime(expression, expressions) { + var runtimeConditions = []; + var conditions = expression._conditions; + if (!defined(conditions)) { + return; + } + var length = conditions.length; + for (var i = 0; i < length; ++i) { + var statement = conditions[i]; + var cond = String(statement[0]); + var condExpression = String(statement[1]); + runtimeConditions.push(new Statement( + new Expression(cond, expressions), + new Expression(condExpression, expressions) + )); + } + expression._runtimeConditions = runtimeConditions; + } + + /** + * Evaluates the result of an expression, optionally using the provided feature's properties. If the result of + * the expression in the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language} + * is of type Boolean, Number, or String, the corresponding JavaScript + * primitive type will be returned. If the result is a RegExp, a Javascript RegExp + * object will be returned. If the result is a Cartesian2, Cartesian3, or Cartesian4, + * a {@link Cartesian2}, {@link Cartesian3}, or {@link Cartesian4} object will be returned. If the result argument is + * a {@link Color}, the {@link Cartesian4} value is converted to a {@link Color} and then returned. + * + * @param {FrameState} frameState The frame state. + * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. + * @param {Object} [result] The object onto which to store the result. + * @returns {Boolean|Number|String|RegExp|Cartesian2|Cartesian3|Cartesian4|Color} The result of evaluating the expression. + */ + ConditionsExpression.prototype.evaluate = function(frameState, feature, result) { + var conditions = this._runtimeConditions; + if (!defined(conditions)) { + return undefined; + } + var length = conditions.length; + for (var i = 0; i < length; ++i) { + var statement = conditions[i]; + if (statement.condition.evaluate(frameState, feature)) { + return statement.expression.evaluate(frameState, feature, result); + } + } + }; + + /** + * Evaluates the result of a Color expression, using the values defined by a feature. + *

+ * This is equivalent to {@link ConditionsExpression#evaluate} but always returns a {@link Color} object. + *

+ * @param {FrameState} frameState The frame state. + * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. + * @param {Color} [result] The object in which to store the result + * @returns {Color} The modified result parameter or a new Color instance if one was not provided. + */ + ConditionsExpression.prototype.evaluateColor = function(frameState, feature, result) { + var conditions = this._runtimeConditions; + if (!defined(conditions)) { + return undefined; + } + var length = conditions.length; + for (var i = 0; i < length; ++i) { + var statement = conditions[i]; + if (statement.condition.evaluate(frameState, feature)) { + return statement.expression.evaluateColor(frameState, feature, result); + } + } + }; + + /** + * Gets the shader function for this expression. + * Returns undefined if the shader function can't be generated from this expression. + * + * @param {String} functionName Name to give to the generated function. + * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. + * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. + * @param {String} returnType The return type of the generated function. + * + * @returns {String} The shader function. + * + * @private + */ + ConditionsExpression.prototype.getShaderFunction = function(functionName, attributePrefix, shaderState, returnType) { + var conditions = this._runtimeConditions; + if (!defined(conditions) || conditions.length === 0) { + return undefined; + } + + var shaderFunction = ''; + var length = conditions.length; + for (var i = 0; i < length; ++i) { + var statement = conditions[i]; + + var condition = statement.condition.getShaderExpression(attributePrefix, shaderState); + var expression = statement.expression.getShaderExpression(attributePrefix, shaderState); + + // Build the if/else chain from the list of conditions + shaderFunction += + ' ' + ((i === 0) ? 'if' : 'else if') + ' (' + condition + ') \n' + + ' { \n' + + ' return ' + expression + '; \n' + + ' } \n'; + } + + shaderFunction = returnType + ' ' + functionName + '() \n' + + '{ \n' + + shaderFunction + + ' return ' + returnType + '(1.0); \n' + // Return a default value if no conditions are met + '} \n'; + + return shaderFunction; + }; + + return ConditionsExpression; +}); diff --git a/Source/Scene/Empty3DTileContent.js b/Source/Scene/Empty3DTileContent.js new file mode 100644 index 000000000000..f463c70d9ce1 --- /dev/null +++ b/Source/Scene/Empty3DTileContent.js @@ -0,0 +1,192 @@ +/*global define*/ +define([ + '../Core/defineProperties', + '../Core/destroyObject' + ], function( + defineProperties, + destroyObject) { + 'use strict'; + + /** + * Represents empty content for tiles in a + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles} tileset that + * do not have content, e.g., because they are used to optimize hierarchical culling. + *

+ * Implements the {@link Cesium3DTileContent} interface. + *

+ * + * @alias Empty3DTileContent + * @constructor + * + * @private + */ + function Empty3DTileContent(tileset, tile) { + this._tileset = tileset; + this._tile = tile; + + /** + * @inheritdoc Cesium3DTileContent#featurePropertiesDirty + */ + this.featurePropertiesDirty = false; + } + + defineProperties(Empty3DTileContent.prototype, { + /** + * @inheritdoc Cesium3DTileContent#featuresLength + */ + featuresLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#pointsLength + */ + pointsLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#trianglesLength + */ + trianglesLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#geometryByteLength + */ + geometryByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#texturesByteLength + */ + texturesByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTableByteLength + */ + batchTableByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#innerContents + */ + innerContents : { + get : function() { + return undefined; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#readyPromise + */ + readyPromise : { + get : function() { + return undefined; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tileset + */ + tileset : { + get : function() { + return this._tileset; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tile + */ + tile : { + get : function() { + return this._tile; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#url + */ + url: { + get: function() { + return undefined; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTable + */ + batchTable : { + get : function() { + return undefined; + } + } + }); + + /** + * Part of the {@link Cesium3DTileContent} interface. Empty3DTileContent + * always returns false since a tile of this type does not have any features. + */ + Empty3DTileContent.prototype.hasProperty = function(batchId, name) { + return false; + }; + + /** + * Part of the {@link Cesium3DTileContent} interface. Empty3DTileContent + * always returns undefined since a tile of this type does not have any features. + */ + Empty3DTileContent.prototype.getFeature = function(batchId) { + return undefined; + }; + + /** + * @inheritdoc Cesium3DTileContent#applyDebugSettings + */ + Empty3DTileContent.prototype.applyDebugSettings = function(enabled, color) { + }; + + /** + * @inheritdoc Cesium3DTileContent#applyStyle + */ + Empty3DTileContent.prototype.applyStyle = function(frameState, style) { + }; + + /** + * @inheritdoc Cesium3DTileContent#update + */ + Empty3DTileContent.prototype.update = function(tileset, frameState) { + }; + + /** + * @inheritdoc Cesium3DTileContent#isDestroyed + */ + Empty3DTileContent.prototype.isDestroyed = function() { + return false; + }; + + /** + * @inheritdoc Cesium3DTileContent#destroy + */ + Empty3DTileContent.prototype.destroy = function() { + return destroyObject(this); + }; + + return Empty3DTileContent; +}); diff --git a/Source/Scene/Expression.js b/Source/Scene/Expression.js new file mode 100644 index 000000000000..ff7384ca0fb7 --- /dev/null +++ b/Source/Scene/Expression.js @@ -0,0 +1,1742 @@ +/*global define*/ +define([ + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Cartesian4', + '../Core/Check', + '../Core/Color', + '../Core/defined', + '../Core/defineProperties', + '../Core/isArray', + '../Core/Math', + '../Core/RuntimeError', + '../ThirdParty/jsep', + './ExpressionNodeType' + ], function( + Cartesian2, + Cartesian3, + Cartesian4, + Check, + Color, + defined, + defineProperties, + isArray, + CesiumMath, + RuntimeError, + jsep, + ExpressionNodeType) { + 'use strict'; + + /** + * An expression for a style applied to a {@link Cesium3DTileset}. + *

+ * Evaluates an expression defined using the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language}. + *

+ *

+ * Implements the {@link StyleExpression} interface. + *

+ * + * @alias Expression + * @constructor + * + * @param {String} [expression] The expression defined using the 3D Tiles Styling language. + * @param {Object} [expressions] Additional expressions defined in the style. + * + * @example + * var expression = new Cesium.Expression('(regExp("^Chest").test(${County})) && (${YearBuilt} >= 1970)'); + * expression.evaluate(frameState, feature); // returns true or false depending on the feature's properties + * + * @example + * var expression = new Cesium.Expression('(${Temperature} > 90) ? color("red") : color("white")'); + * expression.evaluateColor(frameState, feature, result); // returns a Cesium.Color object + */ + function Expression(expression, expressions) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('expression', expression); + //>>includeEnd('debug'); + + this._expression = expression; + expression = replaceExpressions(expression, expressions); + expression = replaceVariables(removeBackslashes(expression)); + + // customize jsep operators + jsep.addBinaryOp('=~', 0); + jsep.addBinaryOp('!~', 0); + + var ast; + try { + ast = jsep(expression); + } catch (e) { + throw new RuntimeError(e); + } + + this._runtimeAst = createRuntimeAst(this, ast); + } + + defineProperties(Expression.prototype, { + /** + * Gets the expression defined in the 3D Tiles Styling language. + * + * @memberof Expression.prototype + * + * @type {String} + * @readonly + * + * @default undefined + */ + expression : { + get : function() { + return this._expression; + } + } + }); + + /** + * Evaluates the result of an expression, optionally using the provided feature's properties. If the result of + * the expression in the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language} + * is of type Boolean, Number, or String, the corresponding JavaScript + * primitive type will be returned. If the result is a RegExp, a Javascript RegExp + * object will be returned. If the result is a Cartesian2, Cartesian3, or Cartesian4, + * a {@link Cartesian2}, {@link Cartesian3}, or {@link Cartesian4} object will be returned. If the result argument is + * a {@link Color}, the {@link Cartesian4} value is converted to a {@link Color} and then returned. + * + * @param {FrameState} frameState The frame state. + * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. + * @param {Object} [result] The object onto which to store the result. + * @returns {Boolean|Number|String|RegExp|Cartesian2|Cartesian3|Cartesian4|Color} The result of evaluating the expression. + */ + Expression.prototype.evaluate = function(frameState, feature, result) { + scratchStorage.reset(); + var value = this._runtimeAst.evaluate(frameState, feature); + if ((result instanceof Color) && (value instanceof Cartesian4)) { + return Color.fromCartesian4(value, result); + } + if ((value instanceof Cartesian2) || (value instanceof Cartesian3) || (value instanceof Cartesian4)) { + return value.clone(result); + } + return value; + }; + + /** + * Evaluates the result of a Color expression, optionally using the provided feature's properties. + *

+ * This is equivalent to {@link Expression#evaluate} but always returns a {@link Color} object. + *

+ * + * @param {FrameState} frameState The frame state. + * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. + * @param {Color} [result] The object in which to store the result + * @returns {Color} The modified result parameter or a new Color instance if one was not provided. + */ + Expression.prototype.evaluateColor = function(frameState, feature, result) { + scratchStorage.reset(); + var color = this._runtimeAst.evaluate(frameState, feature); + return Color.fromCartesian4(color, result); + }; + + /** + * Gets the shader function for this expression. + * Returns undefined if the shader function can't be generated from this expression. + * + * @param {String} functionName Name to give to the generated function. + * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. + * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. + * @param {String} returnType The return type of the generated function. + * + * @returns {String} The shader function. + * + * @private + */ + Expression.prototype.getShaderFunction = function(functionName, attributePrefix, shaderState, returnType) { + var shaderExpression = this.getShaderExpression(attributePrefix, shaderState); + + shaderExpression = returnType + ' ' + functionName + '() \n' + + '{ \n' + + ' return ' + shaderExpression + '; \n' + + '} \n'; + + return shaderExpression; + }; + + /** + * Gets the shader expression for this expression. + * Returns undefined if the shader expression can't be generated from this expression. + * + * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. + * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. + * + * @returns {String} The shader expression. + * + * @private + */ + Expression.prototype.getShaderExpression = function(attributePrefix, shaderState) { + return this._runtimeAst.getShaderExpression(attributePrefix, shaderState); + }; + + var unaryOperators = ['!', '-', '+']; + var binaryOperators = ['+', '-', '*', '/', '%', '===', '!==', '>', '>=', '<', '<=', '&&', '||', '!~', '=~']; + + var variableRegex = /\${(.*?)}/g; // Matches ${variable_name} + var backslashRegex = /\\/g; + var backslashReplacement = '@#%'; + var replacementRegex = /@#%/g; + + var scratchColor = new Color(); + + // Scratch storage manager while evaluating deep expressions. + // For example, an expression like dot(vec4(${red}), vec4(${green}) * vec4(${blue}) requires 3 scratch Cartesian4's + var scratchStorage = { + arrayIndex : 0, + arrayArray : [[]], + cartesian2Index : 0, + cartesian3Index : 0, + cartesian4Index : 0, + cartesian2Array : [new Cartesian2()], + cartesian3Array : [new Cartesian3()], + cartesian4Array : [new Cartesian4()], + reset : function() { + this.arrayIndex = 0; + this.cartesian2Index = 0; + this.cartesian3Index = 0; + this.cartesian4Index = 0; + }, + getArray : function() { + if (this.arrayIndex >= this.arrayArray.length) { + this.arrayArray.push([]); + } + var array = this.arrayArray[this.arrayIndex++]; + array.length = 0; + return array; + }, + getCartesian2 : function() { + if (this.cartesian2Index >= this.cartesian2Array.length) { + this.cartesian2Array.push(new Cartesian2()); + } + return this.cartesian2Array[this.cartesian2Index++]; + }, + getCartesian3 : function() { + if (this.cartesian3Index >= this.cartesian3Array.length) { + this.cartesian3Array.push(new Cartesian3()); + } + return this.cartesian3Array[this.cartesian3Index++]; + }, + getCartesian4 : function() { + if (this.cartesian4Index >= this.cartesian4Array.length) { + this.cartesian4Array.push(new Cartesian4()); + } + return this.cartesian4Array[this.cartesian4Index++]; + } + }; + + var unaryFunctions = { + abs : getEvaluateUnaryComponentwise(Math.abs), + sqrt : getEvaluateUnaryComponentwise(Math.sqrt), + cos : getEvaluateUnaryComponentwise(Math.cos), + sin : getEvaluateUnaryComponentwise(Math.sin), + tan : getEvaluateUnaryComponentwise(Math.tan), + acos : getEvaluateUnaryComponentwise(Math.acos), + asin : getEvaluateUnaryComponentwise(Math.asin), + atan : getEvaluateUnaryComponentwise(Math.atan), + radians : getEvaluateUnaryComponentwise(CesiumMath.toRadians), + degrees : getEvaluateUnaryComponentwise(CesiumMath.toDegrees), + sign : getEvaluateUnaryComponentwise(CesiumMath.sign), + floor : getEvaluateUnaryComponentwise(Math.floor), + ceil : getEvaluateUnaryComponentwise(Math.ceil), + round : getEvaluateUnaryComponentwise(Math.round), + exp : getEvaluateUnaryComponentwise(Math.exp), + exp2 : getEvaluateUnaryComponentwise(exp2), + log : getEvaluateUnaryComponentwise(Math.log), + log2 : getEvaluateUnaryComponentwise(log2), + fract : getEvaluateUnaryComponentwise(fract), + length : length, + normalize: normalize + }; + + var binaryFunctions = { + atan2 : getEvaluateBinaryCommponentwise(Math.atan2, false), + pow : getEvaluateBinaryCommponentwise(Math.pow, false), + min : getEvaluateBinaryCommponentwise(Math.min, true), + max : getEvaluateBinaryCommponentwise(Math.max, true), + distance : distance, + dot : dot, + cross : cross + }; + + var ternaryFunctions = { + clamp : getEvaluateTernaryCommponentwise(CesiumMath.clamp, true), + mix : getEvaluateTernaryCommponentwise(CesiumMath.lerp, true) + }; + + function fract(number) { + return number - Math.floor(number); + } + + function exp2(exponent) { + return Math.pow(2.0,exponent); + } + + function log2(number) { + return CesiumMath.logBase(number, 2.0); + } + + function getEvaluateUnaryComponentwise(operation) { + return function(call, left) { + if (typeof left === 'number') { + return operation(left); + } else if (left instanceof Cartesian2) { + return Cartesian2.fromElements(operation(left.x), operation(left.y), scratchStorage.getCartesian2()); + } else if (left instanceof Cartesian3) { + return Cartesian3.fromElements(operation(left.x), operation(left.y), operation(left.z), scratchStorage.getCartesian3()); + } else if (left instanceof Cartesian4) { + return Cartesian4.fromElements(operation(left.x), operation(left.y), operation(left.z), operation(left.w), scratchStorage.getCartesian4()); + } + throw new RuntimeError('Function "' + call + '" requires a vector or number argument. Argument is ' + left + '.'); + }; + } + + function getEvaluateBinaryCommponentwise(operation, allowScalar) { + return function(call, left, right) { + if (allowScalar && typeof right === 'number') { + if (typeof left === 'number') { + return operation(left, right); + } else if (left instanceof Cartesian2) { + return Cartesian2.fromElements(operation(left.x, right), operation(left.y, right), scratchStorage.getCartesian2()); + } else if (left instanceof Cartesian3) { + return Cartesian3.fromElements(operation(left.x, right), operation(left.y, right), operation(left.z, right), scratchStorage.getCartesian3()); + } else if (left instanceof Cartesian4) { + return Cartesian4.fromElements(operation(left.x, right), operation(left.y, right), operation(left.z, right), operation(left.w, right), scratchStorage.getCartesian4()); + } + } + + if (typeof left === 'number' && typeof right === 'number') { + return operation(left, right); + } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { + return Cartesian2.fromElements(operation(left.x, right.x), operation(left.y, right.y), scratchStorage.getCartesian2()); + } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { + return Cartesian3.fromElements(operation(left.x, right.x), operation(left.y, right.y), operation(left.z, right.z), scratchStorage.getCartesian3()); + } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { + return Cartesian4.fromElements(operation(left.x, right.x), operation(left.y, right.y), operation(left.z, right.z), operation(left.w, right.w), scratchStorage.getCartesian4()); + } + + throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); + }; + } + + function getEvaluateTernaryCommponentwise(operation, allowScalar) { + return function(call, left, right, test) { + if (allowScalar && typeof test === 'number') { + if (typeof left === 'number' && typeof right === 'number') { + return operation(left, right, test); + } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { + return Cartesian2.fromElements(operation(left.x, right.x, test), operation(left.y, right.y, test), scratchStorage.getCartesian2()); + } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { + return Cartesian3.fromElements(operation(left.x, right.x, test), operation(left.y, right.y, test), operation(left.z, right.z, test), scratchStorage.getCartesian3()); + } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { + return Cartesian4.fromElements(operation(left.x, right.x, test), operation(left.y, right.y, test), operation(left.z, right.z, test), operation(left.w, right.w, test), scratchStorage.getCartesian4()); + } + } + + if (typeof left === 'number' && typeof right === 'number' && typeof test === 'number') { + return operation(left, right, test); + } else if (left instanceof Cartesian2 && right instanceof Cartesian2 && test instanceof Cartesian2) { + return Cartesian2.fromElements(operation(left.x, right.x, test.x), operation(left.y, right.y, test.y), scratchStorage.getCartesian2()); + } else if (left instanceof Cartesian3 && right instanceof Cartesian3 && test instanceof Cartesian3) { + return Cartesian3.fromElements(operation(left.x, right.x, test.x), operation(left.y, right.y, test.y), operation(left.z, right.z, test.z), scratchStorage.getCartesian3()); + } else if (left instanceof Cartesian4 && right instanceof Cartesian4 && test instanceof Cartesian4) { + return Cartesian4.fromElements(operation(left.x, right.x, test.x), operation(left.y, right.y, test.y), operation(left.z, right.z, test.z), operation(left.w, right.w, test.w), scratchStorage.getCartesian4()); + } + + throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ', ' + right + ', and ' + test + '.'); + }; + } + + function length(call, left) { + if (typeof left === 'number') { + return Math.abs(left); + } else if (left instanceof Cartesian2) { + return Cartesian2.magnitude(left); + } else if (left instanceof Cartesian3) { + return Cartesian3.magnitude(left); + } else if (left instanceof Cartesian4) { + return Cartesian4.magnitude(left); + } + + throw new RuntimeError('Function "' + call + '" requires a vector or number argument. Argument is ' + left + '.'); + } + + function normalize(call, left) { + if (typeof left === 'number') { + return 1.0; + } else if (left instanceof Cartesian2) { + return Cartesian2.normalize(left, scratchStorage.getCartesian2()); + } else if (left instanceof Cartesian3) { + return Cartesian3.normalize(left, scratchStorage.getCartesian3()); + } else if (left instanceof Cartesian4) { + return Cartesian4.normalize(left, scratchStorage.getCartesian4()); + } + + throw new RuntimeError('Function "' + call + '" requires a vector or number argument. Argument is ' + left + '.'); + } + + function distance(call, left, right) { + if (typeof left === 'number' && typeof right === 'number') { + return Math.abs(left - right); + } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { + return Cartesian2.distance(left, right); + } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { + return Cartesian3.distance(left, right); + } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { + return Cartesian4.distance(left, right); + } + + throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); + } + + function dot(call, left, right) { + if (typeof left === 'number' && typeof right === 'number') { + return left * right; + } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { + return Cartesian2.dot(left, right); + } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { + return Cartesian3.dot(left, right); + } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { + return Cartesian4.dot(left, right); + } + + throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); + } + + function cross(call, left, right) { + if (left instanceof Cartesian3 && right instanceof Cartesian3) { + return Cartesian3.cross(left, right, scratchStorage.getCartesian3()); + } + + throw new RuntimeError('Function "' + call + '" requires vec3 arguments. Arguments are ' + left + ' and ' + right + '.'); + } + + function Node(type, value, left, right, test) { + this._type = type; + this._value = value; + this._left = left; + this._right = right; + this._test = test; + this.evaluate = undefined; + + setEvaluateFunction(this); + } + + function replaceExpressions(expression, expressions) { + if (!defined(expressions)) { + return expression; + } + for (var key in expressions) { + if (expressions.hasOwnProperty(key)) { + var expressionPlaceholder = new RegExp('\\$\\{' + key + '\\}', 'g'); + var expressionReplace = expressions[key]; + if (defined(expressionReplace)) { + expression = expression.replace(expressionPlaceholder, expressionReplace); + } + } + } + return expression; + } + + function removeBackslashes(expression) { + return expression.replace(backslashRegex, backslashReplacement); + } + + function replaceBackslashes(expression) { + return expression.replace(replacementRegex, '\\'); + } + + function replaceVariables(expression) { + var exp = expression; + var result = ''; + var i = exp.indexOf('${'); + while (i >= 0) { + // Check if string is inside quotes + var openSingleQuote = exp.indexOf('\''); + var openDoubleQuote = exp.indexOf('"'); + var closeQuote; + if (openSingleQuote >= 0 && openSingleQuote < i) { + closeQuote = exp.indexOf('\'', openSingleQuote + 1); + result += exp.substr(0, closeQuote + 1); + exp = exp.substr(closeQuote + 1); + i = exp.indexOf('${'); + } else if (openDoubleQuote >= 0 && openDoubleQuote < i) { + closeQuote = exp.indexOf('"', openDoubleQuote + 1); + result += exp.substr(0, closeQuote + 1); + exp = exp.substr(closeQuote + 1); + i = exp.indexOf('${'); + } else { + result += exp.substr(0, i); + var j = exp.indexOf('}'); + if (j < 0) { + throw new RuntimeError('Unmatched {.'); + } + result += "czm_" + exp.substr(i + 2, j - (i + 2)); + exp = exp.substr(j + 1); + i = exp.indexOf('${'); + } + } + result += exp; + return result; + } + + function parseLiteral(ast) { + var type = typeof ast.value; + if (ast.value === null) { + return new Node(ExpressionNodeType.LITERAL_NULL, null); + } else if (type === 'boolean') { + return new Node(ExpressionNodeType.LITERAL_BOOLEAN, ast.value); + } else if (type === 'number') { + return new Node(ExpressionNodeType.LITERAL_NUMBER, ast.value); + } else if (type === 'string') { + if (ast.value.indexOf('${') >= 0) { + return new Node(ExpressionNodeType.VARIABLE_IN_STRING, ast.value); + } + return new Node(ExpressionNodeType.LITERAL_STRING, replaceBackslashes(ast.value)); + } + } + + function parseCall(expression, ast) { + var args = ast.arguments; + var argsLength = args.length; + var call; + var val, left, right; + + // Member function calls + if (ast.callee.type === 'MemberExpression') { + call = ast.callee.property.name; + var object = ast.callee.object; + if (call === 'test' || call === 'exec') { + // Make sure this is called on a valid type + if (object.callee.name !== 'regExp') { + throw new RuntimeError(call + ' is not a function.'); + } + if (argsLength === 0) { + if (call === 'test') { + return new Node(ExpressionNodeType.LITERAL_BOOLEAN, false); + } + return new Node(ExpressionNodeType.LITERAL_NULL, null); + } + left = createRuntimeAst(expression, object); + right = createRuntimeAst(expression, args[0]); + return new Node(ExpressionNodeType.FUNCTION_CALL, call, left, right); + } else if (call === 'toString') { + val = createRuntimeAst(expression, object); + return new Node(ExpressionNodeType.FUNCTION_CALL, call, val); + } + + throw new RuntimeError('Unexpected function call "' + call + '".'); + } + + // Non-member function calls + call = ast.callee.name; + if (call === 'color') { + if (argsLength === 0) { + return new Node(ExpressionNodeType.LITERAL_COLOR, call); + } + val = createRuntimeAst(expression, args[0]); + if (defined(args[1])) { + var alpha = createRuntimeAst(expression, args[1]); + return new Node(ExpressionNodeType.LITERAL_COLOR, call, [val, alpha]); + } + return new Node(ExpressionNodeType.LITERAL_COLOR, call, [val]); + } else if (call === 'rgb' || call === 'hsl') { + if (argsLength < 3) { + throw new RuntimeError(call + ' requires three arguments.'); + } + val = [ + createRuntimeAst(expression, args[0]), + createRuntimeAst(expression, args[1]), + createRuntimeAst(expression, args[2]) + ]; + return new Node(ExpressionNodeType.LITERAL_COLOR, call, val); + } else if (call === 'rgba' || call === 'hsla') { + if (argsLength < 4) { + throw new RuntimeError(call + ' requires four arguments.'); + } + val = [ + createRuntimeAst(expression, args[0]), + createRuntimeAst(expression, args[1]), + createRuntimeAst(expression, args[2]), + createRuntimeAst(expression, args[3]) + ]; + return new Node(ExpressionNodeType.LITERAL_COLOR, call, val); + } else if (call === 'vec2' || call === 'vec3' || call === 'vec4') { + // Check for invalid constructors at evaluation time + val = new Array(argsLength); + for (var i = 0; i < argsLength; ++i) { + val[i] = createRuntimeAst(expression, args[i]); + } + return new Node(ExpressionNodeType.LITERAL_VECTOR, call, val); + } else if (call === 'isNaN' || call === 'isFinite') { + if (argsLength === 0) { + if (call === 'isNaN') { + return new Node(ExpressionNodeType.LITERAL_BOOLEAN, true); + } + return new Node(ExpressionNodeType.LITERAL_BOOLEAN, false); + } + val = createRuntimeAst(expression, args[0]); + return new Node(ExpressionNodeType.UNARY, call, val); + } else if (call === 'isExactClass' || call === 'isClass') { + if (argsLength < 1 || argsLength > 1) { + throw new RuntimeError(call + ' requires exactly one argument.'); + } + val = createRuntimeAst(expression, args[0]); + return new Node(ExpressionNodeType.UNARY, call, val); + } else if (call === 'getExactClassName') { + if (argsLength > 0) { + throw new RuntimeError(call + ' does not take any argument.'); + } + return new Node(ExpressionNodeType.UNARY, call); + } else if (defined(unaryFunctions[call])) { + if (argsLength !== 1) { + throw new RuntimeError(call + ' requires exactly one argument.'); + } + val = createRuntimeAst(expression, args[0]); + return new Node(ExpressionNodeType.UNARY, call, val); + } else if (defined(binaryFunctions[call])) { + if (argsLength !== 2) { + throw new RuntimeError(call + ' requires exactly two arguments.'); + } + left = createRuntimeAst(expression, args[0]); + right = createRuntimeAst(expression, args[1]); + return new Node(ExpressionNodeType.BINARY, call, left, right); + } else if (defined(ternaryFunctions[call])) { + if (argsLength !== 3) { + throw new RuntimeError(call + ' requires exactly three arguments.'); + } + left = createRuntimeAst(expression, args[0]); + right = createRuntimeAst(expression, args[1]); + var test = createRuntimeAst(expression, args[2]); + return new Node(ExpressionNodeType.TERNARY, call, left, right, test); + } else if (call === 'Boolean') { + if (argsLength === 0) { + return new Node(ExpressionNodeType.LITERAL_BOOLEAN, false); + } + val = createRuntimeAst(expression, args[0]); + return new Node(ExpressionNodeType.UNARY, call, val); + } else if (call === 'Number') { + if (argsLength === 0) { + return new Node(ExpressionNodeType.LITERAL_NUMBER, 0); + } + val = createRuntimeAst(expression, args[0]); + return new Node(ExpressionNodeType.UNARY, call, val); + } else if (call === 'String') { + if (argsLength === 0) { + return new Node(ExpressionNodeType.LITERAL_STRING, ''); + } + val = createRuntimeAst(expression, args[0]); + return new Node(ExpressionNodeType.UNARY, call, val); + } else if (call === 'regExp') { + return parseRegex(expression, ast); + } + + throw new RuntimeError('Unexpected function call "' + call + '".'); + } + + function parseRegex(expression, ast) { + var args = ast.arguments; + // no arguments, return default regex + if (args.length === 0) { + return new Node(ExpressionNodeType.LITERAL_REGEX, new RegExp()); + } + + var pattern = createRuntimeAst(expression, args[0]); + var exp; + + // optional flag argument supplied + if (args.length > 1) { + var flags = createRuntimeAst(expression, args[1]); + if (isLiteralType(pattern) && isLiteralType(flags)) { + try { + exp = new RegExp(replaceBackslashes(String(pattern._value)), flags._value); + } catch (e) { + throw new RuntimeError(e); + } + return new Node(ExpressionNodeType.LITERAL_REGEX, exp); + } + return new Node(ExpressionNodeType.REGEX, pattern, flags); + } + + // only pattern argument supplied + if (isLiteralType(pattern)) { + try { + exp = new RegExp(replaceBackslashes(String(pattern._value))); + } catch (e) { + throw new RuntimeError(e); + } + return new Node(ExpressionNodeType.LITERAL_REGEX, exp); + } + return new Node(ExpressionNodeType.REGEX, pattern); + } + + function parseKeywordsAndVariables(ast) { + if (isVariable(ast.name)) { + var name = getPropertyName(ast.name); + if (name.substr(0, 8) === 'tiles3d_') { + return new Node(ExpressionNodeType.BUILTIN_VARIABLE, name); + } + return new Node(ExpressionNodeType.VARIABLE, name); + } else if (ast.name === 'NaN') { + return new Node(ExpressionNodeType.LITERAL_NUMBER, NaN); + } else if (ast.name === 'Infinity') { + return new Node(ExpressionNodeType.LITERAL_NUMBER, Infinity); + } else if (ast.name === 'undefined') { + return new Node(ExpressionNodeType.LITERAL_UNDEFINED, undefined); + } + + throw new RuntimeError(ast.name + ' is not defined.'); + } + + function parseMathConstant(ast) { + var name = ast.property.name; + if (name === 'PI') { + return new Node(ExpressionNodeType.LITERAL_NUMBER, Math.PI); + } else if (name === 'E') { + return new Node(ExpressionNodeType.LITERAL_NUMBER, Math.E); + } + } + + function parseMemberExpression(expression, ast) { + if (ast.object.name === 'Math') { + return parseMathConstant(ast); + } + + var val; + var obj = createRuntimeAst(expression, ast.object); + if (ast.computed) { + val = createRuntimeAst(expression, ast.property); + return new Node(ExpressionNodeType.MEMBER, 'brackets', obj, val); + } + + val = new Node(ExpressionNodeType.LITERAL_STRING, ast.property.name); + return new Node(ExpressionNodeType.MEMBER, 'dot', obj, val); + } + + function isLiteralType(node) { + return (node._type >= ExpressionNodeType.LITERAL_NULL); + } + + function isVariable(name) { + return (name.substr(0, 4) === 'czm_'); + } + + function getPropertyName(variable) { + return variable.substr(4); + } + + function createRuntimeAst(expression, ast) { + var node; + var op; + var left; + var right; + + if (ast.type === 'Literal') { + node = parseLiteral(ast); + } else if (ast.type === 'CallExpression') { + node = parseCall(expression, ast); + } else if (ast.type === 'Identifier') { + node = parseKeywordsAndVariables(ast); + } else if (ast.type === 'UnaryExpression') { + op = ast.operator; + var child = createRuntimeAst(expression, ast.argument); + if (unaryOperators.indexOf(op) > -1) { + node = new Node(ExpressionNodeType.UNARY, op, child); + } else { + throw new RuntimeError('Unexpected operator "' + op + '".'); + } + } else if (ast.type === 'BinaryExpression') { + op = ast.operator; + left = createRuntimeAst(expression, ast.left); + right = createRuntimeAst(expression, ast.right); + if (binaryOperators.indexOf(op) > -1) { + node = new Node(ExpressionNodeType.BINARY, op, left, right); + } else { + throw new RuntimeError('Unexpected operator "' + op + '".'); + } + } else if (ast.type === 'LogicalExpression') { + op = ast.operator; + left = createRuntimeAst(expression, ast.left); + right = createRuntimeAst(expression, ast.right); + if (binaryOperators.indexOf(op) > -1) { + node = new Node(ExpressionNodeType.BINARY, op, left, right); + } + } else if (ast.type === 'ConditionalExpression') { + var test = createRuntimeAst(expression, ast.test); + left = createRuntimeAst(expression, ast.consequent); + right = createRuntimeAst(expression, ast.alternate); + node = new Node(ExpressionNodeType.CONDITIONAL, '?', left, right, test); + } else if (ast.type === 'MemberExpression') { + node = parseMemberExpression(expression, ast); + } else if (ast.type === 'ArrayExpression') { + var val = []; + for (var i = 0; i < ast.elements.length; i++) { + val[i] = createRuntimeAst(expression, ast.elements[i]); + } + node = new Node(ExpressionNodeType.ARRAY, val); + } else if (ast.type === 'Compound') { + // empty expression or multiple expressions + throw new RuntimeError('Provide exactly one expression.'); + } else { + throw new RuntimeError('Cannot parse expression.'); + } + + return node; + } + + function setEvaluateFunction(node) { + if (node._type === ExpressionNodeType.CONDITIONAL) { + node.evaluate = node._evaluateConditional; + } else if (node._type === ExpressionNodeType.FUNCTION_CALL) { + if (node._value === 'test') { + node.evaluate = node._evaluateRegExpTest; + } else if (node._value === 'exec') { + node.evaluate = node._evaluateRegExpExec; + } else if (node._value === 'toString') { + node.evaluate = node._evaluateToString; + } + } else if (node._type === ExpressionNodeType.UNARY) { + if (node._value === '!') { + node.evaluate = node._evaluateNot; + } else if (node._value === '-') { + node.evaluate = node._evaluateNegative; + } else if (node._value === '+') { + node.evaluate = node._evaluatePositive; + } else if (node._value === 'isNaN') { + node.evaluate = node._evaluateNaN; + } else if (node._value === 'isFinite') { + node.evaluate = node._evaluateIsFinite; + } else if (node._value === 'isExactClass') { + node.evaluate = node._evaluateIsExactClass; + } else if (node._value === 'isClass') { + node.evaluate = node._evaluateIsClass; + } else if (node._value === 'getExactClassName') { + node.evaluate = node._evaluategetExactClassName; + } else if (node._value === 'Boolean') { + node.evaluate = node._evaluateBooleanConversion; + } else if (node._value === 'Number') { + node.evaluate = node._evaluateNumberConversion; + } else if (node._value === 'String') { + node.evaluate = node._evaluateStringConversion; + } else if (defined(unaryFunctions[node._value])) { + node.evaluate = getEvaluateUnaryFunction(node._value); + } + } else if (node._type === ExpressionNodeType.BINARY) { + if (node._value === '+') { + node.evaluate = node._evaluatePlus; + } else if (node._value === '-') { + node.evaluate = node._evaluateMinus; + } else if (node._value === '*') { + node.evaluate = node._evaluateTimes; + } else if (node._value === '/') { + node.evaluate = node._evaluateDivide; + } else if (node._value === '%') { + node.evaluate = node._evaluateMod; + } else if (node._value === '===') { + node.evaluate = node._evaluateEqualsStrict; + } else if (node._value === '!==') { + node.evaluate = node._evaluateNotEqualsStrict; + } else if (node._value === '<') { + node.evaluate = node._evaluateLessThan; + } else if (node._value === '<=') { + node.evaluate = node._evaluateLessThanOrEquals; + } else if (node._value === '>') { + node.evaluate = node._evaluateGreaterThan; + } else if (node._value === '>=') { + node.evaluate = node._evaluateGreaterThanOrEquals; + } else if (node._value === '&&') { + node.evaluate = node._evaluateAnd; + } else if (node._value === '||') { + node.evaluate = node._evaluateOr; + } else if (node._value === '=~') { + node.evaluate = node._evaluateRegExpMatch; + } else if (node._value === '!~') { + node.evaluate = node._evaluateRegExpNotMatch; + } else if (defined(binaryFunctions[node._value])) { + node.evaluate = getEvaluateBinaryFunction(node._value); + } + } else if (node._type === ExpressionNodeType.TERNARY) { + node.evaluate = getEvaluateTernaryFunction(node._value); + } else if (node._type === ExpressionNodeType.MEMBER) { + if (node._value === 'brackets') { + node.evaluate = node._evaluateMemberBrackets; + } else { + node.evaluate = node._evaluateMemberDot; + } + } else if (node._type === ExpressionNodeType.ARRAY) { + node.evaluate = node._evaluateArray; + } else if (node._type === ExpressionNodeType.VARIABLE) { + node.evaluate = node._evaluateVariable; + } else if (node._type === ExpressionNodeType.VARIABLE_IN_STRING) { + node.evaluate = node._evaluateVariableString; + } else if (node._type === ExpressionNodeType.LITERAL_COLOR) { + node.evaluate = node._evaluateLiteralColor; + } else if (node._type === ExpressionNodeType.LITERAL_VECTOR) { + node.evaluate = node._evaluateLiteralVector; + } else if (node._type === ExpressionNodeType.LITERAL_STRING) { + node.evaluate = node._evaluateLiteralString; + } else if (node._type === ExpressionNodeType.REGEX) { + node.evaluate = node._evaluateRegExp; + } else if (node._type === ExpressionNodeType.BUILTIN_VARIABLE) { + if (node._value === 'tiles3d_tileset_time') { + node.evaluate = evaluateTilesetTime; + } + } else { + node.evaluate = node._evaluateLiteral; + } + } + + function evaluateTilesetTime(frameState, feature) { + return feature.content.tileset.timeSinceLoad; + } + + function getEvaluateUnaryFunction(call) { + var evaluate = unaryFunctions[call]; + return function(feature) { + var left = this._left.evaluate(feature); + return evaluate(call, left); + }; + } + + function getEvaluateBinaryFunction(call) { + var evaluate = binaryFunctions[call]; + return function(feature) { + var left = this._left.evaluate(feature); + var right = this._right.evaluate(feature); + return evaluate(call, left, right); + }; + } + + function getEvaluateTernaryFunction(call) { + var evaluate = ternaryFunctions[call]; + return function(feature) { + var left = this._left.evaluate(feature); + var right = this._right.evaluate(feature); + var test = this._test.evaluate(feature); + return evaluate(call, left, right, test); + }; + } + + Node.prototype._evaluateLiteral = function(frameState, feature) { + return this._value; + }; + + Node.prototype._evaluateLiteralColor = function(frameState, feature) { + var color = scratchColor; + var args = this._left; + if (this._value === 'color') { + if (!defined(args)) { + Color.fromBytes(255, 255, 255, 255, color); + } else if (args.length > 1) { + Color.fromCssColorString(args[0].evaluate(frameState, feature), color); + color.alpha = args[1].evaluate(frameState, feature); + } else { + Color.fromCssColorString(args[0].evaluate(frameState, feature), color); + } + } else if (this._value === 'rgb') { + Color.fromBytes( + args[0].evaluate(frameState, feature), + args[1].evaluate(frameState, feature), + args[2].evaluate(frameState, feature), + 255, color); + } else if (this._value === 'rgba') { + // convert between css alpha (0 to 1) and cesium alpha (0 to 255) + var a = args[3].evaluate(frameState, feature) * 255; + Color.fromBytes( + args[0].evaluate(frameState, feature), + args[1].evaluate(frameState, feature), + args[2].evaluate(frameState, feature), + a, color); + } else if (this._value === 'hsl') { + Color.fromHsl( + args[0].evaluate(frameState, feature), + args[1].evaluate(frameState, feature), + args[2].evaluate(frameState, feature), + 1.0, color); + } else if (this._value === 'hsla') { + Color.fromHsl( + args[0].evaluate(frameState, feature), + args[1].evaluate(frameState, feature), + args[2].evaluate(frameState, feature), + args[3].evaluate(frameState, feature), + color); + } + return Cartesian4.fromColor(color, scratchStorage.getCartesian4()); + }; + + Node.prototype._evaluateLiteralVector = function(frameState, feature) { + // Gather the components that make up the vector, which includes components from interior vectors. + // For example vec3(1, 2, 3) or vec3(vec2(1, 2), 3) are both valid. + // + // If the number of components does not equal the vector's size, then a RuntimeError is thrown - with two exceptions: + // 1. A vector may be constructed from a larger vector and drop the extra components. + // 2. A vector may be constructed from a single component - vec3(1) will become vec3(1, 1, 1). + // + // Examples of invalid constructors include: + // vec4(1, 2) // not enough components + // vec3(vec2(1, 2)) // not enough components + // vec3(1, 2, 3, 4) // too many components + // vec2(vec4(1), 1) // too many components + + var components = scratchStorage.getArray(); + var call = this._value; + var args = this._left; + var argsLength = args.length; + for (var i = 0; i < argsLength; ++i) { + var value = args[i].evaluate(frameState, feature); + if (typeof value === 'number') { + components.push(value); + } else if (value instanceof Cartesian2) { + components.push(value.x, value.y); + } else if (value instanceof Cartesian3) { + components.push(value.x, value.y, value.z); + } else if (value instanceof Cartesian4) { + components.push(value.x, value.y, value.z, value.w); + } else { + throw new RuntimeError(call + ' argument must be a vector or number. Argument is ' + value + '.'); + } + } + + var componentsLength = components.length; + var vectorLength = parseInt(call.charAt(3)); + + if (componentsLength === 0) { + throw new RuntimeError('Invalid ' + call + ' constructor. No valid arguments.'); + } else if ((componentsLength < vectorLength) && (componentsLength > 1)) { + throw new RuntimeError('Invalid ' + call + ' constructor. Not enough arguments.'); + } else if ((componentsLength > vectorLength) && (argsLength > 1)) { + throw new RuntimeError('Invalid ' + call + ' constructor. Too many arguments.'); + } + + if (componentsLength === 1) { + // Add the same component 3 more times + var component = components[0]; + components.push(component, component, component); + } + + if (call === 'vec2') { + return Cartesian2.fromArray(components, 0, scratchStorage.getCartesian2()); + } else if (call === 'vec3') { + return Cartesian3.fromArray(components, 0, scratchStorage.getCartesian3()); + } else if (call === 'vec4') { + return Cartesian4.fromArray(components, 0, scratchStorage.getCartesian4()); + } + }; + + Node.prototype._evaluateLiteralString = function(frameState, feature) { + return this._value; + }; + + Node.prototype._evaluateVariableString = function(frameState, feature) { + var result = this._value; + var match = variableRegex.exec(result); + while (match !== null) { + var placeholder = match[0]; + var variableName = match[1]; + var property = feature.getProperty(variableName); + if (!defined(property)) { + property = ''; + } + result = result.replace(placeholder, property); + match = variableRegex.exec(result); + } + return result; + }; + + Node.prototype._evaluateVariable = function(frameState, feature) { + // evaluates to undefined if the property name is not defined for that feature + return feature.getProperty(this._value); + }; + + function checkFeature (ast) { + return (ast._value === 'feature'); + } + + // PERFORMANCE_IDEA: Determine if parent property needs to be computed before runtime + Node.prototype._evaluateMemberDot = function(frameState, feature) { + if (checkFeature(this._left)) { + return feature.getProperty(this._right.evaluate(frameState, feature)); + } + var property = this._left.evaluate(frameState, feature); + if (!defined(property)) { + return undefined; + } + + var member = this._right.evaluate(frameState, feature); + if ((property instanceof Cartesian2) || (property instanceof Cartesian3) || (property instanceof Cartesian4)) { + // Vector components may be accessed with .r, .g, .b, .a and implicitly with .x, .y, .z, .w + if (member === 'r') { + return property.x; + } else if (member === 'g') { + return property.y; + } else if (member === 'b') { + return property.z; + } else if (member === 'a') { + return property.w; + } + } + return property[member]; + }; + + Node.prototype._evaluateMemberBrackets = function(frameState, feature) { + if (checkFeature(this._left)) { + return feature.getProperty(this._right.evaluate(frameState, feature)); + } + var property = this._left.evaluate(frameState, feature); + if (!defined(property)) { + return undefined; + } + + var member = this._right.evaluate(frameState, feature); + if ((property instanceof Cartesian2) || (property instanceof Cartesian3) || (property instanceof Cartesian4)) { + // Vector components may be accessed with [0][1][2][3], ['r']['g']['b']['a'] and implicitly with ['x']['y']['z']['w'] + // For Cartesian2 and Cartesian3 out-of-range components will just return undefined + if (member === 0 || member === 'r') { + return property.x; + } else if (member === 1 || member === 'g') { + return property.y; + } else if (member === 2 || member === 'b') { + return property.z; + } else if (member === 3 || member === 'a') { + return property.w; + } + } + return property[member]; + }; + + Node.prototype._evaluateArray = function(frameState, feature) { + var array = []; + for (var i = 0; i < this._value.length; i++) { + array[i] = this._value[i].evaluate(frameState, feature); + } + return array; + }; + + // PERFORMANCE_IDEA: Have "fast path" functions that deal only with specific types + // that we can assign if we know the types before runtime + + Node.prototype._evaluateNot = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + if (typeof left !== 'boolean') { + throw new RuntimeError('Operator "!" requires a boolean argument. Argument is ' + left + '.'); + } + return !left; + }; + + Node.prototype._evaluateNegative = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + if (left instanceof Cartesian2) { + return Cartesian2.negate(left, scratchStorage.getCartesian2()); + } else if (left instanceof Cartesian3) { + return Cartesian3.negate(left, scratchStorage.getCartesian3()); + } else if (left instanceof Cartesian4) { + return Cartesian4.negate(left, scratchStorage.getCartesian4()); + } else if (typeof left === 'number') { + return -left; + } + + throw new RuntimeError('Operator "-" requires a vector or number argument. Argument is ' + left + '.'); + }; + + Node.prototype._evaluatePositive = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + + if (!((left instanceof Cartesian2) || (left instanceof Cartesian3) || (left instanceof Cartesian4) || (typeof left === 'number'))) { + throw new RuntimeError('Operator "+" requires a vector or number argument. Argument is ' + left + '.'); + } + + return left; + }; + + Node.prototype._evaluateLessThan = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if ((typeof left !== 'number') || (typeof right !== 'number')) { + throw new RuntimeError('Operator "<" requires number arguments. Arguments are ' + left + ' and ' + right + '.'); + } + + return left < right; + }; + + Node.prototype._evaluateLessThanOrEquals = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if ((typeof left !== 'number') || (typeof right !== 'number')) { + throw new RuntimeError('Operator "<=" requires number arguments. Arguments are ' + left + ' and ' + right + '.'); + } + + return left <= right; + }; + + Node.prototype._evaluateGreaterThan = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if ((typeof left !== 'number') || (typeof right !== 'number')) { + throw new RuntimeError('Operator ">" requires number arguments. Arguments are ' + left + ' and ' + right + '.'); + } + + return left > right; + }; + + Node.prototype._evaluateGreaterThanOrEquals = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if ((typeof left !== 'number') || (typeof right !== 'number')) { + throw new RuntimeError('Operator ">=" requires number arguments. Arguments are ' + left + ' and ' + right + '.'); + } + + return left >= right; + }; + + Node.prototype._evaluateOr = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + if (typeof left !== 'boolean') { + throw new RuntimeError('Operator "||" requires boolean arguments. First argument is ' + left + '.'); + } + + // short circuit the expression + if (left) { + return true; + } + + var right = this._right.evaluate(frameState, feature); + if (typeof right !== 'boolean') { + throw new RuntimeError('Operator "||" requires boolean arguments. Second argument is ' + right + '.'); + } + + return left || right; + }; + + Node.prototype._evaluateAnd = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + if (typeof left !== 'boolean') { + throw new RuntimeError('Operator "&&" requires boolean arguments. First argument is ' + left + '.'); + } + + // short circuit the expression + if (!left) { + return false; + } + + var right = this._right.evaluate(frameState, feature); + if (typeof right !== 'boolean') { + throw new RuntimeError('Operator "&&" requires boolean arguments. Second argument is ' + right + '.'); + } + + return left && right; + }; + + Node.prototype._evaluatePlus = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + if ((right instanceof Cartesian2) && (left instanceof Cartesian2)) { + return Cartesian2.add(left, right, scratchStorage.getCartesian2()); + } else if ((right instanceof Cartesian3) && (left instanceof Cartesian3)) { + return Cartesian3.add(left, right, scratchStorage.getCartesian3()); + } else if ((right instanceof Cartesian4) && (left instanceof Cartesian4)) { + return Cartesian4.add(left, right, scratchStorage.getCartesian4()); + } else if ((typeof left === 'string') || (typeof right === 'string')) { + // If only one argument is a string the other argument calls its toString function. + return left + right; + } else if ((typeof left === 'number') && (typeof right === 'number')) { + return left + right; + } + + throw new RuntimeError('Operator "+" requires vector or number arguments of matching types, or at least one string argument. Arguments are ' + left + ' and ' + right + '.'); + }; + + Node.prototype._evaluateMinus = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + if ((right instanceof Cartesian2) && (left instanceof Cartesian2)) { + return Cartesian2.subtract(left, right, scratchStorage.getCartesian2()); + } else if ((right instanceof Cartesian3) && (left instanceof Cartesian3)) { + return Cartesian3.subtract(left, right, scratchStorage.getCartesian3()); + } else if ((right instanceof Cartesian4) && (left instanceof Cartesian4)) { + return Cartesian4.subtract(left, right, scratchStorage.getCartesian4()); + } else if ((typeof left === 'number') && (typeof right === 'number')) { + return left - right; + } + + throw new RuntimeError('Operator "-" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); + }; + + Node.prototype._evaluateTimes = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + if ((right instanceof Cartesian2) && (left instanceof Cartesian2)) { + return Cartesian2.multiplyComponents(left, right, scratchStorage.getCartesian2()); + } else if ((right instanceof Cartesian2) && (typeof left === 'number')) { + return Cartesian2.multiplyByScalar(right, left, scratchStorage.getCartesian2()); + } else if ((left instanceof Cartesian2) && (typeof right === 'number')) { + return Cartesian2.multiplyByScalar(left, right, scratchStorage.getCartesian2()); + } else if ((right instanceof Cartesian3) && (left instanceof Cartesian3)) { + return Cartesian3.multiplyComponents(left, right, scratchStorage.getCartesian3()); + } else if ((right instanceof Cartesian3) && (typeof left === 'number')) { + return Cartesian3.multiplyByScalar(right, left, scratchStorage.getCartesian3()); + } else if ((left instanceof Cartesian3) && (typeof right === 'number')) { + return Cartesian3.multiplyByScalar(left, right, scratchStorage.getCartesian3()); + } else if ((right instanceof Cartesian4) && (left instanceof Cartesian4)) { + return Cartesian4.multiplyComponents(left, right, scratchStorage.getCartesian4()); + } else if ((right instanceof Cartesian4) && (typeof left === 'number')) { + return Cartesian4.multiplyByScalar(right, left, scratchStorage.getCartesian4()); + } else if ((left instanceof Cartesian4) && (typeof right === 'number')) { + return Cartesian4.multiplyByScalar(left, right, scratchStorage.getCartesian4()); + } else if ((typeof left === 'number') && (typeof right === 'number')) { + return left * right; + } + + throw new RuntimeError('Operator "*" requires vector or number arguments. If both arguments are vectors they must be matching types. Arguments are ' + left + ' and ' + right + '.'); + }; + + Node.prototype._evaluateDivide = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + if ((right instanceof Cartesian2) && (left instanceof Cartesian2)) { + return Cartesian2.divideComponents(left, right, scratchStorage.getCartesian2()); + } else if ((left instanceof Cartesian2) && (typeof right === 'number')) { + return Cartesian2.divideByScalar(left, right, scratchStorage.getCartesian2()); + } else if ((right instanceof Cartesian3) && (left instanceof Cartesian3)) { + return Cartesian3.divideComponents(left, right, scratchStorage.getCartesian3()); + } else if ((left instanceof Cartesian3) && (typeof right === 'number')) { + return Cartesian3.divideByScalar(left, right, scratchStorage.getCartesian3()); + } else if ((right instanceof Cartesian4) && (left instanceof Cartesian4)) { + return Cartesian4.divideComponents(left, right, scratchStorage.getCartesian4()); + } else if ((left instanceof Cartesian4) && (typeof right === 'number')) { + return Cartesian4.divideByScalar(left, right, scratchStorage.getCartesian4()); + } else if ((typeof left === 'number') && (typeof right === 'number')) { + return left / right; + } + + throw new RuntimeError('Operator "/" requires vector or number arguments of matching types, or a number as the second argument. Arguments are ' + left + ' and ' + right + '.'); + }; + + Node.prototype._evaluateMod = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + if ((right instanceof Cartesian2) && (left instanceof Cartesian2)) { + return Cartesian2.fromElements(left.x % right.x, left.y % right.y, scratchStorage.getCartesian2()); + } else if ((right instanceof Cartesian3) && (left instanceof Cartesian3)) { + return Cartesian3.fromElements(left.x % right.x, left.y % right.y, left.z % right.z, scratchStorage.getCartesian3()); + } else if ((right instanceof Cartesian4) && (left instanceof Cartesian4)) { + return Cartesian4.fromElements(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w, scratchStorage.getCartesian4()); + } else if ((typeof left === 'number') && (typeof right === 'number')) { + return left % right; + } + + throw new RuntimeError('Operator "%" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); + }; + + Node.prototype._evaluateEqualsStrict = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + if ((right instanceof Cartesian2) && (left instanceof Cartesian2) || + (right instanceof Cartesian3) && (left instanceof Cartesian3) || + (right instanceof Cartesian4) && (left instanceof Cartesian4)) { + return left.equals(right); + } + return left === right; + }; + + Node.prototype._evaluateNotEqualsStrict = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + if ((right instanceof Cartesian2) && (left instanceof Cartesian2) || + (right instanceof Cartesian3) && (left instanceof Cartesian3) || + (right instanceof Cartesian4) && (left instanceof Cartesian4)) { + return !left.equals(right); + } + return left !== right; + }; + + Node.prototype._evaluateConditional = function(frameState, feature) { + var test = this._test.evaluate(frameState, feature); + + if (typeof test !== 'boolean') { + throw new RuntimeError('Conditional argument of conditional expression must be a boolean. Argument is ' + test + '.'); + } + + if (test) { + return this._left.evaluate(frameState, feature); + } + return this._right.evaluate(frameState, feature); + }; + + Node.prototype._evaluateNaN = function(frameState, feature) { + return isNaN(this._left.evaluate(frameState, feature)); + }; + + Node.prototype._evaluateIsFinite = function(frameState, feature) { + return isFinite(this._left.evaluate(frameState, feature)); + }; + + Node.prototype._evaluateIsExactClass = function(frameState, feature) { + return feature.isExactClass(this._left.evaluate(frameState, feature)); + }; + + Node.prototype._evaluateIsClass = function(frameState, feature) { + return feature.isClass(this._left.evaluate(frameState, feature)); + }; + + Node.prototype._evaluategetExactClassName = function(frameState, feature) { + return feature.getExactClassName(); + }; + + Node.prototype._evaluateBooleanConversion = function(frameState, feature) { + return Boolean(this._left.evaluate(frameState, feature)); + }; + + Node.prototype._evaluateNumberConversion = function(frameState, feature) { + return Number(this._left.evaluate(frameState, feature)); + }; + + Node.prototype._evaluateStringConversion = function(frameState, feature) { + return String(this._left.evaluate(frameState, feature)); + }; + + Node.prototype._evaluateRegExp = function(frameState, feature) { + var pattern = this._value.evaluate(frameState, feature); + var flags = ''; + + if (defined(this._left)) { + flags = this._left.evaluate(frameState, feature); + } + + var exp; + try { + exp = new RegExp(pattern, flags); + } catch (e) { + throw new RuntimeError(e); + } + return exp; + }; + + Node.prototype._evaluateRegExpTest = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if (!((left instanceof RegExp) && (typeof right === 'string'))) { + throw new RuntimeError('RegExp.test requires the first argument to be a RegExp and the second argument to be a string. Arguments are ' + left + ' and ' + right + '.'); + } + + return left.test(right); + }; + + Node.prototype._evaluateRegExpMatch = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if ((left instanceof RegExp) && (typeof right === 'string')) { + return left.test(right); + } else if ((right instanceof RegExp) && (typeof left === 'string')) { + return right.test(left); + } + + throw new RuntimeError('Operator "=~" requires one RegExp argument and one string argument. Arguments are ' + left + ' and ' + right + '.'); + }; + + Node.prototype._evaluateRegExpNotMatch = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if ((left instanceof RegExp) && (typeof right === 'string')) { + return !(left.test(right)); + } else if ((right instanceof RegExp) && (typeof left === 'string')) { + return !(right.test(left)); + } + + throw new RuntimeError('Operator "!~" requires one RegExp argument and one string argument. Arguments are ' + left + ' and ' + right + '.'); + }; + + Node.prototype._evaluateRegExpExec = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + var right = this._right.evaluate(frameState, feature); + + if (!((left instanceof RegExp) && (typeof right === 'string'))) { + throw new RuntimeError('RegExp.exec requires the first argument to be a RegExp and the second argument to be a string. Arguments are ' + left + ' and ' + right + '.'); + } + + var exec = left.exec(right); + if (!defined(exec)) { + return null; + } + return exec[1]; + }; + + Node.prototype._evaluateToString = function(frameState, feature) { + var left = this._left.evaluate(frameState, feature); + if ((left instanceof RegExp) || (left instanceof Cartesian2) || (left instanceof Cartesian3) || (left instanceof Cartesian4)) { + return String(left); + } + + throw new RuntimeError('Unexpected function call "' + this._value + '".'); + }; + + function convertHSLToRGB(ast) { + // Check if the color contains any nested expressions to see if the color can be converted here. + // E.g. "hsl(0.9, 0.6, 0.7)" is able to convert directly to rgb, "hsl(0.9, 0.6, ${Height})" is not. + var channels = ast._left; + var length = channels.length; + for (var i = 0; i < length; ++i) { + if (channels[i]._type !== ExpressionNodeType.LITERAL_NUMBER) { + return undefined; + } + } + var h = channels[0]._value; + var s = channels[1]._value; + var l = channels[2]._value; + var a = (length === 4) ? channels[3]._value : 1.0; + return Color.fromHsl(h, s, l, a, scratchColor); + } + + function convertRGBToColor(ast) { + // Check if the color contains any nested expressions to see if the color can be converted here. + // E.g. "rgb(255, 255, 255)" is able to convert directly to Color, "rgb(255, 255, ${Height})" is not. + var channels = ast._left; + var length = channels.length; + for (var i = 0; i < length; ++i) { + if (channels[i]._type !== ExpressionNodeType.LITERAL_NUMBER) { + return undefined; + } + } + var color = scratchColor; + color.red = channels[0]._value / 255.0; + color.green = channels[1]._value / 255.0; + color.blue = channels[2]._value / 255.0; + color.alpha = (length === 4) ? channels[3]._value : 1.0; + return color; + } + + function numberToString(number) { + if (number % 1 === 0) { + // Add a .0 to whole numbers + return number.toFixed(1); + } + + return number.toString(); + } + + function colorToVec3(color) { + var r = numberToString(color.red); + var g = numberToString(color.green); + var b = numberToString(color.blue); + return 'vec3(' + r + ', ' + g + ', ' + b + ')'; + } + + function colorToVec4(color) { + var r = numberToString(color.red); + var g = numberToString(color.green); + var b = numberToString(color.blue); + var a = numberToString(color.alpha); + return 'vec4(' + r + ', ' + g + ', ' + b + ', ' + a + ')'; + } + + function getExpressionArray(array, attributePrefix, shaderState, parent) { + var length = array.length; + var expressions = new Array(length); + for (var i = 0; i < length; ++i) { + expressions[i] = array[i].getShaderExpression(attributePrefix, shaderState, parent); + } + return expressions; + } + + Node.prototype.getShaderExpression = function(attributePrefix, shaderState, parent) { + var color; + var left; + var right; + var test; + + var type = this._type; + var value = this._value; + + if (defined(this._left)) { + if (isArray(this._left)) { + // Left can be an array if the type is LITERAL_COLOR or LITERAL_VECTOR + left = getExpressionArray(this._left, attributePrefix, shaderState, this); + } else { + left = this._left.getShaderExpression(attributePrefix, shaderState, this); + } + } + + if (defined(this._right)) { + right = this._right.getShaderExpression(attributePrefix, shaderState, this); + } + + if (defined(this._test)) { + test = this._test.getShaderExpression(attributePrefix, shaderState, this); + } + + if (isArray(this._value)) { + // For ARRAY type + value = getExpressionArray(this._value, attributePrefix, shaderState, this); + } + + switch (type) { + case ExpressionNodeType.VARIABLE: + return attributePrefix + value; + case ExpressionNodeType.UNARY: + // Supported types: +, -, !, Boolean, Number + if (value === 'Boolean') { + return 'bool(' + left + ')'; + } else if (value === 'Number') { + return 'float(' + left + ')'; + } else if (value === 'round') { + return 'floor(' + left + ' + 0.5)'; + } else if (defined(unaryFunctions[value])) { + return value + '(' + left + ')'; + } else if ((value === 'isNaN') || (value === 'isFinite') || (value === 'String') || (value === 'isExactClass') || (value === 'isClass') || (value === 'getExactClassName')) { + throw new RuntimeError('Error generating style shader: "' + value + '" is not supported.'); + } else if (defined(unaryFunctions[value])) { + return value + '(' + left + ')'; + } + return value + left; + case ExpressionNodeType.BINARY: + // Supported types: ||, &&, ===, !==, <, >, <=, >=, +, -, *, /, % + if (value === '%') { + return 'mod(' + left + ', ' + right + ')'; + } else if (value === '===') { + return '(' + left + ' == ' + right + ')'; + } else if (value === '!==') { + return '(' + left + ' != ' + right + ')'; + } else if (value === 'atan2') { + return 'atan(' + left + ', ' + right + ')'; + } else if (defined(binaryFunctions[value])) { + return value + '(' + left + ', ' + right + ')'; + } + return '(' + left + ' ' + value + ' ' + right + ')'; + case ExpressionNodeType.TERNARY: + if (defined(ternaryFunctions[value])) { + return value + '(' + left + ', ' + right + ', ' + test + ')'; + } + break; + case ExpressionNodeType.CONDITIONAL: + return '(' + test + ' ? ' + left + ' : ' + right + ')'; + case ExpressionNodeType.MEMBER: + // This is intended for accessing the components of vector properties. String members aren't supported. + // Check for 0.0 rather than 0 because all numbers are previously converted to decimals. + if (right === 'r' || right === 'x' || right === '0.0') { + return left + '[0]'; + } else if (right === 'g' || right === 'y' || right === '1.0') { + return left + '[1]'; + } else if (right === 'b' || right === 'z' || right === '2.0') { + return left + '[2]'; + } else if (right === 'a' || right === 'w' || right === '3.0') { + return left + '[3]'; + } + return left + '[int(' + right + ')]'; + case ExpressionNodeType.FUNCTION_CALL: + throw new RuntimeError('Error generating style shader: "' + value + '" is not supported.'); + case ExpressionNodeType.ARRAY: + if (value.length === 4) { + return 'vec4(' + value[0] + ', ' + value[1] + ', ' + value[2] + ', ' + value[3] + ')'; + } else if (value.length === 3) { + return 'vec3(' + value[0] + ', ' + value[1] + ', ' + value[2] + ')'; + } else if (value.length === 2) { + return 'vec2(' + value[0] + ', ' + value[1] + ')'; + } + throw new RuntimeError('Error generating style shader: Invalid array length. Array length should be 2, 3, or 4.'); + case ExpressionNodeType.REGEX: + throw new RuntimeError('Error generating style shader: Regular expressions are not supported.'); + case ExpressionNodeType.VARIABLE_IN_STRING: + throw new RuntimeError('Error generating style shader: Converting a variable to a string is not supported.'); + case ExpressionNodeType.LITERAL_NULL: + throw new RuntimeError('Error generating style shader: null is not supported.'); + case ExpressionNodeType.LITERAL_BOOLEAN: + return value ? 'true' : 'false'; + case ExpressionNodeType.LITERAL_NUMBER: + return numberToString(value); + case ExpressionNodeType.LITERAL_STRING: + if (defined(parent) && (parent._type === ExpressionNodeType.MEMBER)) { + if (value === 'r' || value === 'g' || value === 'b' || value === 'a' || + value === 'x' || value === 'y' || value === 'z' || value === 'w') { + return value; + } + } + // Check for css color strings + color = Color.fromCssColorString(value, scratchColor); + if (defined(color)) { + return colorToVec3(color); + } + throw new RuntimeError('Error generating style shader: String literals are not supported.'); + case ExpressionNodeType.LITERAL_COLOR: + var args = left; + if (value === 'color') { + if (!defined(args)) { + return 'vec4(1.0)'; + } else if (args.length > 1) { + var rgb = args[0]; + var alpha = args[1]; + if (alpha !== '1.0') { + shaderState.translucent = true; + } + return 'vec4(' + rgb + ', ' + alpha + ')'; + } + return 'vec4(' + args[0] + ', 1.0)'; + } else if (value === 'rgb') { + color = convertRGBToColor(this); + if (defined(color)) { + return colorToVec4(color); + } + return 'vec4(' + args[0] + ' / 255.0, ' + args[1] + ' / 255.0, ' + args[2] + ' / 255.0, 1.0)'; + } else if (value === 'rgba') { + if (args[3] !== '1.0') { + shaderState.translucent = true; + } + color = convertRGBToColor(this); + if (defined(color)) { + return colorToVec4(color); + } + return 'vec4(' + args[0] + ' / 255.0, ' + args[1] + ' / 255.0, ' + args[2] + ' / 255.0, ' + args[3] + ')'; + } else if (value === 'hsl') { + color = convertHSLToRGB(this); + if (defined(color)) { + return colorToVec4(color); + } + return 'vec4(czm_HSLToRGB(vec3(' + args[0] + ', ' + args[1] + ', ' + args[2] + ')), 1.0)'; + } else if (value === 'hsla') { + color = convertHSLToRGB(this); + if (defined(color)) { + if (color.alpha !== 1.0) { + shaderState.translucent = true; + } + return colorToVec4(color); + } + if (args[3] !== '1.0') { + shaderState.translucent = true; + } + return 'vec4(czm_HSLToRGB(vec3(' + args[0] + ', ' + args[1] + ', ' + args[2] + ')), ' + args[3] + ')'; + } + break; + case ExpressionNodeType.LITERAL_VECTOR: + var length = left.length; + var vectorExpression = value + '('; + for (var i = 0; i < length; ++i) { + vectorExpression += left[i]; + if (i < (length - 1)) { + vectorExpression += ', '; + } + } + vectorExpression += ')'; + return vectorExpression; + case ExpressionNodeType.LITERAL_REGEX: + throw new RuntimeError('Error generating style shader: Regular expressions are not supported.'); + case ExpressionNodeType.LITERAL_UNDEFINED: + throw new RuntimeError('Error generating style shader: undefined is not supported.'); + case ExpressionNodeType.BUILTIN_VARIABLE: + if (value === 'tiles3d_tileset_time') { + return 'u_tilesetTime'; + } + } + }; + + return Expression; +}); diff --git a/Source/Scene/ExpressionNodeType.js b/Source/Scene/ExpressionNodeType.js new file mode 100644 index 000000000000..b6529ef93632 --- /dev/null +++ b/Source/Scene/ExpressionNodeType.js @@ -0,0 +1,34 @@ +/*global define*/ +define([ + '../Core/freezeObject' +], function( + freezeObject) { + 'use strict'; + + /** + * @private + */ + var ExpressionNodeType = { + VARIABLE : 0, + UNARY : 1, + BINARY : 2, + TERNARY : 3, + CONDITIONAL : 4, + MEMBER : 5, + FUNCTION_CALL : 6, + ARRAY : 7, + REGEX: 8, + VARIABLE_IN_STRING : 9, + LITERAL_NULL : 10, + LITERAL_BOOLEAN : 11, + LITERAL_NUMBER : 12, + LITERAL_STRING : 13, + LITERAL_COLOR : 14, + LITERAL_VECTOR : 15, + LITERAL_REGEX : 16, + LITERAL_UNDEFINED : 17, + BUILTIN_VARIABLE : 18 + }; + + return freezeObject(ExpressionNodeType); +}); diff --git a/Source/Scene/FrameState.js b/Source/Scene/FrameState.js index 962ed8ea890f..da3a84c4d2bb 100644 --- a/Source/Scene/FrameState.js +++ b/Source/Scene/FrameState.js @@ -9,7 +9,7 @@ define([ * State information about the current frame. An instance of this class * is provided to update functions. * - * @param {Context} context The rendering context. + * @param {Context} context The rendering context * @param {CreditDisplay} creditDisplay Handles adding and removing credits from an HTML element * @param {JobScheduler} jobScheduler The job scheduler * @@ -21,12 +21,14 @@ define([ function FrameState(context, creditDisplay, jobScheduler) { /** * The rendering context. + * * @type {Context} */ this.context = context; /** * An array of rendering commands. + * * @type {DrawCommand[]} */ this.commandList = []; @@ -39,6 +41,7 @@ define([ /** * The current mode of the scene. + * * @type {SceneMode} * @default {@link SceneMode.SCENE3D} */ @@ -85,6 +88,7 @@ define([ /** * The current camera. + * * @type {Camera} * @default undefined */ @@ -92,6 +96,7 @@ define([ /** * The culling volume. + * * @type {CullingVolume} * @default undefined */ @@ -99,6 +104,7 @@ define([ /** * The current occluder. + * * @type {Occluder} * @default undefined */ @@ -116,12 +122,14 @@ define([ this.passes = { /** * true if the primitive should update for a render pass, false otherwise. + * * @type {Boolean} * @default false */ render : false, /** * true if the primitive should update for a picking pass, false otherwise. + * * @type {Boolean} * @default false */ @@ -137,6 +145,7 @@ define([ /** * The credit display. + * * @type {CreditDisplay} */ this.creditDisplay = creditDisplay; @@ -162,6 +171,7 @@ define([ /** * Gets whether or not to optimized for 3D only. + * * @type {Boolean} * @default false */ @@ -176,12 +186,14 @@ define([ enabled : false, /** * A positive number used to mix the color and fog color based on camera distance. + * * @type {Number} * @default undefined */ density : undefined, /** * A scalar used to modify the screen space error of geometry partially in fog. + * * @type {Number} * @default undefined */ @@ -275,12 +287,9 @@ define([ this.minimumDisableDepthTestDistance = undefined; } - FrameState.prototype.addCommand = function(command) { - this.commandList.push(command); - }; - /** * A function that will be called at the end of the frame. + * * @callback FrameState~AfterRenderCallback */ diff --git a/Source/Scene/GoogleEarthImageryProvider.js b/Source/Scene/GoogleEarthImageryProvider.js index 010d653db2b0..cca47c60bf32 100644 --- a/Source/Scene/GoogleEarthImageryProvider.js +++ b/Source/Scene/GoogleEarthImageryProvider.js @@ -34,4 +34,3 @@ define([ return GoogleEarthImageryProvider; }); - diff --git a/Source/Scene/Instanced3DModel3DTileContent.js b/Source/Scene/Instanced3DModel3DTileContent.js new file mode 100644 index 000000000000..a4c347774a88 --- /dev/null +++ b/Source/Scene/Instanced3DModel3DTileContent.js @@ -0,0 +1,523 @@ +/*global define*/ +define([ + '../Core/AttributeCompression', + '../Core/Cartesian3', + '../Core/Color', + '../Core/ComponentDatatype', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/DeveloperError', + '../Core/FeatureDetection', + '../Core/Ellipsoid', + '../Core/getAbsoluteUri', + '../Core/getBaseUri', + '../Core/getStringFromTypedArray', + '../Core/joinUrls', + '../Core/Matrix3', + '../Core/Matrix4', + '../Core/Quaternion', + '../Core/RequestType', + '../Core/RuntimeError', + '../Core/Transforms', + '../Core/TranslationRotationScale', + './Cesium3DTileBatchTable', + './Cesium3DTileFeature', + './Cesium3DTileFeatureTable', + './ModelInstanceCollection' + ], function( + AttributeCompression, + Cartesian3, + Color, + ComponentDatatype, + defaultValue, + defined, + defineProperties, + destroyObject, + DeveloperError, + FeatureDetection, + Ellipsoid, + getAbsoluteUri, + getBaseUri, + getStringFromTypedArray, + joinUrls, + Matrix3, + Matrix4, + Quaternion, + RequestType, + RuntimeError, + Transforms, + TranslationRotationScale, + Cesium3DTileBatchTable, + Cesium3DTileFeature, + Cesium3DTileFeatureTable, + ModelInstanceCollection) { + 'use strict'; + + // Bail out if the browser doesn't support typed arrays, to prevent the setup function + // from failing, since we won't be able to create a WebGL context anyway. + if (!FeatureDetection.supportsTypedArrays()) { + return {}; + } + + /** + * Represents the contents of a + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Instanced3DModel/README.md|Instanced 3D Model} + * tile in a {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles} tileset. + *

+ * Implements the {@link Cesium3DTileContent} interface. + *

+ * + * @alias Instanced3DModel3DTileContent + * @constructor + * + * @private + */ + function Instanced3DModel3DTileContent(tileset, tile, url, arrayBuffer, byteOffset) { + this._tileset = tileset; + this._tile = tile; + this._url = url; + this._modelInstanceCollection = undefined; + this._batchTable = undefined; + this._features = undefined; + + /** + * @inheritdoc Cesium3DTileContent#featurePropertiesDirty + */ + this.featurePropertiesDirty = false; + + initialize(this, arrayBuffer, byteOffset); + } + + defineProperties(Instanced3DModel3DTileContent.prototype, { + /** + * @inheritdoc Cesium3DTileContent#featuresLength + */ + featuresLength : { + get : function() { + return this._batchTable.featuresLength; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#pointsLength + */ + pointsLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#trianglesLength + */ + trianglesLength : { + get : function() { + var model = this._modelInstanceCollection._model; + if (defined(model)) { + return model.trianglesLength; + } + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#geometryByteLength + */ + geometryByteLength : { + get : function() { + var model = this._modelInstanceCollection._model; + if (defined(model)) { + return model.geometryByteLength; + } + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#texturesByteLength + */ + texturesByteLength : { + get : function() { + var model = this._modelInstanceCollection._model; + if (defined(model)) { + return model.texturesByteLength; + } + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTableByteLength + */ + batchTableByteLength : { + get : function() { + return this._batchTable.memorySizeInBytes; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#innerContents + */ + innerContents : { + get : function() { + return undefined; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#readyPromise + */ + readyPromise : { + get : function() { + return this._modelInstanceCollection.readyPromise; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tileset + */ + tileset : { + get : function() { + return this._tileset; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tile + */ + tile : { + get : function() { + return this._tile; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#url + */ + url: { + get: function() { + return this._url; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTable + */ + batchTable : { + get : function() { + return this._batchTable; + } + } + }); + + var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; + var propertyScratch1 = new Array(4); + var propertyScratch2 = new Array(4); + + function initialize(content, arrayBuffer, byteOffset) { + var byteStart = defaultValue(byteOffset, 0); + byteOffset = byteStart; + + var uint8Array = new Uint8Array(arrayBuffer); + var view = new DataView(arrayBuffer); + byteOffset += sizeOfUint32; // Skip magic + + var version = view.getUint32(byteOffset, true); + if (version !== 1) { + throw new RuntimeError('Only Instanced 3D Model version 1 is supported. Version ' + version + ' is not.'); + } + byteOffset += sizeOfUint32; + + var byteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var featureTableJsonByteLength = view.getUint32(byteOffset, true); + if (featureTableJsonByteLength === 0) { + throw new RuntimeError('featureTableJsonByteLength is zero, the feature table must be defined.'); + } + byteOffset += sizeOfUint32; + + var featureTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var batchTableJsonByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var batchTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var gltfFormat = view.getUint32(byteOffset, true); + if (gltfFormat !== 1 && gltfFormat !== 0) { + throw new RuntimeError('Only glTF format 0 (uri) or 1 (embedded) are supported. Format ' + gltfFormat + ' is not.'); + } + byteOffset += sizeOfUint32; + + var featureTableString = getStringFromTypedArray(uint8Array, byteOffset, featureTableJsonByteLength); + var featureTableJson = JSON.parse(featureTableString); + byteOffset += featureTableJsonByteLength; + + var featureTableBinary = new Uint8Array(arrayBuffer, byteOffset, featureTableBinaryByteLength); + byteOffset += featureTableBinaryByteLength; + + var featureTable = new Cesium3DTileFeatureTable(featureTableJson, featureTableBinary); + var instancesLength = featureTable.getGlobalProperty('INSTANCES_LENGTH'); + featureTable.featuresLength = instancesLength; + + if (!defined(instancesLength)) { + throw new RuntimeError('Feature table global property: INSTANCES_LENGTH must be defined'); + } + + var batchTableJson; + var batchTableBinary; + if (batchTableJsonByteLength > 0) { + var batchTableString = getStringFromTypedArray(uint8Array, byteOffset, batchTableJsonByteLength); + batchTableJson = JSON.parse(batchTableString); + byteOffset += batchTableJsonByteLength; + + if (batchTableBinaryByteLength > 0) { + // Has a batch table binary + batchTableBinary = new Uint8Array(arrayBuffer, byteOffset, batchTableBinaryByteLength); + // Copy the batchTableBinary section and let the underlying ArrayBuffer be freed + batchTableBinary = new Uint8Array(batchTableBinary); + byteOffset += batchTableBinaryByteLength; + } + } + + content._batchTable = new Cesium3DTileBatchTable(content, instancesLength, batchTableJson, batchTableBinary); + + var gltfByteLength = byteStart + byteLength - byteOffset; + if (gltfByteLength === 0) { + throw new RuntimeError('glTF byte length is zero, i3dm must have a glTF to instance.'); + } + var gltfView = new Uint8Array(arrayBuffer, byteOffset, gltfByteLength); + byteOffset += gltfByteLength; + + // Create model instance collection + var collectionOptions = { + instances : new Array(instancesLength), + batchTable : content._batchTable, + cull : false, // Already culled by 3D Tiles + url : undefined, + requestType : RequestType.TILES3D, + gltf : undefined, + basePath : undefined, + incrementallyLoadTextures : false, + upAxis : content._tileset._gltfUpAxis + }; + + if (gltfFormat === 0) { + var gltfUrl = getStringFromTypedArray(gltfView); + collectionOptions.url = getAbsoluteUri(joinUrls(getBaseUri(content._url, true), gltfUrl)); + } else { + collectionOptions.gltf = gltfView; + collectionOptions.basePath = getAbsoluteUri(getBaseUri(content._url, true)); + } + + var eastNorthUp = featureTable.getGlobalProperty('EAST_NORTH_UP'); + + var rtcCenter; + var rtcCenterArray = featureTable.getGlobalProperty('RTC_CENTER', ComponentDatatype.FLOAT, 3); + if (defined(rtcCenterArray)) { + rtcCenter = Cartesian3.unpack(rtcCenterArray); + } + + var instances = collectionOptions.instances; + var instancePosition = new Cartesian3(); + var instancePositionArray = new Array(3); + var instanceNormalRight = new Cartesian3(); + var instanceNormalUp = new Cartesian3(); + var instanceNormalForward = new Cartesian3(); + var instanceRotation = new Matrix3(); + var instanceQuaternion = new Quaternion(); + var instanceScale = new Cartesian3(); + var instanceTranslationRotationScale = new TranslationRotationScale(); + var instanceTransform = new Matrix4(); + for (var i = 0; i < instancesLength; i++) { + // Get the instance position + var position = featureTable.getProperty('POSITION', ComponentDatatype.FLOAT, 3, i, propertyScratch1); + if (!defined(position)) { + position = instancePositionArray; + var positionQuantized = featureTable.getProperty('POSITION_QUANTIZED', ComponentDatatype.UNSIGNED_SHORT, 3, i, propertyScratch1); + if (!defined(positionQuantized)) { + throw new RuntimeError('Either POSITION or POSITION_QUANTIZED must be defined for each instance.'); + } + var quantizedVolumeOffset = featureTable.getGlobalProperty('QUANTIZED_VOLUME_OFFSET', ComponentDatatype.FLOAT, 3); + if (!defined(quantizedVolumeOffset)) { + throw new RuntimeError('Global property: QUANTIZED_VOLUME_OFFSET must be defined for quantized positions.'); + } + var quantizedVolumeScale = featureTable.getGlobalProperty('QUANTIZED_VOLUME_SCALE', ComponentDatatype.FLOAT, 3); + if (!defined(quantizedVolumeScale)) { + throw new RuntimeError('Global property: QUANTIZED_VOLUME_SCALE must be defined for quantized positions.'); + } + for (var j = 0; j < 3; j++) { + position[j] = (positionQuantized[j] / 65535.0 * quantizedVolumeScale[j]) + quantizedVolumeOffset[j]; + } + } + Cartesian3.unpack(position, 0, instancePosition); + if (defined(rtcCenter)) { + Cartesian3.add(instancePosition, rtcCenter, instancePosition); + } + instanceTranslationRotationScale.translation = instancePosition; + + // Get the instance rotation + var normalUp = featureTable.getProperty('NORMAL_UP', ComponentDatatype.FLOAT, 3, i, propertyScratch1); + var normalRight = featureTable.getProperty('NORMAL_RIGHT', ComponentDatatype.FLOAT, 3, i, propertyScratch2); + var hasCustomOrientation = false; + if (defined(normalUp)) { + if (!defined(normalRight)) { + throw new RuntimeError('To define a custom orientation, both NORMAL_UP and NORMAL_RIGHT must be defined.'); + } + Cartesian3.unpack(normalUp, 0, instanceNormalUp); + Cartesian3.unpack(normalRight, 0, instanceNormalRight); + hasCustomOrientation = true; + } else { + var octNormalUp = featureTable.getProperty('NORMAL_UP_OCT32P', ComponentDatatype.UNSIGNED_SHORT, 2, i, propertyScratch1); + var octNormalRight = featureTable.getProperty('NORMAL_RIGHT_OCT32P', ComponentDatatype.UNSIGNED_SHORT, 2, i, propertyScratch2); + if (defined(octNormalUp)) { + if (!defined(octNormalRight)) { + throw new RuntimeError('To define a custom orientation with oct-encoded vectors, both NORMAL_UP_OCT32P and NORMAL_RIGHT_OCT32P must be defined.'); + } + AttributeCompression.octDecodeInRange(octNormalUp[0], octNormalUp[1], 65535, instanceNormalUp); + AttributeCompression.octDecodeInRange(octNormalRight[0], octNormalRight[1], 65535, instanceNormalRight); + hasCustomOrientation = true; + } else if (eastNorthUp) { + Transforms.eastNorthUpToFixedFrame(instancePosition, Ellipsoid.WGS84, instanceTransform); + Matrix4.getRotation(instanceTransform, instanceRotation); + } else { + Matrix3.clone(Matrix3.IDENTITY, instanceRotation); + } + } + if (hasCustomOrientation) { + Cartesian3.cross(instanceNormalRight, instanceNormalUp, instanceNormalForward); + Cartesian3.normalize(instanceNormalForward, instanceNormalForward); + Matrix3.setColumn(instanceRotation, 0, instanceNormalRight, instanceRotation); + Matrix3.setColumn(instanceRotation, 1, instanceNormalUp, instanceRotation); + Matrix3.setColumn(instanceRotation, 2, instanceNormalForward, instanceRotation); + } + Quaternion.fromRotationMatrix(instanceRotation, instanceQuaternion); + instanceTranslationRotationScale.rotation = instanceQuaternion; + + // Get the instance scale + instanceScale = Cartesian3.fromElements(1.0, 1.0, 1.0, instanceScale); + var scale = featureTable.getProperty('SCALE', ComponentDatatype.FLOAT, 1, i); + if (defined(scale)) { + Cartesian3.multiplyByScalar(instanceScale, scale, instanceScale); + } + var nonUniformScale = featureTable.getProperty('SCALE_NON_UNIFORM', ComponentDatatype.FLOAT, 3, i, propertyScratch1); + if (defined(nonUniformScale)) { + instanceScale.x *= nonUniformScale[0]; + instanceScale.y *= nonUniformScale[1]; + instanceScale.z *= nonUniformScale[2]; + } + instanceTranslationRotationScale.scale = instanceScale; + + // Get the batchId + var batchId = featureTable.getProperty('BATCH_ID', ComponentDatatype.UNSIGNED_SHORT, 1, i); + if (!defined(batchId)) { + // If BATCH_ID semantic is undefined, batchId is just the instance number + batchId = i; + } + + // Create the model matrix and the instance + Matrix4.fromTranslationRotationScale(instanceTranslationRotationScale, instanceTransform); + var modelMatrix = instanceTransform.clone(); + instances[i] = { + modelMatrix : modelMatrix, + batchId : batchId + }; + } + + content._modelInstanceCollection = new ModelInstanceCollection(collectionOptions); + } + + function createFeatures(content) { + var tileset = content._tileset; + var featuresLength = content.featuresLength; + if (!defined(content._features) && (featuresLength > 0)) { + var features = new Array(featuresLength); + for (var i = 0; i < featuresLength; ++i) { + features[i] = new Cesium3DTileFeature(tileset, content, i); + } + content._features = features; + } + } + + /** + * @inheritdoc Cesium3DTileContent#hasProperty + */ + Instanced3DModel3DTileContent.prototype.hasProperty = function(batchId, name) { + return this._batchTable.hasProperty(batchId, name); + }; + + /** + * @inheritdoc Cesium3DTileContent#getFeature + */ + Instanced3DModel3DTileContent.prototype.getFeature = function(batchId) { + var featuresLength = this.featuresLength; + //>>includeStart('debug', pragmas.debug); + if (!defined(batchId) || (batchId < 0) || (batchId >= featuresLength)) { + throw new DeveloperError('batchId is required and between zero and featuresLength - 1 (' + (featuresLength - 1) + ').'); + } + //>>includeEnd('debug'); + + createFeatures(this); + return this._features[batchId]; + }; + + /** + * @inheritdoc Cesium3DTileContent#applyDebugSettings + */ + Instanced3DModel3DTileContent.prototype.applyDebugSettings = function(enabled, color) { + color = enabled ? color : Color.WHITE; + this._batchTable.setAllColor(color); + }; + + /** + * @inheritdoc Cesium3DTileContent#applyStyle + */ + Instanced3DModel3DTileContent.prototype.applyStyle = function(frameState, style) { + this._batchTable.applyStyle(frameState, style); + }; + + /** + * @inheritdoc Cesium3DTileContent#update + */ + Instanced3DModel3DTileContent.prototype.update = function(tileset, frameState) { + var commandStart = frameState.commandList.length; + + // In the PROCESSING state we may be calling update() to move forward + // the content's resource loading. In the READY state, it will + // actually generate commands. + this._batchTable.update(tileset, frameState); + this._modelInstanceCollection.modelMatrix = this._tile.computedTransform; + this._modelInstanceCollection.shadows = this._tileset.shadows; + this._modelInstanceCollection.debugWireframe = this._tileset.debugWireframe; + this._modelInstanceCollection.update(frameState); + + // If any commands were pushed, add derived commands + var commandEnd = frameState.commandList.length; + if ((commandStart < commandEnd) && frameState.passes.render) { + this._batchTable.addDerivedCommands(frameState, commandStart); + } + }; + + /** + * @inheritdoc Cesium3DTileContent#isDestroyed + */ + Instanced3DModel3DTileContent.prototype.isDestroyed = function() { + return false; + }; + + /** + * @inheritdoc Cesium3DTileContent#destroy + */ + Instanced3DModel3DTileContent.prototype.destroy = function() { + this._modelInstanceCollection = this._modelInstanceCollection && this._modelInstanceCollection.destroy(); + this._batchTable = this._batchTable && this._batchTable.destroy(); + + return destroyObject(this); + }; + return Instanced3DModel3DTileContent; +}); diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 065408ca19fd..01f858ad35c8 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -53,6 +53,7 @@ define([ '../ThirdParty/gltfDefaults', '../ThirdParty/Uri', '../ThirdParty/when', + './AttributeType', './Axis', './BlendingState', './ColorBlendMode', @@ -122,6 +123,7 @@ define([ gltfDefaults, Uri, when, + AttributeType, Axis, BlendingState, ColorBlendMode, @@ -242,7 +244,9 @@ define([ // Note that this is a global cache, compared to renderer resources, which // are cached per context. function CachedGltf(options) { - this._gltf = modelMaterialsCommon(gltfDefaults(options.gltf)); + this._gltf = modelMaterialsCommon(gltfDefaults(options.gltf), { + addBatchIdToGeneratedShaders : options.addBatchIdToGeneratedShaders + }); this._bgltf = options.bgltf; this.ready = options.ready; this.modelsToLoad = []; @@ -341,6 +345,7 @@ define([ * @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the model in wireframe. * @param {HeightReference} [options.heightReference] Determines how the model is drawn relative to terrain. * @param {Scene} [options.scene] Must be passed in for models that use the height reference property. + * @param {Boolean} [options.addBatchIdToGeneratedShaders=false] Determines if shaders generated for materials using the KHR_materials_common extension should include a batchId attribute. For models contained in b3dm tiles. * @param {DistanceDisplayCondition} [options.distanceDisplayCondition] The condition specifying at what distance from the camera that this model will be displayed. * @param {Color} [options.color=Color.WHITE] A color that blends with the model's rendered color. * @param {ColorBlendMode} [options.colorBlendMode=ColorBlendMode.HIGHLIGHT] Defines how the color blends with the model. @@ -390,13 +395,15 @@ define([ cachedGltf = new CachedGltf({ gltf : result.glTF, bgltf : gltf, - ready : true + ready : true, + addBatchIdToGeneratedShaders : options.addBatchIdToGeneratedShaders }); } else { // Normal glTF (JSON) cachedGltf = new CachedGltf({ gltf : options.gltf, - ready : true + ready : true, + addBatchIdToGeneratedShaders : options.addBatchIdToGeneratedShaders }); } @@ -533,8 +540,7 @@ define([ * * @private */ - this.pickPrimitive = options.pickPrimitive; - + this._pickObject = options.pickObject; this._allowPicking = defaultValue(options.allowPicking, true); this._ready = false; @@ -630,6 +636,7 @@ define([ this._pickFragmentShaderLoaded = options.pickFragmentShaderLoaded; this._pickUniformMapLoaded = options.pickUniformMapLoaded; this._ignoreCommands = defaultValue(options.ignoreCommands, false); + this._requestType = options.requestType; this._upAxis = defaultValue(options.upAxis, Axis.Y); /** @@ -680,10 +687,10 @@ define([ this._cachedRendererResources = undefined; this._loadRendererResourcesFromCache = false; - this._cachedVertexMemorySizeInBytes = 0; - this._cachedTextureMemorySizeInBytes = 0; - this._vertexMemorySizeInBytes = 0; - this._textureMemorySizeInBytes = 0; + this._cachedGeometryByteLength = 0; + this._cachedTexturesByteLength = 0; + this._geometryByteLength = 0; + this._texturesByteLength = 0; this._trianglesLength = 0; this._nodeCommands = []; @@ -1001,13 +1008,13 @@ define([ }, /** - * Gets the model's vertex memory in bytes. This includes all vertex and index buffers. + * Gets the model's geometry memory in bytes. This includes all vertex and index buffers. * * @private */ - vertexMemorySizeInBytes : { + geometryByteLength : { get : function() { - return this._vertexMemorySizeInBytes; + return this._geometryByteLength; } }, @@ -1016,20 +1023,20 @@ define([ * * @private */ - textureMemorySizeInBytes : { + texturesByteLength : { get : function() { - return this._textureMemorySizeInBytes; + return this._texturesByteLength; } }, /** - * Gets the model's cached vertex memory in bytes. This includes all vertex and index buffers. + * Gets the model's cached geometry memory in bytes. This includes all vertex and index buffers. * * @private */ - cachedVertexMemorySizeInBytes : { + cachedGeometryByteLength : { get : function() { - return this._cachedVertexMemorySizeInBytes; + return this._cachedGeometryByteLength; } }, @@ -1038,9 +1045,9 @@ define([ * * @private */ - cachedTextureMemorySizeInBytes : { + cachedTexturesByteLength : { get : function() { - return this._cachedTextureMemorySizeInBytes; + return this._cachedTexturesByteLength; } } }); @@ -1811,7 +1818,7 @@ define([ }); vertexBuffer.vertexArrayDestroyable = false; model._rendererResources.buffers[bufferViewId] = vertexBuffer; - model._vertexMemorySizeInBytes += vertexBuffer.sizeInBytes; + model._geometryByteLength += vertexBuffer.sizeInBytes; } /////////////////////////////////////////////////////////////////////////// @@ -1849,7 +1856,7 @@ define([ }); indexBuffer.vertexArrayDestroyable = false; model._rendererResources.buffers[bufferViewId] = indexBuffer; - model._vertexMemorySizeInBytes += indexBuffer.sizeInBytes; + model._geometryByteLength += indexBuffer.sizeInBytes; } var scratchVertexBufferJob = new CreateVertexBufferJob(); @@ -2359,7 +2366,7 @@ define([ } model._rendererResources.textures[gltfTexture.id] = tx; - model._textureMemorySizeInBytes += tx.sizeInBytes; + model._texturesByteLength += tx.sizeInBytes; } var scratchCreateTextureJob = new CreateTextureJob(); @@ -3256,19 +3263,19 @@ define([ var uniformVariable = 'gltf_u_dec_' + attribute.toLowerCase(); switch (a.type) { - case 'SCALAR': + case AttributeType.SCALAR: uniformMap[uniformVariable] = getMat2UniformFunction(decodeMatrix, model).func; setUniforms[uniformVariable] = true; break; - case 'VEC2': + case AttributeType.VEC2: uniformMap[uniformVariable] = getMat3UniformFunction(decodeMatrix, model).func; setUniforms[uniformVariable] = true; break; - case 'VEC3': + case AttributeType.VEC3: uniformMap[uniformVariable] = getMat4UniformFunction(decodeMatrix, model).func; setUniforms[uniformVariable] = true; break; - case 'VEC4': + case AttributeType.VEC4: // VEC4 attributes are split into scale and translate because there is no mat5 in GLSL var uniformVariableScale = uniformVariable + '_scale'; var uniformVariableTranslate = uniformVariable + '_translate'; @@ -3447,12 +3454,16 @@ define([ // GLTF_SPEC: Offical means to determine translucency. https://github.com/KhronosGroup/glTF/issues/105 var isTranslucent = rs.blending.enabled; - var owner = { - primitive : defaultValue(model.pickPrimitive, model), - id : model.id, - node : runtimeNode.publicNode, - mesh : runtimeMeshesByName[mesh.name] - }; + + var owner = model._pickObject; + if (!defined(owner)) { + owner = { + primitive : model, + id : model.id, + node : runtimeNode.publicNode, + mesh : runtimeMeshesByName[mesh.name] + }; + } var castShadows = ShadowMode.castShadows(model._shadows); var receiveShadows = ShadowMode.receiveShadows(model._shadows); @@ -3627,7 +3638,7 @@ define([ model._runtime.nodes = runtimeNodes; } - function getVertexMemorySizeInBytes(buffers) { + function getGeometryByteLength(buffers) { var memory = 0; for (var id in buffers) { if (buffers.hasOwnProperty(id)) { @@ -3637,7 +3648,7 @@ define([ return memory; } - function getTextureMemorySizeInBytes(textures) { + function getTexturesByteLength(textures) { var memory = 0; for (var id in textures) { if (textures.hasOwnProperty(id)) { @@ -3669,8 +3680,8 @@ define([ createVertexArrays(model, context); } - model._cachedVertexMemorySizeInBytes += getVertexMemorySizeInBytes(cachedResources.buffers); - model._cachedTextureMemorySizeInBytes += getTextureMemorySizeInBytes(cachedResources.textures); + model._cachedGeometryByteLength += getGeometryByteLength(cachedResources.buffers); + model._cachedTexturesByteLength += getTexturesByteLength(cachedResources.textures); } else { createBuffers(model, frameState); // using glTF bufferViews createPrograms(model, frameState); @@ -4653,6 +4664,7 @@ define([ // and then have them visible immediately when show is set to true. if (show && !this._ignoreCommands) { // PERFORMANCE_IDEA: This is terrible + var commandList = frameState.commandList; var passes = frameState.passes; var nodeCommands = this._nodeCommands; var length = nodeCommands.length; @@ -4668,13 +4680,13 @@ define([ if (nc.show) { var command = translucent ? nc.translucentCommand : nc.command; command = silhouette ? nc.silhouetteModelCommand : command; - frameState.addCommand(command); + commandList.push(command); boundingVolume = nc.command.boundingVolume; if (frameState.mode === SceneMode.SCENE2D && (boundingVolume.center.y + boundingVolume.radius > idl2D || boundingVolume.center.y - boundingVolume.radius < idl2D)) { var command2D = translucent ? nc.translucentCommand2D : nc.command2D; command2D = silhouette ? nc.silhouetteModelCommand2D : command2D; - frameState.addCommand(command2D); + commandList.push(command2D); } } } @@ -4684,11 +4696,11 @@ define([ for (i = 0; i < length; ++i) { nc = nodeCommands[i]; if (nc.show) { - frameState.addCommand(nc.silhouetteColorCommand); + commandList.push(nc.silhouetteColorCommand); boundingVolume = nc.command.boundingVolume; if (frameState.mode === SceneMode.SCENE2D && (boundingVolume.center.y + boundingVolume.radius > idl2D || boundingVolume.center.y - boundingVolume.radius < idl2D)) { - frameState.addCommand(nc.silhouetteColorCommand2D); + commandList.push(nc.silhouetteColorCommand2D); } } } @@ -4700,12 +4712,12 @@ define([ nc = nodeCommands[i]; if (nc.show) { var pickCommand = nc.pickCommand; - frameState.addCommand(pickCommand); + commandList.push(pickCommand); boundingVolume = pickCommand.boundingVolume; if (frameState.mode === SceneMode.SCENE2D && (boundingVolume.center.y + boundingVolume.radius > idl2D || boundingVolume.center.y - boundingVolume.radius < idl2D)) { - frameState.addCommand(nc.pickCommand2D); + commandList.push(nc.pickCommand2D); } } } diff --git a/Source/Scene/ModelAnimationCache.js b/Source/Scene/ModelAnimationCache.js index b013c8e584d4..ad78f54b5f18 100644 --- a/Source/Scene/ModelAnimationCache.js +++ b/Source/Scene/ModelAnimationCache.js @@ -1,23 +1,23 @@ /*global define*/ define([ '../Core/Cartesian3', - '../Core/defaultValue', '../Core/defined', '../Core/LinearSpline', '../Core/Matrix4', '../Core/Quaternion', '../Core/QuaternionSpline', '../Core/WebGLConstants', + './AttributeType', './getBinaryAccessor' ], function( Cartesian3, - defaultValue, defined, LinearSpline, Matrix4, Quaternion, QuaternionSpline, WebGLConstants, + AttributeType, getBinaryAccessor) { 'use strict'; @@ -72,15 +72,15 @@ define([ var typedArray = getBinaryAccessor(accessor).createArrayBufferView(buffer.buffer, buffer.byteOffset + accessor.byteOffset, count); var i; - if ((componentType === WebGLConstants.FLOAT) && (type === 'SCALAR')) { + if ((componentType === WebGLConstants.FLOAT) && (type === AttributeType.SCALAR)) { values = typedArray; } - else if ((componentType === WebGLConstants.FLOAT) && (type === 'VEC3')) { + else if ((componentType === WebGLConstants.FLOAT) && (type === AttributeType.VEC3)) { values = new Array(count); for (i = 0; i < count; ++i) { values[i] = Cartesian3.fromArray(typedArray, 3 * i); } - } else if ((componentType === WebGLConstants.FLOAT) && (type === 'VEC4')) { + } else if ((componentType === WebGLConstants.FLOAT) && (type === AttributeType.VEC4)) { values = new Array(count); for (i = 0; i < count; ++i) { var byteOffset = 4 * i; @@ -137,12 +137,12 @@ define([ var type = accessor.type; if (sampler.interpolation === 'LINEAR') { - if ((componentType === WebGLConstants.FLOAT) && (type === 'VEC3')) { + if ((componentType === WebGLConstants.FLOAT) && (type === AttributeType.VEC3)) { spline = new LinearSpline({ times : times, points : controlPoints }); - } else if ((componentType === WebGLConstants.FLOAT) && (type === 'VEC4')) { + } else if ((componentType === WebGLConstants.FLOAT) && (type === AttributeType.VEC4)) { spline = new QuaternionSpline({ times : times, points : controlPoints @@ -185,7 +185,7 @@ define([ var typedArray = getBinaryAccessor(accessor).createArrayBufferView(buffer.buffer, buffer.byteOffset + accessor.byteOffset, count); matrices = new Array(count); - if ((componentType === WebGLConstants.FLOAT) && (type === 'MAT4')) { + if ((componentType === WebGLConstants.FLOAT) && (type === AttributeType.MAT4)) { for (var i = 0; i < count; ++i) { matrices[i] = Matrix4.fromArray(typedArray, 16 * i); } diff --git a/Source/Scene/ModelInstance.js b/Source/Scene/ModelInstance.js new file mode 100644 index 000000000000..399893de4528 --- /dev/null +++ b/Source/Scene/ModelInstance.js @@ -0,0 +1,43 @@ +/*global define*/ +define([ + '../Core/defineProperties', + '../Core/Matrix4' +], function( + defineProperties, + Matrix4) { + 'use strict'; + + /** + * @private + */ + function ModelInstance(collection, modelMatrix, instanceId) { + this.primitive = collection; + this._modelMatrix = Matrix4.clone(modelMatrix); + this._instanceId = instanceId; + } + + defineProperties(ModelInstance.prototype, { + instanceId : { + get : function() { + return this._instanceId; + } + }, + model : { + get : function() { + return this.primitive._model; + } + }, + modelMatrix : { + get : function() { + return Matrix4.clone(this._modelMatrix); + }, + set : function(value) { + Matrix4.clone(value, this._modelMatrix); + this.primitive.expandBoundingSphere(this._modelMatrix); + this.primitive._dirty = true; + } + } + }); + + return ModelInstance; +}); diff --git a/Source/Scene/ModelInstanceCollection.js b/Source/Scene/ModelInstanceCollection.js new file mode 100644 index 000000000000..e76f677a8261 --- /dev/null +++ b/Source/Scene/ModelInstanceCollection.js @@ -0,0 +1,1000 @@ +/*global define*/ +define([ + '../Core/BoundingSphere', + '../Core/Cartesian3', + '../Core/clone', + '../Core/Color', + '../Core/ComponentDatatype', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/DeveloperError', + '../Core/Matrix4', + '../Core/PrimitiveType', + '../Core/RuntimeError', + '../Core/Transforms', + '../Renderer/Buffer', + '../Renderer/BufferUsage', + '../Renderer/DrawCommand', + '../Renderer/ShaderSource', + '../ThirdParty/when', + './getAttributeOrUniformBySemantic', + './Model', + './ModelInstance', + './SceneMode', + './ShadowMode' + ], function( + BoundingSphere, + Cartesian3, + clone, + Color, + ComponentDatatype, + defaultValue, + defined, + defineProperties, + destroyObject, + DeveloperError, + Matrix4, + PrimitiveType, + RuntimeError, + Transforms, + Buffer, + BufferUsage, + DrawCommand, + ShaderSource, + when, + getAttributeOrUniformBySemantic, + Model, + ModelInstance, + SceneMode, + ShadowMode) { + 'use strict'; + + var LoadState = { + NEEDS_LOAD : 0, + LOADING : 1, + LOADED : 2, + FAILED : 3 + }; + + /** + * A 3D model instance collection. All instances reference the same underlying model, but have unique + * per-instance properties like model matrix, pick id, etc. + * + * Instances are rendered relative-to-center and for best results instances should be positioned close to one another. + * Otherwise there may be precision issues if, for example, instances are placed on opposite sides of the globe. + * + * @alias ModelInstanceCollection + * @constructor + * + * @param {Object} options Object with the following properties: + * @param {Object[]} [options.instances] An array of instances, where each instance contains a modelMatrix and optional batchId when options.batchTable is defined. + * @param {Cesium3DTileBatchTable} [options.batchTable] The batch table of the instanced 3D Tile. + * @param {String} [options.url] The url to the .gltf file. + * @param {Object} [options.headers] HTTP headers to send with the request. + * @param {Object} [options.requestType] The request type, used for request prioritization + * @param {Object|ArrayBuffer|Uint8Array} [options.gltf] The object for the glTF JSON or an arraybuffer of Binary glTF defined by the CESIUM_binary_glTF extension. + * @param {String} [options.basePath=''] The base path that paths in the glTF JSON are relative to. + * @param {Boolean} [options.dynamic=false] Hint if instance model matrices will be updated frequently. + * @param {Boolean} [options.show=true] Determines if the collection will be shown. + * @param {Boolean} [options.allowPicking=true] When true, each instance is pickable with {@link Scene#pick}. + * @param {Boolean} [options.asynchronous=true] Determines if model WebGL resource creation will be spread out over several frames or block until completion once all glTF files are loaded. + * @param {Boolean} [options.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the model is loaded. + * @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the collection casts or receives shadows from each light source. + * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Draws the bounding sphere for the collection. + * @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the instances in wireframe. + * + * @exception {DeveloperError} Must specify either or , but not both. + * @exception {DeveloperError} Shader program cannot be optimized for instancing. Parameters cannot have any of the following semantics: MODEL, MODELINVERSE, MODELVIEWINVERSE, MODELVIEWPROJECTIONINVERSE, MODELINVERSETRANSPOSE. + * + * @private + */ + function ModelInstanceCollection(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + + //>>includeStart('debug', pragmas.debug); + if (!defined(options.gltf) && !defined(options.url)) { + throw new DeveloperError('Either options.gltf or options.url is required.'); + } + + if (defined(options.gltf) && defined(options.url)) { + throw new DeveloperError('Cannot pass in both options.gltf and options.url.'); + } + //>>includeEnd('debug'); + + this.show = defaultValue(options.show, true); + + this._instancingSupported = false; + this._dynamic = defaultValue(options.dynamic, false); + this._allowPicking = defaultValue(options.allowPicking, true); + this._cull = defaultValue(options.cull, true); // Undocumented option + this._ready = false; + this._readyPromise = when.defer(); + this._state = LoadState.NEEDS_LOAD; + this._dirty = false; + + this._instances = createInstances(this, options.instances); + + // When the model instance collection is backed by an i3dm tile, + // use its batch table resources to modify the shaders, attributes, and uniform maps. + this._batchTable = options.batchTable; + + this._model = undefined; + this._vertexBufferTypedArray = undefined; // Hold onto the vertex buffer contents when dynamic is true + this._vertexBuffer = undefined; + this._batchIdBuffer = undefined; + this._instancedUniformsByProgram = undefined; + + this._drawCommands = []; + this._pickCommands = []; + this._modelCommands = undefined; + + this._boundingSphere = createBoundingSphere(this); + this._center = Cartesian3.clone(this._boundingSphere.center); + this._rtcTransform = new Matrix4(); + this._rtcModelView = new Matrix4(); // Holds onto uniform + + this._mode = undefined; + + this.modelMatrix = Matrix4.clone(Matrix4.IDENTITY); + this._modelMatrix = Matrix4.clone(this.modelMatrix); + + // Passed on to Model + this._url = options.url; + this._headers = options.headers; + this._requestType = options.requestType; + this._gltf = options.gltf; + this._basePath = options.basePath; + this._asynchronous = options.asynchronous; + this._incrementallyLoadTextures = options.incrementallyLoadTextures; + this._upAxis = options.upAxis; // Undocumented option + + this.shadows = defaultValue(options.shadows, ShadowMode.ENABLED); + this._shadows = this.shadows; + + this.debugShowBoundingVolume = defaultValue(options.debugShowBoundingVolume, false); + this._debugShowBoundingVolume = false; + + this.debugWireframe = defaultValue(options.debugWireframe, false); + this._debugWireframe = false; + } + + defineProperties(ModelInstanceCollection.prototype, { + allowPicking : { + get : function() { + return this._allowPicking; + } + }, + length : { + get : function() { + return this._instances.length; + } + }, + activeAnimations : { + get : function() { + return this._model.activeAnimations; + } + }, + ready : { + get : function() { + return this._ready; + } + }, + readyPromise : { + get : function() { + return this._readyPromise.promise; + } + } + }); + + function createInstances(collection, instancesOptions) { + instancesOptions = defaultValue(instancesOptions, []); + var length = instancesOptions.length; + var instances = new Array(length); + for (var i = 0; i < length; ++i) { + var instanceOptions = instancesOptions[i]; + var modelMatrix = instanceOptions.modelMatrix; + var instanceId = defaultValue(instanceOptions.batchId, i); + instances[i] = new ModelInstance(collection, modelMatrix, instanceId); + } + return instances; + } + + function createBoundingSphere(collection) { + var instancesLength = collection.length; + var points = new Array(instancesLength); + for (var i = 0; i < instancesLength; ++i) { + points[i] = Matrix4.getTranslation(collection._instances[i]._modelMatrix, new Cartesian3()); + } + + return BoundingSphere.fromPoints(points); + } + + var scratchCartesian = new Cartesian3(); + var scratchMatrix = new Matrix4(); + + ModelInstanceCollection.prototype.expandBoundingSphere = function(instanceModelMatrix) { + var translation = Matrix4.getTranslation(instanceModelMatrix, scratchCartesian); + BoundingSphere.expand(this._boundingSphere, translation, this._boundingSphere); + }; + + function getInstancedUniforms(collection, programName) { + if (defined(collection._instancedUniformsByProgram)) { + return collection._instancedUniformsByProgram[programName]; + } + + var instancedUniformsByProgram = {}; + collection._instancedUniformsByProgram = instancedUniformsByProgram; + + // When using CESIUM_RTC_MODELVIEW the CESIUM_RTC center is ignored. Instances are always rendered relative-to-center. + var modelSemantics = ['MODEL', 'MODELVIEW', 'CESIUM_RTC_MODELVIEW', 'MODELVIEWPROJECTION', 'MODELINVERSE', 'MODELVIEWINVERSE', 'MODELVIEWPROJECTIONINVERSE', 'MODELINVERSETRANSPOSE', 'MODELVIEWINVERSETRANSPOSE']; + var supportedSemantics = ['MODELVIEW', 'CESIUM_RTC_MODELVIEW', 'MODELVIEWPROJECTION', 'MODELVIEWINVERSETRANSPOSE']; + + var gltf = collection._model.gltf; + var techniques = gltf.techniques; + for (var techniqueName in techniques) { + if (techniques.hasOwnProperty(techniqueName)) { + var technique = techniques[techniqueName]; + var parameters = technique.parameters; + var uniforms = technique.uniforms; + var program = technique.program; + + // Different techniques may share the same program, skip if already processed. + // This assumes techniques that share a program do not declare different semantics for the same uniforms. + if (!defined(instancedUniformsByProgram[program])) { + var uniformMap = {}; + instancedUniformsByProgram[program] = uniformMap; + for (var uniformName in uniforms) { + if (uniforms.hasOwnProperty(uniformName)) { + var parameterName = uniforms[uniformName]; + var parameter = parameters[parameterName]; + var semantic = parameter.semantic; + if (defined(semantic) && (modelSemantics.indexOf(semantic) > -1)) { + if (supportedSemantics.indexOf(semantic) > -1) { + uniformMap[uniformName] = semantic; + } else { + throw new RuntimeError('Shader program cannot be optimized for instancing. ' + + 'Parameter "' + parameter + '" in program "' + programName + + '" uses unsupported semantic "' + semantic + '"' + ); + } + } + } + } + } + } + } + + return instancedUniformsByProgram[programName]; + } + + var vertexShaderCached; + + function getVertexShaderCallback(collection) { + return function(vs, programName) { + var instancedUniforms = getInstancedUniforms(collection, programName); + var usesBatchTable = defined(collection._batchTable); + + var renamedSource = ShaderSource.replaceMain(vs, 'czm_instancing_main'); + + var globalVarsHeader = ''; + var globalVarsMain = ''; + for (var uniform in instancedUniforms) { + if (instancedUniforms.hasOwnProperty(uniform)) { + var semantic = instancedUniforms[uniform]; + var varName; + if (semantic === 'MODELVIEW' || semantic === 'CESIUM_RTC_MODELVIEW') { + varName = 'czm_instanced_modelView'; + } else if (semantic === 'MODELVIEWPROJECTION') { + varName = 'czm_instanced_modelViewProjection'; + globalVarsHeader += 'mat4 czm_instanced_modelViewProjection;\n'; + globalVarsMain += 'czm_instanced_modelViewProjection = czm_projection * czm_instanced_modelView;\n'; + } else if (semantic === 'MODELVIEWINVERSETRANSPOSE') { + varName = 'czm_instanced_modelViewInverseTranspose'; + globalVarsHeader += 'mat3 czm_instanced_modelViewInverseTranspose;\n'; + globalVarsMain += 'czm_instanced_modelViewInverseTranspose = mat3(czm_instanced_modelView);\n'; + } + + // Remove the uniform declaration + var regex = new RegExp('uniform.*' + uniform + '.*'); + renamedSource = renamedSource.replace(regex, ''); + + // Replace all occurrences of the uniform with the global variable + regex = new RegExp(uniform + '\\b', 'g'); + renamedSource = renamedSource.replace(regex, varName); + } + } + + // czm_instanced_model is the model matrix of the instance relative to center + // czm_instanced_modifiedModelView is the transform from the center to view + // czm_instanced_nodeTransform is the local offset of the node within the model + var uniforms = + 'uniform mat4 czm_instanced_modifiedModelView;\n' + + 'uniform mat4 czm_instanced_nodeTransform;\n'; + + var batchIdAttribute = usesBatchTable ? 'attribute float a_batchId;\n' : ''; + + var instancedSource = + uniforms + + globalVarsHeader + + 'mat4 czm_instanced_modelView;\n' + + 'attribute vec4 czm_modelMatrixRow0;\n' + + 'attribute vec4 czm_modelMatrixRow1;\n' + + 'attribute vec4 czm_modelMatrixRow2;\n' + + batchIdAttribute + + renamedSource + + 'void main()\n' + + '{\n' + + ' mat4 czm_instanced_model = mat4(czm_modelMatrixRow0.x, czm_modelMatrixRow1.x, czm_modelMatrixRow2.x, 0.0, czm_modelMatrixRow0.y, czm_modelMatrixRow1.y, czm_modelMatrixRow2.y, 0.0, czm_modelMatrixRow0.z, czm_modelMatrixRow1.z, czm_modelMatrixRow2.z, 0.0, czm_modelMatrixRow0.w, czm_modelMatrixRow1.w, czm_modelMatrixRow2.w, 1.0);\n' + + ' czm_instanced_modelView = czm_instanced_modifiedModelView * czm_instanced_model * czm_instanced_nodeTransform;\n' + + globalVarsMain + + ' czm_instancing_main();\n' + + '}'; + + vertexShaderCached = instancedSource; + + if (usesBatchTable) { + instancedSource = collection._batchTable.getVertexShaderCallback(true, 'a_batchId')(instancedSource); + } + + return instancedSource; + }; + } + + function getFragmentShaderCallback(collection) { + return function(fs) { + var batchTable = collection._batchTable; + if (defined(batchTable)) { + var gltf = collection._model.gltf; + var diffuseUniformName = getAttributeOrUniformBySemantic(gltf, '_3DTILESDIFFUSE'); + fs = batchTable.getFragmentShaderCallback(true, diffuseUniformName)(fs); + } + return fs; + }; + } + + function getPickVertexShaderCallback(collection) { + return function (vs) { + // Use the vertex shader that was generated earlier + vs = vertexShaderCached; + var usesBatchTable = defined(collection._batchTable); + var allowPicking = collection._allowPicking; + if (usesBatchTable) { + vs = collection._batchTable.getPickVertexShaderCallback('a_batchId')(vs); + } else if (allowPicking) { + vs = ShaderSource.createPickVertexShaderSource(vs); + } + return vs; + }; + } + + function getPickFragmentShaderCallback(collection) { + return function(fs) { + var usesBatchTable = defined(collection._batchTable); + var allowPicking = collection._allowPicking; + if (usesBatchTable) { + fs = collection._batchTable.getPickFragmentShaderCallback()(fs); + } else if (allowPicking) { + fs = ShaderSource.createPickFragmentShaderSource(fs, 'varying'); + } + return fs; + }; + } + + function createModifiedModelView(collection, context) { + return function() { + return Matrix4.multiply(context.uniformState.view, collection._rtcTransform, collection._rtcModelView); + }; + } + + function createNodeTransformFunction(node) { + return function() { + return node.computedMatrix; + }; + } + + function getUniformMapCallback(collection, context) { + return function(uniformMap, programName, node) { + uniformMap = clone(uniformMap); + uniformMap.czm_instanced_modifiedModelView = createModifiedModelView(collection, context); + uniformMap.czm_instanced_nodeTransform = createNodeTransformFunction(node); + + // Remove instanced uniforms from the uniform map + var instancedUniforms = getInstancedUniforms(collection, programName); + for (var uniform in instancedUniforms) { + if (instancedUniforms.hasOwnProperty(uniform)) { + delete uniformMap[uniform]; + } + } + + if (defined(collection._batchTable)) { + uniformMap = collection._batchTable.getUniformMapCallback()(uniformMap); + } + + return uniformMap; + }; + } + + function getPickUniformMapCallback(collection) { + return function(uniformMap) { + // Uses the uniform map generated from getUniformMapCallback + if (defined(collection._batchTable)) { + uniformMap = collection._batchTable.getPickUniformMapCallback()(uniformMap); + } + return uniformMap; + }; + } + + function getVertexShaderNonInstancedCallback(collection) { + return function(vs) { + if (defined(collection._batchTable)) { + vs = collection._batchTable.getVertexShaderCallback(true, 'a_batchId')(vs); + // Treat a_batchId as a uniform rather than a vertex attribute + vs = 'uniform float a_batchId\n;' + vs; + } + return vs; + }; + } + + function getPickVertexShaderNonInstancedCallback(collection) { + return function(vs) { + if (defined(collection._batchTable)) { + vs = collection._batchTable.getPickVertexShaderCallback('a_batchId')(vs); + // Treat a_batchId as a uniform rather than a vertex attribute + vs = 'uniform float a_batchId\n;' + vs; + } + return vs; + }; + } + + function getPickFragmentShaderNonInstancedCallback(collection) { + return function(fs) { + var usesBatchTable = defined(collection._batchTable); + var allowPicking = collection._allowPicking; + if (usesBatchTable) { + fs = collection._batchTable.getPickFragmentShaderCallback()(fs); + } else if (allowPicking) { + fs = ShaderSource.createPickFragmentShaderSource(fs, 'uniform'); + } + return fs; + }; + } + + function getUniformMapNonInstancedCallback(collection) { + return function(uniformMap) { + if (defined(collection._batchTable)) { + uniformMap = collection._batchTable.getUniformMapCallback()(uniformMap); + } + + return uniformMap; + }; + } + + function getVertexBufferTypedArray(collection) { + var instances = collection._instances; + var instancesLength = collection.length; + var collectionCenter = collection._center; + var vertexSizeInFloats = 12; + + var bufferData = collection._vertexBufferTypedArray; + if (!defined(bufferData)) { + bufferData = new Float32Array(instancesLength * vertexSizeInFloats); + } + if (collection._dynamic) { + // Hold onto the buffer data so we don't have to allocate new memory every frame. + collection._vertexBufferTypedArray = bufferData; + } + + for (var i = 0; i < instancesLength; ++i) { + var modelMatrix = instances[i]._modelMatrix; + + // Instance matrix is relative to center + var instanceMatrix = Matrix4.clone(modelMatrix, scratchMatrix); + instanceMatrix[12] -= collectionCenter.x; + instanceMatrix[13] -= collectionCenter.y; + instanceMatrix[14] -= collectionCenter.z; + + var offset = i * vertexSizeInFloats; + + // First three rows of the model matrix + bufferData[offset + 0] = instanceMatrix[0]; + bufferData[offset + 1] = instanceMatrix[4]; + bufferData[offset + 2] = instanceMatrix[8]; + bufferData[offset + 3] = instanceMatrix[12]; + bufferData[offset + 4] = instanceMatrix[1]; + bufferData[offset + 5] = instanceMatrix[5]; + bufferData[offset + 6] = instanceMatrix[9]; + bufferData[offset + 7] = instanceMatrix[13]; + bufferData[offset + 8] = instanceMatrix[2]; + bufferData[offset + 9] = instanceMatrix[6]; + bufferData[offset + 10] = instanceMatrix[10]; + bufferData[offset + 11] = instanceMatrix[14]; + } + + return bufferData; + } + + function createVertexBuffer(collection, context) { + var i; + var instances = collection._instances; + var instancesLength = collection.length; + var dynamic = collection._dynamic; + var usesBatchTable = defined(collection._batchTable); + var allowPicking = collection._allowPicking; + + if (usesBatchTable) { + var batchIdBufferData = new Uint16Array(instancesLength); + for (i = 0; i < instancesLength; ++i) { + batchIdBufferData[i] = instances[i]._instanceId; + } + collection._batchIdBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : batchIdBufferData, + usage : BufferUsage.STATIC_DRAW + }); + } + + if (allowPicking && !usesBatchTable) { + var pickIdBuffer = new Uint8Array(instancesLength * 4); + for (i = 0; i < instancesLength; ++i) { + var pickId = collection._pickIds[i]; + var pickColor = pickId.color; + var offset = i * 4; + pickIdBuffer[offset] = Color.floatToByte(pickColor.red); + pickIdBuffer[offset + 1] = Color.floatToByte(pickColor.green); + pickIdBuffer[offset + 2] = Color.floatToByte(pickColor.blue); + pickIdBuffer[offset + 3] = Color.floatToByte(pickColor.alpha); + } + collection._pickIdBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : pickIdBuffer, + usage : BufferUsage.STATIC_DRAW + }); + } + + var vertexBufferTypedArray = getVertexBufferTypedArray(collection); + collection._vertexBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : vertexBufferTypedArray, + usage : dynamic ? BufferUsage.STREAM_DRAW : BufferUsage.STATIC_DRAW + }); + } + + function updateVertexBuffer(collection) { + var vertexBufferTypedArray = getVertexBufferTypedArray(collection); + collection._vertexBuffer.copyFromArrayView(vertexBufferTypedArray); + } + + function createPickIds(collection, context) { + // PERFORMANCE_IDEA: we could skip the pick buffer completely by allocating + // a continuous range of pickIds and then converting the base pickId + batchId + // to RGBA in the shader. The only consider is precision issues, which might + // not be an issue in WebGL 2. + var instances = collection._instances; + var instancesLength = instances.length; + var pickIds = new Array(instancesLength); + for (var i = 0; i < instancesLength; ++i) { + pickIds[i] = context.createPickId(instances[i]); + } + return pickIds; + } + + function createModel(collection, context) { + var instancingSupported = collection._instancingSupported; + var usesBatchTable = defined(collection._batchTable); + var allowPicking = collection._allowPicking; + + var modelOptions = { + url : collection._url, + headers : collection._headers, + requestType : collection._requestType, + gltf : collection._gltf, + basePath : collection._basePath, + shadows : collection._shadows, + cacheKey : undefined, + asynchronous : collection._asynchronous, + allowPicking : allowPicking, + incrementallyLoadTextures : collection._incrementallyLoadTextures, + upAxis : collection._upAxis, + precreatedAttributes : undefined, + vertexShaderLoaded : undefined, + fragmentShaderLoaded : undefined, + uniformMapLoaded : undefined, + pickVertexShaderLoaded : undefined, + pickFragmentShaderLoaded : undefined, + pickUniformMapLoaded : undefined, + ignoreCommands : true + }; + + if (allowPicking && !usesBatchTable) { + collection._pickIds = createPickIds(collection, context); + } + + if (instancingSupported) { + createVertexBuffer(collection, context); + + var vertexSizeInFloats = 12; + var componentSizeInBytes = ComponentDatatype.getSizeInBytes(ComponentDatatype.FLOAT); + + var instancedAttributes = { + czm_modelMatrixRow0 : { + index : 0, // updated in Model + vertexBuffer : collection._vertexBuffer, + componentsPerAttribute : 4, + componentDatatype : ComponentDatatype.FLOAT, + normalize : false, + offsetInBytes : 0, + strideInBytes : componentSizeInBytes * vertexSizeInFloats, + instanceDivisor : 1 + }, + czm_modelMatrixRow1 : { + index : 0, // updated in Model + vertexBuffer : collection._vertexBuffer, + componentsPerAttribute : 4, + componentDatatype : ComponentDatatype.FLOAT, + normalize : false, + offsetInBytes : componentSizeInBytes * 4, + strideInBytes : componentSizeInBytes * vertexSizeInFloats, + instanceDivisor : 1 + }, + czm_modelMatrixRow2 : { + index : 0, // updated in Model + vertexBuffer : collection._vertexBuffer, + componentsPerAttribute : 4, + componentDatatype : ComponentDatatype.FLOAT, + normalize : false, + offsetInBytes : componentSizeInBytes * 8, + strideInBytes : componentSizeInBytes * vertexSizeInFloats, + instanceDivisor : 1 + } + }; + + // When using a batch table, add a batch id attribute + if (usesBatchTable) { + instancedAttributes.a_batchId = { + index : 0, // updated in Model + vertexBuffer : collection._batchIdBuffer, + componentsPerAttribute : 1, + componentDatatype : ComponentDatatype.UNSIGNED_SHORT, + normalize : false, + offsetInBytes : 0, + strideInBytes : 0, + instanceDivisor : 1 + }; + } + + if (allowPicking && !usesBatchTable) { + instancedAttributes.pickColor = { + index : 0, // updated in Model + vertexBuffer : collection._pickIdBuffer, + componentsPerAttribute : 4, + componentDatatype : ComponentDatatype.UNSIGNED_BYTE, + normalize : true, + offsetInBytes : 0, + strideInBytes : 0, + instanceDivisor : 1 + }; + } + + modelOptions.precreatedAttributes = instancedAttributes; + modelOptions.vertexShaderLoaded = getVertexShaderCallback(collection); + modelOptions.fragmentShaderLoaded = getFragmentShaderCallback(collection); + modelOptions.uniformMapLoaded = getUniformMapCallback(collection, context); + modelOptions.pickVertexShaderLoaded = getPickVertexShaderCallback(collection); + modelOptions.pickFragmentShaderLoaded = getPickFragmentShaderCallback(collection); + modelOptions.pickUniformMapLoaded = getPickUniformMapCallback(collection); + + if (defined(collection._url)) { + modelOptions.cacheKey = collection._url + '#instanced'; + } + } else { + modelOptions.vertexShaderLoaded = getVertexShaderNonInstancedCallback(collection); + modelOptions.fragmentShaderLoaded = getFragmentShaderCallback(collection); + modelOptions.uniformMapLoaded = getUniformMapNonInstancedCallback(collection, context); + modelOptions.pickVertexShaderLoaded = getPickVertexShaderNonInstancedCallback(collection); + modelOptions.pickFragmentShaderLoaded = getPickFragmentShaderNonInstancedCallback(collection); + modelOptions.pickUniformMapLoaded = getPickUniformMapCallback(collection); + } + + if (defined(collection._url)) { + collection._model = Model.fromGltf(modelOptions); + } else { + collection._model = new Model(modelOptions); + } + } + + function updateWireframe(collection) { + if (collection._debugWireframe !== collection.debugWireframe) { + collection._debugWireframe = collection.debugWireframe; + + // This assumes the original primitive was TRIANGLES and that the triangles + // are connected for the wireframe to look perfect. + var primitiveType = collection.debugWireframe ? PrimitiveType.LINES : PrimitiveType.TRIANGLES; + var commands = collection._drawCommands; + var length = commands.length; + for (var i = 0; i < length; ++i) { + commands[i].primitiveType = primitiveType; + } + } + } + function updateShowBoundingVolume(collection) { + if (collection.debugShowBoundingVolume !== collection._debugShowBoundingVolume) { + collection._debugShowBoundingVolume = collection.debugShowBoundingVolume; + + var commands = collection._drawCommands; + var length = commands.length; + for (var i = 0; i < length; ++i) { + commands[i].debugShowBoundingVolume = collection.debugShowBoundingVolume; + } + } + } + + function createCommands(collection, drawCommands, pickCommands) { + var commandsLength = drawCommands.length; + var instancesLength = collection.length; + var allowPicking = collection.allowPicking; + var boundingSphere = collection._boundingSphere; + var cull = collection._cull; + + for (var i = 0; i < commandsLength; ++i) { + var drawCommand = DrawCommand.shallowClone(drawCommands[i]); + drawCommand.instanceCount = instancesLength; + drawCommand.boundingVolume = boundingSphere; + drawCommand.cull = cull; + collection._drawCommands.push(drawCommand); + + if (allowPicking) { + var pickCommand = DrawCommand.shallowClone(pickCommands[i]); + pickCommand.instanceCount = instancesLength; + pickCommand.boundingVolume = boundingSphere; + pickCommand.cull = cull; + collection._pickCommands.push(pickCommand); + } + } + } + + function createBatchIdFunction(batchId) { + return function() { + return batchId; + }; + } + + function createPickColorFunction(color) { + return function() { + return color; + }; + } + + function createCommandsNonInstanced(collection, drawCommands, pickCommands) { + // When instancing is disabled, create commands for every instance. + var instances = collection._instances; + var commandsLength = drawCommands.length; + var instancesLength = collection.length; + var allowPicking = collection.allowPicking; + var usesBatchTable = defined(collection._batchTable); + var cull = collection._cull; + + for (var i = 0; i < commandsLength; ++i) { + for (var j = 0; j < instancesLength; ++j) { + var drawCommand = DrawCommand.shallowClone(drawCommands[i]); + drawCommand.modelMatrix = new Matrix4(); // Updated in updateCommandsNonInstanced + drawCommand.boundingVolume = new BoundingSphere(); // Updated in updateCommandsNonInstanced + drawCommand.cull = cull; + drawCommand.uniformMap = clone(drawCommand.uniformMap); + if (usesBatchTable) { + drawCommand.uniformMap.a_batchId = createBatchIdFunction(instances[j]._instanceId); + } + collection._drawCommands.push(drawCommand); + + if (allowPicking) { + var pickCommand = DrawCommand.shallowClone(pickCommands[i]); + pickCommand.modelMatrix = new Matrix4(); // Updated in updateCommandsNonInstanced + pickCommand.boundingVolume = new BoundingSphere(); // Updated in updateCommandsNonInstanced + pickCommand.cull = cull; + pickCommand.uniformMap = clone(pickCommand.uniformMap); + if (usesBatchTable) { + pickCommand.uniformMap.a_batchId = createBatchIdFunction(instances[j]._instanceId); + } else if (allowPicking) { + var pickId = collection._pickIds[j]; + pickCommand.uniformMap.czm_pickColor = createPickColorFunction(pickId.color); + } + collection._pickCommands.push(pickCommand); + } + } + } + } + + function updateCommandsNonInstanced(collection) { + var modelCommands = collection._modelCommands; + var commandsLength = modelCommands.length; + var instancesLength = collection.length; + var allowPicking = collection.allowPicking; + var collectionTransform = collection._rtcTransform; + var collectionCenter = collection._center; + + for (var i = 0; i < commandsLength; ++i) { + var modelCommand = modelCommands[i]; + for (var j = 0; j < instancesLength; ++j) { + var commandIndex = i * instancesLength + j; + var drawCommand = collection._drawCommands[commandIndex]; + var instanceMatrix = Matrix4.clone(collection._instances[j]._modelMatrix, scratchMatrix); + instanceMatrix[12] -= collectionCenter.x; + instanceMatrix[13] -= collectionCenter.y; + instanceMatrix[14] -= collectionCenter.z; + instanceMatrix = Matrix4.multiply(collectionTransform, instanceMatrix, scratchMatrix); + var nodeMatrix = modelCommand.modelMatrix; + var modelMatrix = drawCommand.modelMatrix; + Matrix4.multiply(instanceMatrix, nodeMatrix, modelMatrix); + + var nodeBoundingSphere = modelCommand.boundingVolume; + var boundingSphere = drawCommand.boundingVolume; + BoundingSphere.transform(nodeBoundingSphere, instanceMatrix, boundingSphere); + + if (allowPicking) { + var pickCommand = collection._pickCommands[commandIndex]; + Matrix4.clone(modelMatrix, pickCommand.modelMatrix); + BoundingSphere.clone(boundingSphere, pickCommand.boundingVolume); + } + } + } + } + + function getModelCommands(model) { + var nodeCommands = model._nodeCommands; + var length = nodeCommands.length; + + var drawCommands = []; + var pickCommands = []; + + for (var i = 0; i < length; ++i) { + var nc = nodeCommands[i]; + if (nc.show) { + drawCommands.push(nc.command); + pickCommands.push(nc.pickCommand); + } + } + + return { + draw: drawCommands, + pick: pickCommands + }; + } + + function updateShadows(collection) { + if (collection.shadows !== collection._shadows) { + collection._shadows = collection.shadows; + + var castShadows = ShadowMode.castShadows(collection.shadows); + var receiveShadows = ShadowMode.receiveShadows(collection.shadows); + + var drawCommands = collection._drawCommands; + var length = drawCommands.length; + for (var i = 0; i < length; ++i) { + var drawCommand = drawCommands[i]; + drawCommand.castShadows = castShadows; + drawCommand.receiveShadows = receiveShadows; + } + } + } + + ModelInstanceCollection.prototype.update = function(frameState) { + if (frameState.mode === SceneMode.MORPHING) { + return; + } + + if (!this.show) { + return; + } + + if (this.length === 0) { + return; + } + + var context = frameState.context; + + if (this._state === LoadState.NEEDS_LOAD) { + this._state = LoadState.LOADING; + this._instancingSupported = context.instancedArrays; + createModel(this, context); + var that = this; + this._model.readyPromise.otherwise(function(error) { + that._state = LoadState.FAILED; + that._readyPromise.reject(error); + }); + } + + var instancingSupported = this._instancingSupported; + var model = this._model; + model.update(frameState); + + if (model.ready && (this._state === LoadState.LOADING)) { + this._state = LoadState.LOADED; + this._ready = true; + + // Expand bounding volume to fit the radius of the loaded model including the model's offset from the center + var modelRadius = model.boundingSphere.radius + Cartesian3.magnitude(model.boundingSphere.center); + this._boundingSphere.radius += modelRadius; + + var modelCommands = getModelCommands(model); + this._modelCommands = modelCommands.draw; + + if (instancingSupported) { + createCommands(this, modelCommands.draw, modelCommands.pick); + } else { + createCommandsNonInstanced(this, modelCommands.draw, modelCommands.pick); + updateCommandsNonInstanced(this); + } + + this._readyPromise.resolve(this); + return; + } + + if (this._state !== LoadState.LOADED) { + return; + } + + var modeChanged = (frameState.mode !== this._mode); + var modelMatrix = this.modelMatrix; + var modelMatrixChanged = !Matrix4.equals(this._modelMatrix, modelMatrix); + + if (modeChanged || modelMatrixChanged) { + this._mode = frameState.mode; + Matrix4.clone(modelMatrix, this._modelMatrix); + var rtcTransform = Matrix4.multiplyByTranslation(this._modelMatrix, this._center, this._rtcTransform); + if (this._mode !== SceneMode.SCENE3D) { + rtcTransform = Transforms.basisTo2D(frameState.mapProjection, rtcTransform, rtcTransform); + } + Matrix4.getTranslation(rtcTransform, this._boundingSphere.center); + } + + if (instancingSupported && this._dirty) { + // If at least one instance has moved assume the collection is now dynamic + this._dynamic = true; + this._dirty = false; + + // PERFORMANCE_IDEA: only update dirty sub-sections instead of the whole collection + updateVertexBuffer(this); + } + + // If any node changes due to an animation, update the commands. This could be inefficient if the model is + // composed of many nodes and only one changes, however it is probably fine in the general use case. + // Only applies when instancing is disabled. The instanced shader automatically handles node transformations. + if (!instancingSupported && (model.dirty || this._dirty || modeChanged || modelMatrixChanged)) { + updateCommandsNonInstanced(this); + } + + updateShadows(this); + updateWireframe(this); + updateShowBoundingVolume(this); + + var passes = frameState.passes; + var commandList = frameState.commandList; + var commands = passes.render ? this._drawCommands : this._pickCommands; + var commandsLength = commands.length; + + for (var i = 0; i < commandsLength; ++i) { + commandList.push(commands[i]); + } + }; + + ModelInstanceCollection.prototype.isDestroyed = function() { + return false; + }; + + ModelInstanceCollection.prototype.destroy = function() { + this._model = this._model && this._model.destroy(); + + var pickIds = this._pickIds; + if (defined(pickIds)) { + var length = pickIds.length; + for (var i = 0; i < length; ++i) { + pickIds[i].destroy(); + } + } + + return destroyObject(this); + }; + + return ModelInstanceCollection; +}); diff --git a/Source/Scene/PointCloud3DTileContent.js b/Source/Scene/PointCloud3DTileContent.js new file mode 100644 index 000000000000..4e9675c9e966 --- /dev/null +++ b/Source/Scene/PointCloud3DTileContent.js @@ -0,0 +1,1264 @@ +/*global define*/ +define([ + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Cartesian4', + '../Core/Color', + '../Core/combine', + '../Core/ComponentDatatype', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/DeveloperError', + '../Core/FeatureDetection', + '../Core/getStringFromTypedArray', + '../Core/Matrix4', + '../Core/oneTimeWarning', + '../Core/PrimitiveType', + '../Core/RuntimeError', + '../Core/Transforms', + '../Renderer/Buffer', + '../Renderer/BufferUsage', + '../Renderer/DrawCommand', + '../Renderer/Pass', + '../Renderer/RenderState', + '../Renderer/ShaderProgram', + '../Renderer/ShaderSource', + '../Renderer/VertexArray', + '../ThirdParty/when', + './BlendingState', + './Cesium3DTileBatchTable', + './Cesium3DTileFeature', + './Cesium3DTileFeatureTable', + './SceneMode', + './ShadowMode' + ], function( + Cartesian2, + Cartesian3, + Cartesian4, + Color, + combine, + ComponentDatatype, + defaultValue, + defined, + defineProperties, + destroyObject, + DeveloperError, + FeatureDetection, + getStringFromTypedArray, + Matrix4, + oneTimeWarning, + PrimitiveType, + RuntimeError, + Transforms, + Buffer, + BufferUsage, + DrawCommand, + Pass, + RenderState, + ShaderProgram, + ShaderSource, + VertexArray, + when, + BlendingState, + Cesium3DTileBatchTable, + Cesium3DTileFeature, + Cesium3DTileFeatureTable, + SceneMode, + ShadowMode) { + 'use strict'; + + // Bail out if the browser doesn't support typed arrays, to prevent the setup function + // from failing, since we won't be able to create a WebGL context anyway. + if (!FeatureDetection.supportsTypedArrays()) { + return {}; + } + + /** + * Represents the contents of a + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/PointCloud/README.md|Points} + * tile in a {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles} tileset. + *

+ * Implements the {@link Cesium3DTileContent} interface. + *

+ * + * @alias PointCloud3DTileContent + * @constructor + * + * @private + */ + function PointCloud3DTileContent(tileset, tile, url, arrayBuffer, byteOffset) { + this._tileset = tileset; + this._tile = tile; + this._url = url; + + // Hold onto the payload until the render resources are created + this._parsedContent = undefined; + + this._drawCommand = undefined; + this._pickCommand = undefined; + this._pickId = undefined; // Only defined when batchTable is undefined + this._isTranslucent = false; + this._styleTranslucent = false; + this._constantColor = Color.clone(Color.WHITE); + this._rtcCenter = undefined; + this._batchTable = undefined; // Used when feature table contains BATCH_ID semantic + + // These values are used to regenerate the shader when the style changes + this._styleableShaderAttributes = undefined; + this._isQuantized = false; + this._isOctEncoded16P = false; + this._isRGB565 = false; + this._hasColors = false; + this._hasNormals = false; + this._hasBatchIds = false; + + // Use per-point normals to hide back-facing points. + this.backFaceCulling = false; + this._backFaceCulling = false; + + this._opaqueRenderState = undefined; + this._translucentRenderState = undefined; + + this._highlightColor = Color.clone(Color.WHITE); + this._pointSize = 1.0; + this._quantizedVolumeScale = undefined; + this._quantizedVolumeOffset = undefined; + + this._modelMatrix = Matrix4.clone(Matrix4.IDENTITY); + this._mode = undefined; + + this._readyPromise = when.defer(); + this._pointsLength = 0; + this._geometryByteLength = 0; + + this._features = undefined; + + /** + * @inheritdoc Cesium3DTileContent#featurePropertiesDirty + */ + this.featurePropertiesDirty = false; + + initialize(this, arrayBuffer, byteOffset); + } + + defineProperties(PointCloud3DTileContent.prototype, { + /** + * @inheritdoc Cesium3DTileContent#featuresLength + */ + featuresLength : { + get : function() { + if (defined(this._batchTable)) { + return this._batchTable.featuresLength; + } + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#pointsLength + */ + pointsLength : { + get : function() { + return this._pointsLength; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#trianglesLength + */ + trianglesLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#geometryByteLength + */ + geometryByteLength : { + get : function() { + return this._geometryByteLength; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#texturesByteLength + */ + texturesByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTableByteLength + */ + batchTableByteLength : { + get : function() { + if (defined(this._batchTable)) { + return this._batchTable.memorySizeInBytes; + } + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#innerContents + */ + innerContents : { + get : function() { + return undefined; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#readyPromise + */ + readyPromise : { + get : function() { + return this._readyPromise.promise; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tileset + */ + tileset : { + get : function() { + return this._tileset; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tile + */ + tile : { + get : function() { + return this._tile; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#url + */ + url : { + get : function() { + return this._url; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTable + */ + batchTable : { + get : function() { + return this._batchTable; + } + } + }); + + var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; + + function initialize(content, arrayBuffer, byteOffset) { + byteOffset = defaultValue(byteOffset, 0); + + var uint8Array = new Uint8Array(arrayBuffer); + var view = new DataView(arrayBuffer); + byteOffset += sizeOfUint32; // Skip magic + + var version = view.getUint32(byteOffset, true); + if (version !== 1) { + throw new RuntimeError('Only Point Cloud tile version 1 is supported. Version ' + version + ' is not.'); + } + byteOffset += sizeOfUint32; + + // Skip byteLength + byteOffset += sizeOfUint32; + + var featureTableJsonByteLength = view.getUint32(byteOffset, true); + if (featureTableJsonByteLength === 0) { + throw new RuntimeError('Feature table must have a byte length greater than zero'); + } + byteOffset += sizeOfUint32; + + var featureTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var batchTableJsonByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + var batchTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var featureTableString = getStringFromTypedArray(uint8Array, byteOffset, featureTableJsonByteLength); + var featureTableJson = JSON.parse(featureTableString); + byteOffset += featureTableJsonByteLength; + + var featureTableBinary = new Uint8Array(arrayBuffer, byteOffset, featureTableBinaryByteLength); + byteOffset += featureTableBinaryByteLength; + + // Get the batch table JSON and binary + var batchTableJson; + var batchTableBinary; + if (batchTableJsonByteLength > 0) { + // Has a batch table JSON + var batchTableString = getStringFromTypedArray(uint8Array, byteOffset, batchTableJsonByteLength); + batchTableJson = JSON.parse(batchTableString); + byteOffset += batchTableJsonByteLength; + + if (batchTableBinaryByteLength > 0) { + // Has a batch table binary + batchTableBinary = new Uint8Array(arrayBuffer, byteOffset, batchTableBinaryByteLength); + byteOffset += batchTableBinaryByteLength; + } + } + + var featureTable = new Cesium3DTileFeatureTable(featureTableJson, featureTableBinary); + + var pointsLength = featureTable.getGlobalProperty('POINTS_LENGTH'); + featureTable.featuresLength = pointsLength; + + if (!defined(pointsLength)) { + throw new RuntimeError('Feature table global property: POINTS_LENGTH must be defined'); + } + + // Get the positions + var positions; + var isQuantized = false; + + if (defined(featureTableJson.POSITION)) { + positions = featureTable.getPropertyArray('POSITION', ComponentDatatype.FLOAT, 3); + var rtcCenter = featureTable.getGlobalProperty('RTC_CENTER', ComponentDatatype.FLOAT, 3); + if (defined(rtcCenter)) { + content._rtcCenter = Cartesian3.unpack(rtcCenter); + } + } else if (defined(featureTableJson.POSITION_QUANTIZED)) { + positions = featureTable.getPropertyArray('POSITION_QUANTIZED', ComponentDatatype.UNSIGNED_SHORT, 3); + isQuantized = true; + + var quantizedVolumeScale = featureTable.getGlobalProperty('QUANTIZED_VOLUME_SCALE', ComponentDatatype.FLOAT, 3); + if (!defined(quantizedVolumeScale)) { + throw new RuntimeError('Global property: QUANTIZED_VOLUME_SCALE must be defined for quantized positions.'); + } + content._quantizedVolumeScale = Cartesian3.unpack(quantizedVolumeScale); + + var quantizedVolumeOffset = featureTable.getGlobalProperty('QUANTIZED_VOLUME_OFFSET', ComponentDatatype.FLOAT, 3); + if (!defined(quantizedVolumeOffset)) { + throw new RuntimeError('Global property: QUANTIZED_VOLUME_OFFSET must be defined for quantized positions.'); + } + content._quantizedVolumeOffset = Cartesian3.unpack(quantizedVolumeOffset); + } + + if (!defined(positions)) { + throw new RuntimeError('Either POSITION or POSITION_QUANTIZED must be defined.'); + } + + // Get the colors + var colors; + var isTranslucent = false; + var isRGB565 = false; + + if (defined(featureTableJson.RGBA)) { + colors = featureTable.getPropertyArray('RGBA', ComponentDatatype.UNSIGNED_BYTE, 4); + isTranslucent = true; + } else if (defined(featureTableJson.RGB)) { + colors = featureTable.getPropertyArray('RGB', ComponentDatatype.UNSIGNED_BYTE, 3); + } else if (defined(featureTableJson.RGB565)) { + colors = featureTable.getPropertyArray('RGB565', ComponentDatatype.UNSIGNED_SHORT, 1); + isRGB565 = true; + } else if (defined(featureTableJson.CONSTANT_RGBA)) { + var constantRGBA = featureTable.getGlobalProperty('CONSTANT_RGBA', ComponentDatatype.UNSIGNED_BYTE, 4); + content._constantColor = Color.fromBytes(constantRGBA[0], constantRGBA[1], constantRGBA[2], constantRGBA[3], content._constantColor); + } else { + // Use a default constant color + content._constantColor = Color.clone(Color.DARKGRAY, content._constantColor); + } + + content._isTranslucent = isTranslucent; + + // Get the normals + var normals; + var isOctEncoded16P = false; + + if (defined(featureTableJson.NORMAL)) { + normals = featureTable.getPropertyArray('NORMAL', ComponentDatatype.FLOAT, 3); + } else if (defined(featureTableJson.NORMAL_OCT16P)) { + normals = featureTable.getPropertyArray('NORMAL_OCT16P', ComponentDatatype.UNSIGNED_BYTE, 2); + isOctEncoded16P = true; + } + + // Get the batchIds and batch table. BATCH_ID does not need to be defined when the point cloud has per-point properties. + var batchIds; + if (defined(featureTableJson.BATCH_ID)) { + batchIds = featureTable.getPropertyArray('BATCH_ID', ComponentDatatype.UNSIGNED_SHORT, 1); + + var batchLength = featureTable.getGlobalProperty('BATCH_LENGTH'); + if (!defined(batchLength)) { + throw new RuntimeError('Global property: BATCH_LENGTH must be defined when BATCH_ID is defined.'); + } + + if (defined(batchTableBinary)) { + // Copy the batchTableBinary section and let the underlying ArrayBuffer be freed + batchTableBinary = new Uint8Array(batchTableBinary); + } + content._batchTable = new Cesium3DTileBatchTable(content, batchLength, batchTableJson, batchTableBinary); + } + + // If points are not batched and there are per-point properties, use these properties for styling purposes + var styleableProperties; + if (!defined(batchIds) && defined(batchTableBinary)) { + styleableProperties = Cesium3DTileBatchTable.getBinaryProperties(pointsLength, batchTableJson, batchTableBinary); + + // WebGL does not support UNSIGNED_INT, INT, or DOUBLE vertex attributes. Convert these to FLOAT. + for (var name in styleableProperties) { + if (styleableProperties.hasOwnProperty(name)) { + var property = styleableProperties[name]; + var typedArray = property.typedArray; + var componentDatatype = ComponentDatatype.fromTypedArray(typedArray); + if (componentDatatype === ComponentDatatype.INT || componentDatatype === ComponentDatatype.UNSIGNED_INT || componentDatatype === ComponentDatatype.DOUBLE) { + oneTimeWarning('Cast pnts property to floats', 'Point cloud property "' + name + '" will be casted to a float array because INT, UNSIGNED_INT, and DOUBLE are not valid WebGL vertex attribute types. Some precision may be lost.'); + property.typedArray = new Float32Array(typedArray); + } + } + } + } + + content._parsedContent = { + positions : positions, + colors : colors, + normals : normals, + batchIds : batchIds, + styleableProperties : styleableProperties + }; + content._pointsLength = pointsLength; + content._isQuantized = isQuantized; + content._isOctEncoded16P = isOctEncoded16P; + content._isRGB565 = isRGB565; + content._hasColors = defined(colors); + content._hasNormals = defined(normals); + content._hasBatchIds = defined(batchIds); + } + + var scratchPointSizeAndTilesetTime = new Cartesian2(); + + var positionLocation = 0; + var colorLocation = 1; + var normalLocation = 2; + var batchIdLocation = 3; + var numberOfAttributes = 4; + + function createResources(content, frameState) { + var context = frameState.context; + var parsedContent = content._parsedContent; + var pointsLength = content._pointsLength; + var positions = parsedContent.positions; + var colors = parsedContent.colors; + var normals = parsedContent.normals; + var batchIds = parsedContent.batchIds; + var styleableProperties = parsedContent.styleableProperties; + var hasStyleableProperties = defined(styleableProperties); + var isQuantized = content._isQuantized; + var isOctEncoded16P = content._isOctEncoded16P; + var isRGB565 = content._isRGB565; + var isTranslucent = content._isTranslucent; + var hasColors = content._hasColors; + var hasNormals = content._hasNormals; + var hasBatchIds = content._hasBatchIds; + + var batchTable = content._batchTable; + var hasBatchTable = defined(batchTable); + + var styleableVertexAttributes = []; + var styleableShaderAttributes = {}; + content._styleableShaderAttributes = styleableShaderAttributes; + + if (hasStyleableProperties) { + var attributeLocation = numberOfAttributes; + + for (var name in styleableProperties) { + if (styleableProperties.hasOwnProperty(name)) { + var property = styleableProperties[name]; + var typedArray = property.typedArray; + var componentCount = property.componentCount; + var componentDatatype = ComponentDatatype.fromTypedArray(typedArray); + + var vertexBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : property.typedArray, + usage : BufferUsage.STATIC_DRAW + }); + + content._geometryByteLength += vertexBuffer.sizeInBytes; + + var vertexAttribute = { + index : attributeLocation, + vertexBuffer : vertexBuffer, + componentsPerAttribute : componentCount, + componentDatatype : componentDatatype, + normalize : false, + offsetInBytes : 0, + strideInBytes : 0 + }; + + styleableVertexAttributes.push(vertexAttribute); + styleableShaderAttributes[name] = { + location : attributeLocation, + componentCount : componentCount + }; + ++attributeLocation; + } + } + } + + var uniformMap = { + u_pointSizeAndTilesetTime : function() { + scratchPointSizeAndTilesetTime.x = content._pointSize; + scratchPointSizeAndTilesetTime.y = content._tileset.timeSinceLoad; + return scratchPointSizeAndTilesetTime; + }, + u_highlightColor : function() { + return content._highlightColor; + }, + u_constantColor : function() { + return content._constantColor; + } + }; + + if (isQuantized) { + uniformMap = combine(uniformMap, { + u_quantizedVolumeScale : function() { + return content._quantizedVolumeScale; + } + }); + } + + var positionsVertexBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : positions, + usage : BufferUsage.STATIC_DRAW + }); + content._geometryByteLength += positionsVertexBuffer.sizeInBytes; + + var colorsVertexBuffer; + if (hasColors) { + colorsVertexBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : colors, + usage : BufferUsage.STATIC_DRAW + }); + content._geometryByteLength += colorsVertexBuffer.sizeInBytes; + } + + var normalsVertexBuffer; + if (hasNormals) { + normalsVertexBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : normals, + usage : BufferUsage.STATIC_DRAW + }); + content._geometryByteLength += normalsVertexBuffer.sizeInBytes; + } + + var batchIdsVertexBuffer; + if (hasBatchIds) { + batchIdsVertexBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : batchIds, + usage : BufferUsage.STATIC_DRAW + }); + content._geometryByteLength += batchIdsVertexBuffer.sizeInBytes; + } + + var attributes = []; + if (isQuantized) { + attributes.push({ + index : positionLocation, + vertexBuffer : positionsVertexBuffer, + componentsPerAttribute : 3, + componentDatatype : ComponentDatatype.UNSIGNED_SHORT, + normalize : true, // Convert position to 0 to 1 before entering the shader + offsetInBytes : 0, + strideInBytes : 0 + }); + } else { + attributes.push({ + index : positionLocation, + vertexBuffer : positionsVertexBuffer, + componentsPerAttribute : 3, + componentDatatype : ComponentDatatype.FLOAT, + normalize : false, + offsetInBytes : 0, + strideInBytes : 0 + }); + } + + if (hasColors) { + if (isRGB565) { + attributes.push({ + index : colorLocation, + vertexBuffer : colorsVertexBuffer, + componentsPerAttribute : 1, + componentDatatype : ComponentDatatype.UNSIGNED_SHORT, + normalize : false, + offsetInBytes : 0, + strideInBytes : 0 + }); + } else { + var colorComponentsPerAttribute = isTranslucent ? 4 : 3; + attributes.push({ + index : colorLocation, + vertexBuffer : colorsVertexBuffer, + componentsPerAttribute : colorComponentsPerAttribute, + componentDatatype : ComponentDatatype.UNSIGNED_BYTE, + normalize : true, + offsetInBytes : 0, + strideInBytes : 0 + }); + } + } + + if (hasNormals) { + if (isOctEncoded16P) { + attributes.push({ + index : normalLocation, + vertexBuffer : normalsVertexBuffer, + componentsPerAttribute : 2, + componentDatatype : ComponentDatatype.UNSIGNED_BYTE, + normalize : false, + offsetInBytes : 0, + strideInBytes : 0 + }); + } else { + attributes.push({ + index : normalLocation, + vertexBuffer : normalsVertexBuffer, + componentsPerAttribute : 3, + componentDatatype : ComponentDatatype.FLOAT, + normalize : false, + offsetInBytes : 0, + strideInBytes : 0 + }); + } + } + + if (hasBatchIds) { + attributes.push({ + index : batchIdLocation, + vertexBuffer : batchIdsVertexBuffer, + componentsPerAttribute : 1, + componentDatatype : ComponentDatatype.fromTypedArray(batchIds), + normalize : false, + offsetInBytes : 0, + strideInBytes : 0 + }); + } + + if (hasStyleableProperties) { + attributes = attributes.concat(styleableVertexAttributes); + } + + var vertexArray = new VertexArray({ + context : context, + attributes : attributes + }); + + var drawUniformMap = uniformMap; + + if (hasBatchTable) { + drawUniformMap = batchTable.getUniformMapCallback()(uniformMap); + } + + var pickUniformMap; + + if (hasBatchTable) { + pickUniformMap = batchTable.getPickUniformMapCallback()(uniformMap); + } else { + content._pickId = context.createPickId({ + primitive : content._tileset, + content : content + }); + + pickUniformMap = combine(uniformMap, { + czm_pickColor : function() { + return content._pickId.color; + } + }); + } + + content._opaqueRenderState = RenderState.fromCache({ + depthTest : { + enabled : true + } + }); + + content._translucentRenderState = RenderState.fromCache({ + depthTest : { + enabled : true + }, + depthMask : false, + blending : BlendingState.ALPHA_BLEND + }); + + content._drawCommand = new DrawCommand({ + boundingVolume : undefined, // Updated in update + cull : false, // Already culled by 3D Tiles + modelMatrix : new Matrix4(), + primitiveType : PrimitiveType.POINTS, + vertexArray : vertexArray, + count : pointsLength, + shaderProgram : undefined, // Updated in createShaders + uniformMap : drawUniformMap, + renderState : isTranslucent ? content._translucentRenderState : content._opaqueRenderState, + pass : isTranslucent ? Pass.TRANSLUCENT : Pass.CESIUM_3D_TILE, + owner : content, + castShadows : false, + receiveShadows : false + }); + + content._pickCommand = new DrawCommand({ + boundingVolume : undefined, // Updated in update + cull : false, // Already culled by 3D Tiles + modelMatrix : new Matrix4(), + primitiveType : PrimitiveType.POINTS, + vertexArray : vertexArray, + count : pointsLength, + shaderProgram : undefined, // Updated in createShaders + uniformMap : pickUniformMap, + renderState : isTranslucent ? content._translucentRenderState : content._opaqueRenderState, + pass : isTranslucent ? Pass.TRANSLUCENT : Pass.CESIUM_3D_TILE, + owner : content + }); + } + + var defaultProperties = ['POSITION', 'COLOR', 'NORMAL', 'POSITION_ABSOLUTE']; + + function getStyleableProperties(source, properties) { + // Get all the properties used by this style + var regex = /czm_tiles3d_style_(\w+)/g; + var matches = regex.exec(source); + while (matches !== null) { + var name = matches[1]; + if (properties.indexOf(name) === -1) { + properties.push(name); + } + matches = regex.exec(source); + } + } + + function getVertexAttribute(vertexArray, index) { + var numberOfAttributes = vertexArray.numberOfAttributes; + for (var i = 0; i < numberOfAttributes; ++i) { + var attribute = vertexArray.getAttribute(i); + if (attribute.index === index) { + return attribute; + } + } + } + + function modifyStyleFunction(source) { + // Replace occurrences of czm_tiles3d_style_DEFAULTPROPERTY + var length = defaultProperties.length; + for (var i = 0; i < length; ++i) { + var property = defaultProperties[i]; + var styleName = 'czm_tiles3d_style_' + property; + var replaceName = property.toLowerCase(); + source = source.replace(new RegExp(styleName + '(\\W)', 'g'), replaceName + '$1'); + } + + // Edit the function header to accept the point position, color, and normal + return source.replace('()', '(vec3 position, vec3 position_absolute, vec4 color, vec3 normal)'); + } + + function createShaders(content, frameState, style) { + var i; + var name; + var attribute; + + var context = frameState.context; + var batchTable = content._batchTable; + var hasBatchTable = defined(batchTable); + var hasStyle = defined(style); + var isQuantized = content._isQuantized; + var isOctEncoded16P = content._isOctEncoded16P; + var isRGB565 = content._isRGB565; + var isTranslucent = content._isTranslucent; + var hasColors = content._hasColors; + var hasNormals = content._hasNormals; + var hasBatchIds = content._hasBatchIds; + var backFaceCulling = content._backFaceCulling; + var vertexArray = content._drawCommand.vertexArray; + + var colorStyleFunction; + var showStyleFunction; + var pointSizeStyleFunction; + var styleTranslucent = isTranslucent; + + if (hasBatchTable) { + // Styling is handled in the batch table + hasStyle = false; + } + + if (hasStyle) { + var shaderState = { + translucent : false + }; + colorStyleFunction = style.getColorShaderFunction('getColorFromStyle', 'czm_tiles3d_style_', shaderState); + showStyleFunction = style.getShowShaderFunction('getShowFromStyle', 'czm_tiles3d_style_', shaderState); + pointSizeStyleFunction = style.getPointSizeShaderFunction('getPointSizeFromStyle', 'czm_tiles3d_style_', shaderState); + if (defined(colorStyleFunction) && shaderState.translucent) { + styleTranslucent = true; + } + } + + content._styleTranslucent = styleTranslucent; + + var hasColorStyle = defined(colorStyleFunction); + var hasShowStyle = defined(showStyleFunction); + var hasPointSizeStyle = defined(pointSizeStyleFunction); + + // Get the properties in use by the style + var styleableProperties = []; + + if (hasColorStyle) { + getStyleableProperties(colorStyleFunction, styleableProperties); + colorStyleFunction = modifyStyleFunction(colorStyleFunction); + } + if (hasShowStyle) { + getStyleableProperties(showStyleFunction, styleableProperties); + showStyleFunction = modifyStyleFunction(showStyleFunction); + } + if (hasPointSizeStyle) { + getStyleableProperties(pointSizeStyleFunction, styleableProperties); + pointSizeStyleFunction = modifyStyleFunction(pointSizeStyleFunction); + } + + var usesColorSemantic = styleableProperties.indexOf('COLOR') >= 0; + var usesNormalSemantic = styleableProperties.indexOf('NORMAL') >= 0; + + // Split default properties from user properties + var userProperties = styleableProperties.filter(function(property) { return defaultProperties.indexOf(property) === -1; }); + + if (usesNormalSemantic && !hasNormals) { + throw new RuntimeError('Style references the NORMAL semantic but the point cloud does not have normals'); + } + + // Disable vertex attributes that aren't used in the style, enable attributes that are + var styleableShaderAttributes = content._styleableShaderAttributes; + for (name in styleableShaderAttributes) { + if (styleableShaderAttributes.hasOwnProperty(name)) { + attribute = styleableShaderAttributes[name]; + var enabled = (userProperties.indexOf(name) >= 0); + var vertexAttribute = getVertexAttribute(vertexArray, attribute.location); + vertexAttribute.enabled = enabled; + } + } + + var usesColors = hasColors && (!hasColorStyle || usesColorSemantic); + if (hasColors) { + // Disable the color vertex attribute if the color style does not reference the color semantic + var colorVertexAttribute = getVertexAttribute(vertexArray, colorLocation); + colorVertexAttribute.enabled = usesColors; + } + + var attributeLocations = { + a_position : positionLocation + }; + if (usesColors) { + attributeLocations.a_color = colorLocation; + } + if (hasNormals) { + attributeLocations.a_normal = normalLocation; + } + if (hasBatchIds) { + attributeLocations.a_batchId = batchIdLocation; + } + + var attributeDeclarations = ''; + + var length = userProperties.length; + for (i = 0; i < length; ++i) { + name = userProperties[i]; + attribute = styleableShaderAttributes[name]; + if (!defined(attribute)) { + throw new RuntimeError('Style references a property "' + name + '" that does not exist or is not styleable.'); + } + + var componentCount = attribute.componentCount; + var attributeName = 'czm_tiles3d_style_' + name; + var attributeType; + if (componentCount === 1) { + attributeType = 'float'; + } else { + attributeType = 'vec' + componentCount; + } + + attributeDeclarations += 'attribute ' + attributeType + ' ' + attributeName + '; \n'; + attributeLocations[attributeName] = attribute.location; + } + + var vs = 'attribute vec3 a_position; \n' + + 'varying vec4 v_color; \n' + + 'uniform vec2 u_pointSizeAndTilesetTime; \n' + + 'uniform vec4 u_constantColor; \n' + + 'uniform vec4 u_highlightColor; \n' + + 'float u_pointSize; \n' + + 'float u_tilesetTime; \n'; + + vs += attributeDeclarations; + + if (usesColors) { + if (isTranslucent) { + vs += 'attribute vec4 a_color; \n'; + } else if (isRGB565) { + vs += 'attribute float a_color; \n' + + 'const float SHIFT_RIGHT_11 = 1.0 / 2048.0; \n' + + 'const float SHIFT_RIGHT_5 = 1.0 / 32.0; \n' + + 'const float SHIFT_LEFT_11 = 2048.0; \n' + + 'const float SHIFT_LEFT_5 = 32.0; \n' + + 'const float NORMALIZE_6 = 1.0 / 64.0; \n' + + 'const float NORMALIZE_5 = 1.0 / 32.0; \n'; + } else { + vs += 'attribute vec3 a_color; \n'; + } + } + if (hasNormals) { + if (isOctEncoded16P) { + vs += 'attribute vec2 a_normal; \n'; + } else { + vs += 'attribute vec3 a_normal; \n'; + } + } + + if (hasBatchIds) { + vs += 'attribute float a_batchId; \n'; + } + + if (isQuantized) { + vs += 'uniform vec3 u_quantizedVolumeScale; \n'; + } + + if (hasColorStyle) { + vs += colorStyleFunction; + } + + if (hasShowStyle) { + vs += showStyleFunction; + } + + if (hasPointSizeStyle) { + vs += pointSizeStyleFunction; + } + + vs += 'void main() \n' + + '{ \n' + + ' u_pointSize = u_pointSizeAndTilesetTime.x; \n' + + ' u_tilesetTime = u_pointSizeAndTilesetTime.y; \n'; + + if (usesColors) { + if (isTranslucent) { + vs += ' vec4 color = a_color; \n'; + } else if (isRGB565) { + vs += ' float compressed = a_color; \n' + + ' float r = floor(compressed * SHIFT_RIGHT_11); \n' + + ' compressed -= r * SHIFT_LEFT_11; \n' + + ' float g = floor(compressed * SHIFT_RIGHT_5); \n' + + ' compressed -= g * SHIFT_LEFT_5; \n' + + ' float b = compressed; \n' + + ' vec3 rgb = vec3(r * NORMALIZE_5, g * NORMALIZE_6, b * NORMALIZE_5); \n' + + ' vec4 color = vec4(rgb, 1.0); \n'; + } else { + vs += ' vec4 color = vec4(a_color, 1.0); \n'; + } + } else { + vs += ' vec4 color = u_constantColor; \n'; + } + + if (isQuantized) { + vs += ' vec3 position = a_position * u_quantizedVolumeScale; \n'; + } else { + vs += ' vec3 position = a_position; \n'; + } + vs += ' vec3 position_absolute = vec3(czm_model * vec4(position, 1.0)); \n'; + + if (hasNormals) { + if (isOctEncoded16P) { + vs += ' vec3 normal = czm_octDecode(a_normal); \n'; + } else { + vs += ' vec3 normal = a_normal; \n'; + } + } else { + vs += ' vec3 normal = vec3(1.0); \n'; + } + + if (hasColorStyle) { + vs += ' color = getColorFromStyle(position, position_absolute, color, normal); \n'; + } + + if (hasShowStyle) { + vs += ' float show = float(getShowFromStyle(position, position_absolute, color, normal)); \n'; + } + + if (hasPointSizeStyle) { + vs += ' gl_PointSize = getPointSizeFromStyle(position, position_absolute, color, normal); \n'; + } else { + vs += ' gl_PointSize = u_pointSize; \n'; + } + + vs += ' color = color * u_highlightColor; \n'; + + if (hasNormals) { + vs += ' normal = czm_normal * normal; \n' + + ' float diffuseStrength = czm_getLambertDiffuse(czm_sunDirectionEC, normal); \n' + + ' diffuseStrength = max(diffuseStrength, 0.4); \n' + // Apply some ambient lighting + ' color *= diffuseStrength; \n'; + } + + vs += ' v_color = color; \n' + + ' gl_Position = czm_modelViewProjection * vec4(position, 1.0); \n'; + + if (hasNormals && backFaceCulling) { + vs += ' float visible = step(-normal.z, 0.0); \n' + + ' gl_Position *= visible; \n'; + } + + if (hasShowStyle) { + vs += ' gl_Position *= show; \n'; + } + + vs += '} \n'; + + var fs = 'varying vec4 v_color; \n' + + 'void main() \n' + + '{ \n' + + ' gl_FragColor = v_color; \n' + + '} \n'; + + var drawVS = vs; + var drawFS = fs; + + if (hasBatchTable) { + // Batched points always use the HIGHLIGHT color blend mode + drawVS = batchTable.getVertexShaderCallback(false, 'a_batchId')(drawVS); + drawFS = batchTable.getFragmentShaderCallback(false, undefined)(drawFS); + } + + var pickVS = vs; + var pickFS = fs; + + if (hasBatchTable) { + pickVS = batchTable.getPickVertexShaderCallback('a_batchId')(pickVS); + pickFS = batchTable.getPickFragmentShaderCallback()(pickFS); + } else { + pickFS = ShaderSource.createPickFragmentShaderSource(pickFS, 'uniform'); + } + + var drawCommand = content._drawCommand; + if (defined(drawCommand.shaderProgram)) { + // Destroy the old shader + drawCommand.shaderProgram.destroy(); + } + drawCommand.shaderProgram = ShaderProgram.fromCache({ + context : context, + vertexShaderSource : drawVS, + fragmentShaderSource : drawFS, + attributeLocations : attributeLocations + }); + + var pickCommand = content._pickCommand; + if (defined(pickCommand.shaderProgram)) { + // Destroy the old shader + pickCommand.shaderProgram.destroy(); + } + pickCommand.shaderProgram = ShaderProgram.fromCache({ + context : context, + vertexShaderSource : pickVS, + fragmentShaderSource : pickFS, + attributeLocations : attributeLocations + }); + + try { + // Check if the shader compiles correctly. If not there is likely a syntax error with the style. + drawCommand.shaderProgram._bind(); + } catch (error) { + // Rephrase the error. + throw new RuntimeError('Error generating style shader: this may be caused by a type mismatch, index out-of-bounds, or other syntax error.'); + } + } + + function createFeatures(content) { + var tileset = content._tileset; + var featuresLength = content.featuresLength; + if (!defined(content._features) && (featuresLength > 0)) { + var features = new Array(featuresLength); + for (var i = 0; i < featuresLength; ++i) { + features[i] = new Cesium3DTileFeature(tileset, content, i); + } + content._features = features; + } + } + + /** + * @inheritdoc Cesium3DTileContent#hasProperty + */ + PointCloud3DTileContent.prototype.hasProperty = function(batchId, name) { + if (defined(this._batchTable)) { + return this._batchTable.hasProperty(batchId, name); + } + return false; + }; + + /** + * Part of the {@link Cesium3DTileContent} interface. + * + * In this context a feature refers to a group of points that share the same BATCH_ID. + * For example all the points that represent a door in a house point cloud would be a feature. + * + * Features are backed by a batch table and can be colored, shown/hidden, picked, etc like features + * in b3dm and i3dm. + * + * When the BATCH_ID semantic is omitted and the point cloud stores per-point properties, they + * are not accessible by getFeature. They are only used for dynamic styling. + */ + PointCloud3DTileContent.prototype.getFeature = function(batchId) { + if (!defined(this._batchTable)) { + return undefined; + } + var featuresLength = this.featuresLength; + //>>includeStart('debug', pragmas.debug); + if (!defined(batchId) || (batchId < 0) || (batchId >= featuresLength)) { + throw new DeveloperError('batchId is required and between zero and featuresLength - 1 (' + (featuresLength - 1) + ').'); + } + //>>includeEnd('debug'); + createFeatures(this); + return this._features[batchId]; + }; + + /** + * @inheritdoc Cesium3DTileContent#applyDebugSettings + */ + PointCloud3DTileContent.prototype.applyDebugSettings = function(enabled, color) { + this._highlightColor = enabled ? color : Color.WHITE; + }; + + /** + * @inheritdoc Cesium3DTileContent#applyStyle + */ + PointCloud3DTileContent.prototype.applyStyle = function(frameState, style) { + if (defined(this._batchTable)) { + this._batchTable.applyStyle(frameState, style); + } else { + createShaders(this, frameState, style); + } + }; + + var scratchComputedTranslation = new Cartesian4(); + var scratchComputedMatrixIn2D = new Matrix4(); + + /** + * @inheritdoc Cesium3DTileContent#update + */ + PointCloud3DTileContent.prototype.update = function(tileset, frameState) { + var modelMatrix = this._tile.computedTransform; + var modelMatrixChanged = !Matrix4.equals(this._modelMatrix, modelMatrix); + var updateModelMatrix = modelMatrixChanged || this._mode !== frameState.mode; + + this._mode = frameState.mode; + + if (!defined(this._drawCommand)) { + createResources(this, frameState); + createShaders(this, frameState, tileset.style); + updateModelMatrix = true; + + this._readyPromise.resolve(this); + this._parsedContent = undefined; // Unload + } + + if (updateModelMatrix) { + Matrix4.clone(modelMatrix, this._modelMatrix); + if (defined(this._rtcCenter)) { + Matrix4.multiplyByTranslation(modelMatrix, this._rtcCenter, this._drawCommand.modelMatrix); + } else if (defined(this._quantizedVolumeOffset)) { + Matrix4.multiplyByTranslation(modelMatrix, this._quantizedVolumeOffset, this._drawCommand.modelMatrix); + } else { + Matrix4.clone(modelMatrix, this._drawCommand.modelMatrix); + } + + if (frameState.mode !== SceneMode.SCENE3D) { + var projection = frameState.mapProjection; + modelMatrix = this._drawCommand.modelMatrix; + var translation = Matrix4.getColumn(modelMatrix, 3, scratchComputedTranslation); + if (!Cartesian4.equals(translation, Cartesian4.UNIT_W)) { + Transforms.basisTo2D(projection, modelMatrix, modelMatrix); + } else { + var center = this._tile.boundingSphere.center; + var to2D = Transforms.wgs84To2DModelMatrix(projection, center, scratchComputedMatrixIn2D); + Matrix4.multiply(to2D, modelMatrix, modelMatrix); + } + } + + Matrix4.clone(this._drawCommand.modelMatrix, this._pickCommand.modelMatrix); + + var boundingVolume; + if (defined(this._tile._contentBoundingVolume)) { + boundingVolume = this._mode === SceneMode.SCENE3D ? this._tile._contentBoundingVolume.boundingSphere : this._tile._contentBoundingVolume2D.boundingSphere; + } else { + boundingVolume = this._mode === SceneMode.SCENE3D ? this._tile._boundingVolume.boundingSphere : this._tile._boundingVolume2D.boundingSphere; + } + + this._drawCommand.boundingVolume = boundingVolume; + this._pickCommand.boundingVolume = boundingVolume; + } + + this._drawCommand.castShadows = ShadowMode.castShadows(tileset.shadows); + this._drawCommand.receiveShadows = ShadowMode.receiveShadows(tileset.shadows); + + if (this.backFaceCulling !== this._backFaceCulling) { + this._backFaceCulling = this.backFaceCulling; + createShaders(this, frameState, tileset.style); + } + + // Update the render state + var isTranslucent = (this._highlightColor.alpha < 1.0) || (this._constantColor.alpha < 1.0) || this._styleTranslucent; + this._drawCommand.renderState = isTranslucent ? this._translucentRenderState : this._opaqueRenderState; + this._drawCommand.pass = isTranslucent ? Pass.TRANSLUCENT : Pass.CESIUM_3D_TILE; + + if (defined(this._batchTable)) { + this._batchTable.update(tileset, frameState); + } + + var commandList = frameState.commandList; + + var passes = frameState.passes; + if (passes.render) { + commandList.push(this._drawCommand); + } + if (passes.pick) { + commandList.push(this._pickCommand); + } + }; + + /** + * @inheritdoc Cesium3DTileContent#isDestroyed + */ + PointCloud3DTileContent.prototype.isDestroyed = function() { + return false; + }; + + /** + * @inheritdoc Cesium3DTileContent#destroy + */ + PointCloud3DTileContent.prototype.destroy = function() { + var command = this._drawCommand; + var pickCommand = this._pickCommand; + if (defined(command)) { + command.vertexArray = command.vertexArray && command.vertexArray.destroy(); + command.shaderProgram = command.shaderProgram && command.shaderProgram.destroy(); + pickCommand.shaderProgram = pickCommand.shaderProgram && pickCommand.shaderProgram.destroy(); + } + this._batchTable = this._batchTable && this._batchTable.destroy(); + return destroyObject(this); + }; + + return PointCloud3DTileContent; +}); diff --git a/Source/Scene/Primitive.js b/Source/Scene/Primitive.js index 3530c58d6df0..a9ba8c4e2d75 100644 --- a/Source/Scene/Primitive.js +++ b/Source/Scene/Primitive.js @@ -36,8 +36,6 @@ define([ './BatchTable', './CullFace', './DepthFunction', - './Material', - './PolylineMaterialAppearance', './PrimitivePipeline', './PrimitiveState', './SceneMode', @@ -79,8 +77,6 @@ define([ BatchTable, CullFace, DepthFunction, - Material, - PolylineMaterialAppearance, PrimitivePipeline, PrimitiveState, SceneMode, diff --git a/Source/Scene/QuadtreeTileProvider.js b/Source/Scene/QuadtreeTileProvider.js index f577b1596c6e..55fc8b4434c3 100644 --- a/Source/Scene/QuadtreeTileProvider.js +++ b/Source/Scene/QuadtreeTileProvider.js @@ -105,7 +105,7 @@ define([ * Gets the maximum geometric error allowed in a tile at a given level, in meters. This function should not be * called before {@link QuadtreeTileProvider#ready} returns true. * - * @see {QuadtreeTileProvider.computeDefaultLevelZeroMaximumGeometricError} + * @see QuadtreeTileProvider#computeDefaultLevelZeroMaximumGeometricError * * @memberof QuadtreeTileProvider * @function @@ -205,7 +205,7 @@ define([ * * @example * provider = provider && provider(); - * + * * @see QuadtreeTileProvider#isDestroyed */ QuadtreeTileProvider.prototype.destroy = DeveloperError.throwInstantiationError; diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index 79dae9125212..2489e3a3d192 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -1631,6 +1631,11 @@ define([ return; } + if (command instanceof ClearCommand) { + command.execute(context, passState); + return; + } + var shadowsEnabled = scene.frameState.shadowHints.shadowsEnabled; var lightShadowsEnabled = shadowsEnabled && (scene.frameState.shadowHints.lightShadowMaps.length > 0); @@ -1925,6 +1930,13 @@ define([ } } + us.updatePass(Pass.CESIUM_3D_TILE); + commands = frustumCommands.commands[Pass.CESIUM_3D_TILE]; + length = frustumCommands.indices[Pass.CESIUM_3D_TILE]; + for (j = 0; j < length; ++j) { + executeCommand(commands[j], scene, context, passState); + } + // Execute commands in order by pass up to the translucent pass. // Translucent geometry needs special handling (sorting/OIT). var startPass = Pass.GROUND + 1; @@ -1998,7 +2010,7 @@ define([ var command = commandList[i]; updateDerivedCommands(scene, command); - if (command.castShadows && (command.pass === Pass.GLOBE || command.pass === Pass.OPAQUE || command.pass === Pass.TRANSLUCENT)) { + if (command.castShadows && (command.pass === Pass.GLOBE || command.pass === Pass.CESIUM_3D_TILE || command.pass === Pass.OPAQUE || command.pass === Pass.TRANSLUCENT)) { if (isVisible(command, shadowVolume)) { if (isPointLight) { for (var k = 0; k < numberOfPasses; ++k) { @@ -2768,6 +2780,18 @@ define([ * Returns an object with a `primitive` property that contains the first (top) primitive in the scene * at a particular window coordinate or undefined if nothing is at the location. Other properties may * potentially be set depending on the type of primitive. + *

+ * When a feature of a 3D Tiles tileset is picked, pick returns a {@link Cesium3DTileFeature} object. + *

+ * + * @example + * // On mouse over, color the feature yellow. + * handler.setInputAction(function(movement) { + * var feature = scene.pick(movement.endPosition); + * if (feature instanceof Cesium.Cesium3DTileFeature) { + * feature.color = Cesium.Color.YELLOW; + * } + * }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); * * @param {Cartesian2} windowPosition Window coordinates to perform picking on. * @returns {Object} Object containing the picked primitive. diff --git a/Source/Scene/StyleExpression.js b/Source/Scene/StyleExpression.js new file mode 100644 index 000000000000..81fd8315fe7b --- /dev/null +++ b/Source/Scene/StyleExpression.js @@ -0,0 +1,79 @@ +/*global define*/ +define([ + '../Core/DeveloperError' + ], function( + DeveloperError) { + 'use strict'; + + /** + * An expression for a style applied to a {@link Cesium3DTileset}. + *

+ * Derived classes of this interface evaluate expressions in the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language}. + *

+ *

+ * This type describes an interface and is not intended to be instantiated directly. + *

+ * + * @alias StyleExpression + * @constructor + * + * @see Expression + * @see ConditionsExpression + */ + function StyleExpression() { + } + + /** + * Evaluates the result of an expression, optionally using the provided feature's properties. If the result of + * the expression in the + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language} + * is of type Boolean, Number, or String, the corresponding JavaScript + * primitive type will be returned. If the result is a RegExp, a Javascript RegExp + * object will be returned. If the result is a Cartesian2, Cartesian3, or Cartesian4, + * a {@link Cartesian2}, {@link Cartesian3}, or {@link Cartesian4} object will be returned. If the result argument is + * a {@link Color}, the {@link Cartesian4} value is converted to a {@link Color} and then returned. + * + * @param {FrameState} frameState The frame state. + * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. + * @param {Object} [result] The object onto which to store the result. + * @returns {Boolean|Number|String|RegExp|Cartesian2|Cartesian3|Cartesian4|Color} The result of evaluating the expression. + */ + StyleExpression.prototype.evaluate = function(frameState, feature, result) { + DeveloperError.throwInstantiationError(); + }; + + /** + * Evaluates the result of a Color expression, optionally using the provided feature's properties. + *

+ * This is equivalent to {@link StyleExpression#evaluate} but always returns a {@link Color} object. + *

+ * + * @param {FrameState} frameState The frame state. + * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. + * @param {Color} [result] The object in which to store the result. + * @returns {Color} The modified result parameter or a new Color instance if one was not provided. + */ + StyleExpression.prototype.evaluateColor = function(frameState, feature, result) { + DeveloperError.throwInstantiationError(); + }; + + /** + * Gets the shader function for this expression. + * Returns undefined if the shader function can't be generated from this expression. + * + * @param {String} functionName Name to give to the generated function. + * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. + * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. + * @param {String} returnType The return type of the generated function. + * + * @returns {String} The shader function. + * + * @private + */ + StyleExpression.prototype.getShaderFunction = function(functionName, attributePrefix, shaderState, returnType) { + DeveloperError.throwInstantiationError(); + }; + + return StyleExpression; +}); diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js index b02d0565a7b0..2eb8c6b480ed 100644 --- a/Source/Scene/TileBoundingRegion.js +++ b/Source/Scene/TileBoundingRegion.js @@ -6,7 +6,6 @@ define([ '../Core/Check', '../Core/ColorGeometryInstanceAttribute', '../Core/defaultValue', - '../Core/defined', '../Core/defineProperties', '../Core/Ellipsoid', '../Core/GeometryInstance', @@ -27,7 +26,6 @@ define([ Check, ColorGeometryInstanceAttribute, defaultValue, - defined, defineProperties, Ellipsoid, GeometryInstance, @@ -244,7 +242,7 @@ define([ var vectorScratch = new Cartesian3(); /** - * Gets the distance from the camera to the closest point on the tile. This is used for level-of-detail selection. + * Gets the distance from the camera to the closest point on the tile. This is used for level of detail selection. * * @param {FrameState} frameState The state information of the current rendering frame. * @returns {Number} The distance from the camera to the closest point on the tile, in meters. @@ -339,6 +337,8 @@ define([ * * @param {Color} color The desired color of the primitive's mesh * @return {Primitive} + * + * @private */ TileBoundingRegion.prototype.createDebugVolume = function(color) { //>>includeStart('debug', pragmas.debug); diff --git a/Source/Scene/TileBoundingSphere.js b/Source/Scene/TileBoundingSphere.js index 4c00385fdf4a..2f99c5e4d2e7 100644 --- a/Source/Scene/TileBoundingSphere.js +++ b/Source/Scene/TileBoundingSphere.js @@ -4,7 +4,6 @@ define([ '../Core/Cartesian3', '../Core/Check', '../Core/ColorGeometryInstanceAttribute', - '../Core/defined', '../Core/defineProperties', '../Core/GeometryInstance', '../Core/Matrix4', @@ -16,7 +15,6 @@ define([ Cartesian3, Check, ColorGeometryInstanceAttribute, - defined, defineProperties, GeometryInstance, Matrix4, @@ -107,8 +105,8 @@ define([ //>>includeStart('debug', pragmas.debug); Check.defined('frameState', frameState); //>>includeEnd('debug'); - var bs = this._boundingSphere; - return Math.max(0.0, Cartesian3.distance(bs.center, frameState.camera.positionWC) - bs.radius); + var boundingSphere = this._boundingSphere; + return Math.max(0.0, Cartesian3.distance(boundingSphere.center, frameState.camera.positionWC) - boundingSphere.radius); }; /** @@ -129,6 +127,9 @@ define([ /** * Update the bounding sphere after the tile is transformed. + * + * @param {Cartesian3} center The center of the bounding sphere. + * @param {Number} radius The radius of the bounding sphere. */ TileBoundingSphere.prototype.update = function(center, radius) { Cartesian3.clone(center, this._boundingSphere.center); diff --git a/Source/Scene/TileBoundingVolume.js b/Source/Scene/TileBoundingVolume.js index bb16a303a032..6227dc5b55e8 100644 --- a/Source/Scene/TileBoundingVolume.js +++ b/Source/Scene/TileBoundingVolume.js @@ -19,7 +19,7 @@ define([ } /** - * The underlying bounding volume + * The underlying bounding volume. * * @memberof TileBoundingVolume.prototype * @@ -29,7 +29,7 @@ define([ TileBoundingVolume.prototype.boundingVolume = undefined; /** - * The underlying bounding sphere + * The underlying bounding sphere. * * @memberof TileBoundingVolume.prototype * @@ -38,17 +38,6 @@ define([ */ TileBoundingVolume.prototype.boundingSphere = undefined; - /** - * Creates a debug primitive that shows the outline of the tile bounding - * volume. - * - * @param {Color} color The desired color of the primitive's mesh - * @return {Primitive} - */ - TileBoundingVolume.prototype.createDebugVolume = function(color) { - DeveloperError.throwInstantiationError(); - }; - /** * Calculates the distance between the tile and the camera. * @@ -73,5 +62,16 @@ define([ DeveloperError.throwInstantiationError(); }; + /** + * Creates a debug primitive that shows the outline of the tile bounding + * volume. + * + * @param {Color} color The desired color of the primitive's mesh + * @return {Primitive} + */ + TileBoundingVolume.prototype.createDebugVolume = function(color) { + DeveloperError.throwInstantiationError(); + }; + return TileBoundingVolume; }); diff --git a/Source/Scene/TileOrientedBoundingBox.js b/Source/Scene/TileOrientedBoundingBox.js index 5acf800e86a8..a074396ab48a 100644 --- a/Source/Scene/TileOrientedBoundingBox.js +++ b/Source/Scene/TileOrientedBoundingBox.js @@ -5,8 +5,6 @@ define([ '../Core/Cartesian3', '../Core/Check', '../Core/ColorGeometryInstanceAttribute', - '../Core/defaultValue', - '../Core/defined', '../Core/defineProperties', '../Core/GeometryInstance', '../Core/Matrix3', @@ -20,8 +18,6 @@ define([ Cartesian3, Check, ColorGeometryInstanceAttribute, - defaultValue, - defined, defineProperties, GeometryInstance, Matrix3, @@ -50,7 +46,7 @@ define([ defineProperties(TileOrientedBoundingBox.prototype, { /** - * The underlying bounding volume + * The underlying bounding volume. * * @memberof TileOrientedBoundingBox.prototype * @@ -63,7 +59,7 @@ define([ } }, /** - * The underlying bounding sphere + * The underlying bounding sphere. * * @memberof TileOrientedBoundingBox.prototype * @@ -108,6 +104,11 @@ define([ /** * Update the bounding box after the tile is transformed. + * + * @param {Cartesian3} center The center of the box. + * @param {Matrix3} halfAxes The three orthogonal half-axes of the bounding box. + * Equivalently, the transformation matrix, to rotate and scale a 2x2x2 + * cube centered at the origin. */ TileOrientedBoundingBox.prototype.update = function(center, halfAxes) { Cartesian3.clone(center, this._orientedBoundingBox.center); diff --git a/Source/Scene/Tileset3DTileContent.js b/Source/Scene/Tileset3DTileContent.js new file mode 100644 index 000000000000..e258f23c577a --- /dev/null +++ b/Source/Scene/Tileset3DTileContent.js @@ -0,0 +1,221 @@ +/*global define*/ +define([ + '../Core/defaultValue', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/getStringFromTypedArray', + '../Core/RuntimeError', + '../ThirdParty/when' + ], function( + defaultValue, + defineProperties, + destroyObject, + getStringFromTypedArray, + RuntimeError, + when) { + 'use strict'; + + /** + * Represents content for a tile in a + * {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles} tileset whose + * content points to another 3D Tiles tileset. + *

+ * Implements the {@link Cesium3DTileContent} interface. + *

+ * + * @alias Tileset3DTileContent + * @constructor + * + * @private + */ + function Tileset3DTileContent(tileset, tile, url, arrayBuffer, byteOffset) { + this._tileset = tileset; + this._tile = tile; + this._url = url; + this._readyPromise = when.defer(); + + /** + * @inheritdoc Cesium3DTileContent#featurePropertiesDirty + */ + this.featurePropertiesDirty = false; + + initialize(this, arrayBuffer, byteOffset); + } + + defineProperties(Tileset3DTileContent.prototype, { + /** + * @inheritdoc Cesium3DTileContent#featuresLength + */ + featuresLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#pointsLength + */ + pointsLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#trianglesLength + */ + trianglesLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#geometryByteLength + */ + geometryByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#texturesByteLength + */ + texturesByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTableByteLength + */ + batchTableByteLength : { + get : function() { + return 0; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#innerContents + */ + innerContents : { + get : function() { + return undefined; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#readyPromise + */ + readyPromise : { + get : function() { + return this._readyPromise.promise; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tileset + */ + tileset : { + get : function() { + return this._tileset; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#tile + */ + tile : { + get : function() { + return this._tile; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#url + */ + url : { + get : function() { + return this._url; + } + }, + + /** + * @inheritdoc Cesium3DTileContent#batchTable + */ + batchTable : { + get : function() { + return undefined; + } + } + }); + + function initialize(content, arrayBuffer, byteOffset) { + byteOffset = defaultValue(byteOffset, 0); + var uint8Array = new Uint8Array(arrayBuffer); + var jsonString = getStringFromTypedArray(uint8Array, byteOffset); + var tilesetJson; + + try { + tilesetJson = JSON.parse(jsonString); + } catch (error) { + content._readyPromise.reject(new RuntimeError('Invalid tile content.')); + return; + } + + content._tileset.loadTileset(content._url, tilesetJson, content._tile); + content._readyPromise.resolve(content); + } + + /** + * Part of the {@link Cesium3DTileContent} interface. Tileset3DTileContent + * always returns false since a tile of this type does not have any features. + */ + Tileset3DTileContent.prototype.hasProperty = function(batchId, name) { + return false; + }; + + /** + * Part of the {@link Cesium3DTileContent} interface. Tileset3DTileContent + * always returns undefined since a tile of this type does not have any features. + */ + Tileset3DTileContent.prototype.getFeature = function(batchId) { + return undefined; + }; + + /** + * @inheritdoc Cesium3DTileContent#applyDebugSettings + */ + Tileset3DTileContent.prototype.applyDebugSettings = function(enabled, color) { + }; + + /** + * @inheritdoc Cesium3DTileContent#applyStyle + */ + Tileset3DTileContent.prototype.applyStyle = function(frameState, style) { + }; + + /** + * @inheritdoc Cesium3DTileContent#update + */ + Tileset3DTileContent.prototype.update = function(tileset, frameState) { + }; + + /** + * @inheritdoc Cesium3DTileContent#isDestroyed + */ + Tileset3DTileContent.prototype.isDestroyed = function() { + return false; + }; + + /** + * @inheritdoc Cesium3DTileContent#destroy + */ + Tileset3DTileContent.prototype.destroy = function() { + return destroyObject(this); + }; + + return Tileset3DTileContent; +}); diff --git a/Source/Scene/modelMaterialsCommon.js b/Source/Scene/modelMaterialsCommon.js index 27875e305d3f..b151f9baccd3 100644 --- a/Source/Scene/modelMaterialsCommon.js +++ b/Source/Scene/modelMaterialsCommon.js @@ -237,6 +237,11 @@ define([ } } + // Give the diffuse uniform a semantic to support color replacement in 3D Tiles + if (defined(techniqueParameters.diffuse)) { + techniqueParameters.diffuse.semantic = '_3DTILESDIFFUSE'; + } + // Copy light parameters into technique parameters if (defined(lightParameters)) { for (var lightParamName in lightParameters) { @@ -345,6 +350,15 @@ define([ vertexShader += 'attribute vec4 a_weight;\n'; } + if (options.addBatchIdToGeneratedShaders) { + techniqueAttributes.a_batchId = 'batchId'; + techniqueParameters.batchId = { + semantic: '_BATCHID', + type: WebGLConstants.FLOAT + }; + vertexShader += 'attribute float a_batchId;\n'; + } + var hasSpecular = hasNormals && ((lightingModel === 'BLINN') || (lightingModel === 'PHONG')) && defined(techniqueParameters.specular) && defined(techniqueParameters.shininess); @@ -712,6 +726,7 @@ define([ var lightParameters = generateLightParameters(gltf); var hasCesiumRTCExtension = defined(gltf.extensions) && defined(gltf.extensions.CESIUM_RTC); + var addBatchIdToGeneratedShaders = defaultValue(options.addBatchIdToGeneratedShaders, false); var techniques = {}; var materials = gltf.materials; @@ -724,6 +739,7 @@ define([ var technique = techniques[techniqueKey]; if (!defined(technique)) { technique = generateTechnique(gltf, khrMaterialsCommon, lightParameters, { + addBatchIdToGeneratedShaders : addBatchIdToGeneratedShaders, useCesiumRTCMatrixInShaders : hasCesiumRTCExtension }); techniques[techniqueKey] = technique; diff --git a/Source/Shaders/Builtin/Constants/passCesium3DTile.glsl b/Source/Shaders/Builtin/Constants/passCesium3DTile.glsl new file mode 100644 index 000000000000..0899985e516e --- /dev/null +++ b/Source/Shaders/Builtin/Constants/passCesium3DTile.glsl @@ -0,0 +1,9 @@ +/** + * The automatic GLSL constant for {@link Pass#CESIUM_3D_TILE} + * + * @name czm_passCesium3DTile + * @glslConstant + * + * @see czm_pass + */ +const float czm_passCesium3DTile = 3.0; diff --git a/Source/Shaders/Builtin/Constants/passGround.glsl b/Source/Shaders/Builtin/Constants/passGround.glsl index f8fe879839e8..427bc235d4ba 100644 --- a/Source/Shaders/Builtin/Constants/passGround.glsl +++ b/Source/Shaders/Builtin/Constants/passGround.glsl @@ -6,4 +6,4 @@ * * @see czm_pass */ -const float czm_passGround = 3.0; +const float czm_passGround = 4.0; diff --git a/Source/Shaders/Builtin/Constants/passOpaque.glsl b/Source/Shaders/Builtin/Constants/passOpaque.glsl index bf9006c27097..7f7fbe11276e 100644 --- a/Source/Shaders/Builtin/Constants/passOpaque.glsl +++ b/Source/Shaders/Builtin/Constants/passOpaque.glsl @@ -6,4 +6,4 @@ * * @see czm_pass */ -const float czm_passOpaque = 4.0; +const float czm_passOpaque = 5.0; diff --git a/Source/Shaders/Builtin/Constants/passOverlay.glsl b/Source/Shaders/Builtin/Constants/passOverlay.glsl index 60226ad2e9b3..f780993b6638 100644 --- a/Source/Shaders/Builtin/Constants/passOverlay.glsl +++ b/Source/Shaders/Builtin/Constants/passOverlay.glsl @@ -6,4 +6,4 @@ * * @see czm_pass */ -const float czm_passOverlay = 6.0; +const float czm_passOverlay = 7.0; diff --git a/Source/Shaders/Builtin/Constants/passTranslucent.glsl b/Source/Shaders/Builtin/Constants/passTranslucent.glsl index 25aac109e526..eb2bb2d4b7a1 100644 --- a/Source/Shaders/Builtin/Constants/passTranslucent.glsl +++ b/Source/Shaders/Builtin/Constants/passTranslucent.glsl @@ -6,4 +6,4 @@ * * @see czm_pass */ -const float czm_passTranslucent = 5.0; +const float czm_passTranslucent = 6.0; diff --git a/Source/ThirdParty/jsep.js b/Source/ThirdParty/jsep.js new file mode 100644 index 000000000000..442b3d4531ca --- /dev/null +++ b/Source/ThirdParty/jsep.js @@ -0,0 +1,685 @@ +// JavaScript Expression Parser (JSEP) 0.3.1 +// JSEP may be freely distributed under the MIT License +// http://jsep.from.so/ + +/*global define*/ +define(function() { + +/*global module: true, exports: true, console: true */ +(function (root) { + 'use strict'; + // Node Types + // ---------- + + // This is the full set of types that any JSEP node can be. + // Store them here to save space when minified + var COMPOUND = 'Compound', + IDENTIFIER = 'Identifier', + MEMBER_EXP = 'MemberExpression', + LITERAL = 'Literal', + THIS_EXP = 'ThisExpression', + CALL_EXP = 'CallExpression', + UNARY_EXP = 'UnaryExpression', + BINARY_EXP = 'BinaryExpression', + LOGICAL_EXP = 'LogicalExpression', + CONDITIONAL_EXP = 'ConditionalExpression', + ARRAY_EXP = 'ArrayExpression', + + PERIOD_CODE = 46, // '.' + COMMA_CODE = 44, // ',' + SQUOTE_CODE = 39, // single quote + DQUOTE_CODE = 34, // double quotes + OPAREN_CODE = 40, // ( + CPAREN_CODE = 41, // ) + OBRACK_CODE = 91, // [ + CBRACK_CODE = 93, // ] + QUMARK_CODE = 63, // ? + SEMCOL_CODE = 59, // ; + COLON_CODE = 58, // : + + throwError = function(message, index) { + var error = new Error(message + ' at character ' + index); + error.index = index; + error.description = message; + throw error; + }, + + // Operations + // ---------- + + // Set `t` to `true` to save space (when minified, not gzipped) + t = true, + // Use a quickly-accessible map to store all of the unary operators + // Values are set to `true` (it really doesn't matter) + unary_ops = {'-': t, '!': t, '~': t, '+': t}, + // Also use a map for the binary operations but set their values to their + // binary precedence for quick reference: + // see [Order of operations](http://en.wikipedia.org/wiki/Order_of_operations#Programming_language) + binary_ops = { + '||': 1, '&&': 2, '|': 3, '^': 4, '&': 5, + '==': 6, '!=': 6, '===': 6, '!==': 6, + '<': 7, '>': 7, '<=': 7, '>=': 7, + '<<':8, '>>': 8, '>>>': 8, + '+': 9, '-': 9, + '*': 10, '/': 10, '%': 10 + }, + // Get return the longest key length of any object + getMaxKeyLen = function(obj) { + var max_len = 0, len; + for(var key in obj) { + if((len = key.length) > max_len && obj.hasOwnProperty(key)) { + max_len = len; + } + } + return max_len; + }, + max_unop_len = getMaxKeyLen(unary_ops), + max_binop_len = getMaxKeyLen(binary_ops), + // Literals + // ---------- + // Store the values to return for the various literals we may encounter + literals = { + 'true': true, + 'false': false, + 'null': null + }, + // Except for `this`, which is special. This could be changed to something like `'self'` as well + this_str = 'this', + // Returns the precedence of a binary operator or `0` if it isn't a binary operator + binaryPrecedence = function(op_val) { + return binary_ops[op_val] || 0; + }, + // Utility function (gets called from multiple places) + // Also note that `a && b` and `a || b` are *logical* expressions, not binary expressions + createBinaryExpression = function (operator, left, right) { + var type = (operator === '||' || operator === '&&') ? LOGICAL_EXP : BINARY_EXP; + return { + type: type, + operator: operator, + left: left, + right: right + }; + }, + // `ch` is a character code in the next three functions + isDecimalDigit = function(ch) { + return (ch >= 48 && ch <= 57); // 0...9 + }, + isIdentifierStart = function(ch) { + return (ch === 36) || (ch === 95) || // `$` and `_` + (ch >= 65 && ch <= 90) || // A...Z + (ch >= 97 && ch <= 122) || // a...z + (ch >= 128 && !binary_ops[String.fromCharCode(ch)]); // any non-ASCII that is not an operator + }, + isIdentifierPart = function(ch) { + return (ch === 36) || (ch === 95) || // `$` and `_` + (ch >= 65 && ch <= 90) || // A...Z + (ch >= 97 && ch <= 122) || // a...z + (ch >= 48 && ch <= 57) || // 0...9 + (ch >= 128 && !binary_ops[String.fromCharCode(ch)]); // any non-ASCII that is not an operator + }, + + // Parsing + // ------- + // `expr` is a string with the passed in expression + jsep = function(expr) { + // `index` stores the character number we are currently at while `length` is a constant + // All of the gobbles below will modify `index` as we move along + var index = 0, + charAtFunc = expr.charAt, + charCodeAtFunc = expr.charCodeAt, + exprI = function(i) { return charAtFunc.call(expr, i); }, + exprICode = function(i) { return charCodeAtFunc.call(expr, i); }, + length = expr.length, + + // Push `index` up to the next non-space character + gobbleSpaces = function() { + var ch = exprICode(index); + // space or tab + while(ch === 32 || ch === 9) { + ch = exprICode(++index); + } + }, + + // The main parsing function. Much of this code is dedicated to ternary expressions + gobbleExpression = function() { + var test = gobbleBinaryExpression(), + consequent, alternate; + gobbleSpaces(); + if(exprICode(index) === QUMARK_CODE) { + // Ternary expression: test ? consequent : alternate + index++; + consequent = gobbleExpression(); + if(!consequent) { + throwError('Expected expression', index); + } + gobbleSpaces(); + if(exprICode(index) === COLON_CODE) { + index++; + alternate = gobbleExpression(); + if(!alternate) { + throwError('Expected expression', index); + } + return { + type: CONDITIONAL_EXP, + test: test, + consequent: consequent, + alternate: alternate + }; + } else { + throwError('Expected :', index); + } + } else { + return test; + } + }, + + // Search for the operation portion of the string (e.g. `+`, `===`) + // Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`) + // and move down from 3 to 2 to 1 character until a matching binary operation is found + // then, return that binary operation + gobbleBinaryOp = function() { + gobbleSpaces(); + var biop, to_check = expr.substr(index, max_binop_len), tc_len = to_check.length; + while(tc_len > 0) { + if(binary_ops.hasOwnProperty(to_check)) { + index += tc_len; + return to_check; + } + to_check = to_check.substr(0, --tc_len); + } + return false; + }, + + // This function is responsible for gobbling an individual expression, + // e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)` + gobbleBinaryExpression = function() { + var ch_i, node, biop, prec, stack, biop_info, left, right, i; + + // First, try to get the leftmost thing + // Then, check to see if there's a binary operator operating on that leftmost thing + left = gobbleToken(); + biop = gobbleBinaryOp(); + + // If there wasn't a binary operator, just return the leftmost node + if(!biop) { + return left; + } + + // Otherwise, we need to start a stack to properly place the binary operations in their + // precedence structure + biop_info = { value: biop, prec: binaryPrecedence(biop)}; + + right = gobbleToken(); + if(!right) { + throwError("Expected expression after " + biop, index); + } + stack = [left, biop_info, right]; + + // Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm) + while((biop = gobbleBinaryOp())) { + prec = binaryPrecedence(biop); + + if(prec === 0) { + break; + } + biop_info = { value: biop, prec: prec }; + + // Reduce: make a binary expression from the three topmost entries. + while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { + right = stack.pop(); + biop = stack.pop().value; + left = stack.pop(); + node = createBinaryExpression(biop, left, right); + stack.push(node); + } + + node = gobbleToken(); + if(!node) { + throwError("Expected expression after " + biop, index); + } + stack.push(biop_info, node); + } + + i = stack.length - 1; + node = stack[i]; + while(i > 1) { + node = createBinaryExpression(stack[i - 1].value, stack[i - 2], node); + i -= 2; + } + return node; + }, + + // An individual part of a binary expression: + // e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis) + gobbleToken = function() { + var ch, to_check, tc_len; + + gobbleSpaces(); + ch = exprICode(index); + + if(isDecimalDigit(ch) || ch === PERIOD_CODE) { + // Char code 46 is a dot `.` which can start off a numeric literal + return gobbleNumericLiteral(); + } else if(ch === SQUOTE_CODE || ch === DQUOTE_CODE) { + // Single or double quotes + return gobbleStringLiteral(); + } else if(isIdentifierStart(ch) || ch === OPAREN_CODE) { // open parenthesis + // `foo`, `bar.baz` + return gobbleVariable(); + } else if (ch === OBRACK_CODE) { + return gobbleArray(); + } else { + to_check = expr.substr(index, max_unop_len); + tc_len = to_check.length; + while(tc_len > 0) { + if(unary_ops.hasOwnProperty(to_check)) { + index += tc_len; + return { + type: UNARY_EXP, + operator: to_check, + argument: gobbleToken(), + prefix: true + }; + } + to_check = to_check.substr(0, --tc_len); + } + + return false; + } + }, + // Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to + // keep track of everything in the numeric literal and then calling `parseFloat` on that string + gobbleNumericLiteral = function() { + var number = '', ch, chCode; + while(isDecimalDigit(exprICode(index))) { + number += exprI(index++); + } + + if(exprICode(index) === PERIOD_CODE) { // can start with a decimal marker + number += exprI(index++); + + while(isDecimalDigit(exprICode(index))) { + number += exprI(index++); + } + } + + ch = exprI(index); + if(ch === 'e' || ch === 'E') { // exponent marker + number += exprI(index++); + ch = exprI(index); + if(ch === '+' || ch === '-') { // exponent sign + number += exprI(index++); + } + while(isDecimalDigit(exprICode(index))) { //exponent itself + number += exprI(index++); + } + if(!isDecimalDigit(exprICode(index-1)) ) { + throwError('Expected exponent (' + number + exprI(index) + ')', index); + } + } + + + chCode = exprICode(index); + // Check to make sure this isn't a variable name that start with a number (123abc) + if(isIdentifierStart(chCode)) { + throwError('Variable names cannot start with a number (' + + number + exprI(index) + ')', index); + } else if(chCode === PERIOD_CODE) { + throwError('Unexpected period', index); + } + + return { + type: LITERAL, + value: parseFloat(number), + raw: number + }; + }, + + // Parses a string literal, staring with single or double quotes with basic support for escape codes + // e.g. `"hello world"`, `'this is\nJSEP'` + gobbleStringLiteral = function() { + var str = '', quote = exprI(index++), closed = false, ch; + + while(index < length) { + ch = exprI(index++); + if(ch === quote) { + closed = true; + break; + } else if(ch === '\\') { + // Check for all of the common escape codes + ch = exprI(index++); + switch(ch) { + case 'n': str += '\n'; break; + case 'r': str += '\r'; break; + case 't': str += '\t'; break; + case 'b': str += '\b'; break; + case 'f': str += '\f'; break; + case 'v': str += '\x0B'; break; + default : str += '\\' + ch; + } + } else { + str += ch; + } + } + + if(!closed) { + throwError('Unclosed quote after "'+str+'"', index); + } + + return { + type: LITERAL, + value: str, + raw: quote + str + quote + }; + }, + + // Gobbles only identifiers + // e.g.: `foo`, `_value`, `$x1` + // Also, this function checks if that identifier is a literal: + // (e.g. `true`, `false`, `null`) or `this` + gobbleIdentifier = function() { + var ch = exprICode(index), start = index, identifier; + + if(isIdentifierStart(ch)) { + index++; + } else { + throwError('Unexpected ' + exprI(index), index); + } + + while(index < length) { + ch = exprICode(index); + if(isIdentifierPart(ch)) { + index++; + } else { + break; + } + } + identifier = expr.slice(start, index); + + if(literals.hasOwnProperty(identifier)) { + return { + type: LITERAL, + value: literals[identifier], + raw: identifier + }; + } else if(identifier === this_str) { + return { type: THIS_EXP }; + } else { + return { + type: IDENTIFIER, + name: identifier + }; + } + }, + + // Gobbles a list of arguments within the context of a function call + // or array literal. This function also assumes that the opening character + // `(` or `[` has already been gobbled, and gobbles expressions and commas + // until the terminator character `)` or `]` is encountered. + // e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]` + gobbleArguments = function(termination) { + var ch_i, args = [], node, closed = false; + while(index < length) { + gobbleSpaces(); + ch_i = exprICode(index); + if(ch_i === termination) { // done parsing + closed = true; + index++; + break; + } else if (ch_i === COMMA_CODE) { // between expressions + index++; + } else { + node = gobbleExpression(); + if(!node || node.type === COMPOUND) { + throwError('Expected comma', index); + } + args.push(node); + } + } + if (!closed) { + throwError('Expected ' + String.fromCharCode(termination), index); + } + return args; + }, + + // Gobble a non-literal variable name. This variable name may include properties + // e.g. `foo`, `bar.baz`, `foo['bar'].baz` + // It also gobbles function calls: + // e.g. `Math.acos(obj.angle)` + gobbleVariable = function() { + var ch_i, node; + ch_i = exprICode(index); + + if(ch_i === OPAREN_CODE) { + node = gobbleGroup(); + } else { + node = gobbleIdentifier(); + } + gobbleSpaces(); + ch_i = exprICode(index); + while(ch_i === PERIOD_CODE || ch_i === OBRACK_CODE || ch_i === OPAREN_CODE) { + index++; + if(ch_i === PERIOD_CODE) { + gobbleSpaces(); + node = { + type: MEMBER_EXP, + computed: false, + object: node, + property: gobbleIdentifier() + }; + } else if(ch_i === OBRACK_CODE) { + node = { + type: MEMBER_EXP, + computed: true, + object: node, + property: gobbleExpression() + }; + gobbleSpaces(); + ch_i = exprICode(index); + if(ch_i !== CBRACK_CODE) { + throwError('Unclosed [', index); + } + index++; + } else if(ch_i === OPAREN_CODE) { + // A function call is being made; gobble all the arguments + node = { + type: CALL_EXP, + 'arguments': gobbleArguments(CPAREN_CODE), + callee: node + }; + } + gobbleSpaces(); + ch_i = exprICode(index); + } + return node; + }, + + // Responsible for parsing a group of things within parentheses `()` + // This function assumes that it needs to gobble the opening parenthesis + // and then tries to gobble everything within that parenthesis, assuming + // that the next thing it should see is the close parenthesis. If not, + // then the expression probably doesn't have a `)` + gobbleGroup = function() { + index++; + var node = gobbleExpression(); + gobbleSpaces(); + if(exprICode(index) === CPAREN_CODE) { + index++; + return node; + } else { + throwError('Unclosed (', index); + } + }, + + // Responsible for parsing Array literals `[1, 2, 3]` + // This function assumes that it needs to gobble the opening bracket + // and then tries to gobble the expressions as arguments. + gobbleArray = function() { + index++; + return { + type: ARRAY_EXP, + elements: gobbleArguments(CBRACK_CODE) + }; + }, + + nodes = [], ch_i, node; + + while(index < length) { + ch_i = exprICode(index); + + // Expressions can be separated by semicolons, commas, or just inferred without any + // separators + if(ch_i === SEMCOL_CODE || ch_i === COMMA_CODE) { + index++; // ignore separators + } else { + // Try to gobble each expression individually + if((node = gobbleExpression())) { + nodes.push(node); + // If we weren't able to find a binary expression and are out of room, then + // the expression passed in probably has too much + } else if(index < length) { + throwError('Unexpected "' + exprI(index) + '"', index); + } + } + } + + // If there's only one expression just try returning the expression + if(nodes.length === 1) { + return nodes[0]; + } else { + return { + type: COMPOUND, + body: nodes + }; + } + }; + + // To be filled in by the template + jsep.version = '0.3.1'; + jsep.toString = function() { return 'JavaScript Expression Parser (JSEP) v' + jsep.version; }; + + /** + * @method jsep.addUnaryOp + * @param {string} op_name The name of the unary op to add + * @return jsep + */ + jsep.addUnaryOp = function(op_name) { + max_unop_len = Math.max(op_name.length, max_unop_len); + unary_ops[op_name] = t; return this; + }; + + /** + * @method jsep.addBinaryOp + * @param {string} op_name The name of the binary op to add + * @param {number} precedence The precedence of the binary op (can be a float) + * @return jsep + */ + jsep.addBinaryOp = function(op_name, precedence) { + max_binop_len = Math.max(op_name.length, max_binop_len); + binary_ops[op_name] = precedence; + return this; + }; + + /** + * @method jsep.addLiteral + * @param {string} literal_name The name of the literal to add + * @param {*} literal_value The value of the literal + * @return jsep + */ + jsep.addLiteral = function(literal_name, literal_value) { + literals[literal_name] = literal_value; + return this; + }; + + /** + * @method jsep.removeUnaryOp + * @param {string} op_name The name of the unary op to remove + * @return jsep + */ + jsep.removeUnaryOp = function(op_name) { + delete unary_ops[op_name]; + if(op_name.length === max_unop_len) { + max_unop_len = getMaxKeyLen(unary_ops); + } + return this; + }; + + /** + * @method jsep.removeAllUnaryOps + * @return jsep + */ + jsep.removeAllUnaryOps = function() { + unary_ops = {}; + max_unop_len = 0; + + return this; + }; + + /** + * @method jsep.removeBinaryOp + * @param {string} op_name The name of the binary op to remove + * @return jsep + */ + jsep.removeBinaryOp = function(op_name) { + delete binary_ops[op_name]; + if(op_name.length === max_binop_len) { + max_binop_len = getMaxKeyLen(binary_ops); + } + return this; + }; + + /** + * @method jsep.removeAllBinaryOps + * @return jsep + */ + jsep.removeAllBinaryOps = function() { + binary_ops = {}; + max_binop_len = 0; + + return this; + }; + + /** + * @method jsep.removeLiteral + * @param {string} literal_name The name of the literal to remove + * @return jsep + */ + jsep.removeLiteral = function(literal_name) { + delete literals[literal_name]; + return this; + }; + + /** + * @method jsep.removeAllLiterals + * @return jsep + */ + jsep.removeAllLiterals = function() { + literals = {}; + + return this; + }; + + // In desktop environments, have a way to restore the old value for `jsep` + if (typeof exports === 'undefined') { + var old_jsep = root.jsep; + // The star of the show! It's a function! + root.jsep = jsep; + // And a courteous function willing to move out of the way for other similarly-named objects! + jsep.noConflict = function() { + if(root.jsep === jsep) { + root.jsep = old_jsep; + } + return jsep; + }; + } else { + // In Node.JS environments + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = jsep; + } else { + exports.parse = jsep; + } + } +}(this)); + + return jsep.noConflict(); +}); diff --git a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.css b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.css new file mode 100644 index 000000000000..196038b4a88e --- /dev/null +++ b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.css @@ -0,0 +1,149 @@ + +ul.cesium-cesiumInspector-statistics { + margin: 0; + padding-top: 3px; + padding-bottom: 3px; +} + +ul.cesium-cesiumInspector-statistics + ul.cesium-cesiumInspector-statistics { + border-top: 1px solid #aaa; +} + +.cesium-cesiumInspector-slider { + margin-top: 5px; +} + +.cesium-cesiumInspector-slider input[type=number] { + text-align: left; + background-color: #222; + outline: none; + border: 1px solid #444; + color: #edffff; + width: 100px; + border-radius: 3px; + padding: 1px; + margin-left: 10px; + cursor: auto; +} + +.cesium-cesiumInspector-slider input[type=number]::-webkit-outer-spin-button, +.cesium-cesiumInspector-slider input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.cesium-cesiumInspector-slider input[type=range] { + margin-left: 5px; + vertical-align: middle; + -webkit-appearance: none; + background: #ddd; + height: 3px; +} + +input[type=range]:focus { + outline: none; +} + +.cesium-cesiumInspector-slider input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + border: 1px solid #000000; + height: 10px; + width: 10px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; +} + +.cesium-cesiumInspector-slider input[type=range]::-moz-range-thumb { + border: 1px solid #000000; + height: 10px; + width: 10px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; +} + +.cesium-cesiumInspector-slider input[type=range]::-moz-range-thumb { + border: 1px solid #000000; + height: 10px; + width: 10px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; +} + +.cesium-cesiumInspector-hide .cesium-cesiumInspector-styleEditor { + display: none; +} + +.cesium-cesiumInspector-styleEditor { + padding: 10px; + border-radius: 5px; + background: rgba(48, 51, 54, 0.8); + border: 1px solid #444; +} + +.cesium-cesiumInspector-styleEditor textarea { + width: 100%; + height: 300px; + background: transparent; + color: #edffff; + border: none; + padding: 0; + white-space: pre; + overflow-wrap: normal; + overflow-x: auto; +} + +.cesium-3DTilesInspector { + width: 300px; + pointer-events: all; +} + +.cesium-3DTilesInspector-statistics { + font-size: 11px; +} + +.cesium-3DTilesInspector div, .cesium-3DTilesInspector input[type=range] { + width: 100%; + box-sizing: border-box; +} + +.cesium-cesiumInspector-error { + color: #ff9e9e; + overflow: auto; +} + +.cesium-3DTilesInspector .cesium-cesiumInspector-section { + margin-top: 3px; +} + +.cesium-3DTilesInspector .cesium-cesiumInspector-sectionHeader + .cesium-cesiumInspector-show { + border-top: 1px solid white; +} + +input.cesium-cesiumInspector-url { + overflow: hidden; + white-space: nowrap; + overflow-x: scroll; + background-color: transparent; + color: white; + outline: none; + border: none; + height: 1em; + width: 100%; +} + +.cesium-cesiumInspector .field-group { + display: table; +} + +.cesium-cesiumInspector .field-group > label { + display: table-cell; + font-weight: bold; +} + +.cesium-cesiumInspector .field-group > .field { + display: table-cell; + width: 100%; +} diff --git a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.js b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.js new file mode 100644 index 000000000000..8c90f59d6a67 --- /dev/null +++ b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.js @@ -0,0 +1,284 @@ +/*global define*/ +define([ + '../../Core/Check', + '../../Core/defaultValue', + '../../Core/defined', + '../../Core/defineProperties', + '../../Core/destroyObject', + '../../ThirdParty/knockout', + '../getElement', + './Cesium3DTilesInspectorViewModel' +], function( + Check, + defaultValue, + defined, + defineProperties, + destroyObject, + knockout, + getElement, + Cesium3DTilesInspectorViewModel) { + 'use strict'; + + /** + * Inspector widget to aid in debugging 3D Tiles + * + * @alias Cesium3DTilesInspector + * @constructor + * + * @param {Element|String} container The DOM element or ID that will contain the widget. + * @param {Scene} scene the Scene instance to use. + */ + function Cesium3DTilesInspector(container, scene) { + //>includeStart('debug', pragmas.debug); + Check.defined('container', container); + Check.typeOf.object('scene', scene); + //>>includeEnd('debug'); + + container = getElement(container); + var element = document.createElement('div'); + var performanceContainer = document.createElement('div'); + performanceContainer.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : performance, "cesium-cesiumInspector-hide" : !performance}'); + var viewModel = new Cesium3DTilesInspectorViewModel(scene, performanceContainer); + + this._viewModel = viewModel; + this._container = container; + this._element = element; + + var text = document.createElement('div'); + text.textContent = '3D Tiles Inspector'; + text.className = 'cesium-cesiumInspector-button'; + text.setAttribute('data-bind', 'click: toggleInspector'); + element.appendChild(text); + element.className = 'cesium-cesiumInspector cesium-3DTilesInspector'; + element.setAttribute('data-bind', 'css: { "cesium-cesiumInspector-visible" : inspectorVisible, "cesium-cesiumInspector-hidden" : !inspectorVisible}'); + container.appendChild(element); + + var tilesetPanelContents = document.createElement('div'); + var displayPanelContents = document.createElement('div'); + var updatePanelContents = document.createElement('div'); + var loggingPanelContents = document.createElement('div'); + var tileDebugLabelsPanelContents = document.createElement('div'); + var stylePanelContents = document.createElement('div'); + var optimizationPanelContents = document.createElement('div'); + + var properties = document.createElement('div'); + properties.className = 'field-group'; + var propertiesLabel = document.createElement('label'); + propertiesLabel.className = 'field-label'; + propertiesLabel.appendChild(document.createTextNode('Properties: ')); + var propertiesField = document.createElement('div'); + propertiesField.setAttribute('data-bind', 'text: properties'); + properties.appendChild(propertiesLabel); + properties.appendChild(propertiesField); + tilesetPanelContents.appendChild(properties); + tilesetPanelContents.appendChild(makeButton('togglePickTileset', 'Pick Tileset', 'pickActive')); + tilesetPanelContents.appendChild(makeButton('trimTilesCache', 'Trim Tiles Cache')); + tilesetPanelContents.appendChild(makeCheckbox('picking', 'Enable Picking')); + + displayPanelContents.appendChild(makeCheckbox('colorize', 'Colorize')); + displayPanelContents.appendChild(makeCheckbox('wireframe', 'Wireframe')); + displayPanelContents.appendChild(makeCheckbox('showBoundingVolumes', 'Bounding Volumes')); + displayPanelContents.appendChild(makeCheckbox('showContentBoundingVolumes', 'Content Volumes')); + displayPanelContents.appendChild(makeCheckbox('showRequestVolumes', 'Request Volumes')); + + updatePanelContents.appendChild(makeCheckbox('freezeFrame', 'Freeze Frame')); + updatePanelContents.appendChild(makeCheckbox('dynamicScreenSpaceError', 'Dynamic Screen Space Error')); + var sseContainer = document.createElement('div'); + sseContainer.appendChild(makeRangeInput('maximumScreenSpaceError', 0, 128, 1, 'Maximum Screen Space Error')); + updatePanelContents.appendChild(sseContainer); + var dynamicScreenSpaceErrorContainer = document.createElement('div'); + dynamicScreenSpaceErrorContainer.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : dynamicScreenSpaceError, "cesium-cesiumInspector-hide" : !dynamicScreenSpaceError}'); + dynamicScreenSpaceErrorContainer.appendChild(makeRangeInput('dynamicScreenSpaceErrorDensitySliderValue', 0, 1, 0.005, 'Screen Space Error Density', 'dynamicScreenSpaceErrorDensity')); + dynamicScreenSpaceErrorContainer.appendChild(makeRangeInput('dynamicScreenSpaceErrorFactor', 1, 10, 0.1, 'Screen Space Error Factor')); + updatePanelContents.appendChild(dynamicScreenSpaceErrorContainer); + + loggingPanelContents.appendChild(makeCheckbox('performance', 'Performance')); + loggingPanelContents.appendChild(performanceContainer); + loggingPanelContents.appendChild(makeCheckbox('showStatistics', 'Statistics')); + var statistics = document.createElement('div'); + statistics.className = 'cesium-3dTilesInspector-statistics'; + statistics.setAttribute('data-bind', 'html: statisticsText, visible: showStatistics'); + loggingPanelContents.appendChild(statistics); + loggingPanelContents.appendChild(makeCheckbox('showPickStatistics', 'Pick Statistics')); + var pickStatistics = document.createElement('div'); + pickStatistics.className = 'cesium-3dTilesInspector-statistics'; + pickStatistics.setAttribute('data-bind', 'html: pickStatisticsText, visible: showPickStatistics'); + loggingPanelContents.appendChild(pickStatistics); + + stylePanelContents.appendChild(document.createTextNode('Color Blend Mode: ')); + var blendDropdown = document.createElement('select'); + blendDropdown.setAttribute('data-bind', 'options: colorBlendModes, ' + + 'optionsText: "text", ' + + 'optionsValue: "value", ' + + 'value: colorBlendMode'); + stylePanelContents.appendChild(blendDropdown); + var styleEditor = document.createElement('textarea'); + styleEditor.setAttribute('data-bind', 'textInput: styleString, event: { keydown: styleEditorKeyPress }'); + stylePanelContents.className = 'cesium-cesiumInspector-styleEditor'; + stylePanelContents.appendChild(styleEditor); + var closeStylesBtn = makeButton('compileStyle', 'Compile (Ctrl+Enter)'); + stylePanelContents.appendChild(closeStylesBtn); + var errorBox = document.createElement('div'); + errorBox.className = 'cesium-cesiumInspector-error'; + errorBox.setAttribute('data-bind', 'text: editorError'); + stylePanelContents.appendChild(errorBox); + + tileDebugLabelsPanelContents.appendChild(makeCheckbox('showOnlyPickedTileDebugLabel', 'Show Picked Only')); + tileDebugLabelsPanelContents.appendChild(makeCheckbox('showGeometricError', 'Geometric Error')); + tileDebugLabelsPanelContents.appendChild(makeCheckbox('showRenderingStatistics', 'Rendering Statistics')); + tileDebugLabelsPanelContents.appendChild(makeCheckbox('showMemoryUsage', 'Memory Usage (MB)')); + + optimizationPanelContents.appendChild(makeCheckbox('skipLevelOfDetail', 'Skip Tile LODs')); + var skipScreenSpaceErrorFactorContainer = document.createElement('div'); + skipScreenSpaceErrorFactorContainer.appendChild(makeRangeInput('skipScreenSpaceErrorFactor', 1, 50, 1, 'Skip SSE Factor')); + optimizationPanelContents.appendChild(skipScreenSpaceErrorFactorContainer); + var baseScreenSpaceError = document.createElement('div'); + baseScreenSpaceError.appendChild(makeRangeInput('baseScreenSpaceError', 0, 4096, 1, 'SSE before skipping LOD')); + optimizationPanelContents.appendChild(baseScreenSpaceError); + var skipLevelsContainer = document.createElement('div'); + skipLevelsContainer.appendChild(makeRangeInput('skipLevels', 0, 10, 1, 'Min. levels to skip')); + optimizationPanelContents.appendChild(skipLevelsContainer); + optimizationPanelContents.appendChild(makeCheckbox('immediatelyLoadDesiredLevelOfDetail', 'Load only tiles that meet the max. SSE.')); + optimizationPanelContents.appendChild(makeCheckbox('loadSiblings', 'Load siblings of visible tiles.')); + + var tilesetPanel = makeSection('Tileset', 'tilesetVisible', 'toggleTileset', tilesetPanelContents); + var displayPanel = makeSection('Display', 'displayVisible', 'toggleDisplay', displayPanelContents); + var updatePanel = makeSection('Update', 'updateVisible', 'toggleUpdate', updatePanelContents); + var loggingPanel = makeSection('Logging', 'loggingVisible', 'toggleLogging', loggingPanelContents); + var tileDebugLabelsPanel = makeSection('Tile Debug Labels', 'tileDebugLabelsVisible', 'toggleTileDebugLabels', tileDebugLabelsPanelContents); + var stylePanel = makeSection('Style', 'styleVisible', 'toggleStyle', stylePanelContents); + var optimizationPanel = makeSection('Optimization', 'optimizationVisible', 'toggleOptimization', optimizationPanelContents); + + // first add and bind all the toggleable panels + element.appendChild(tilesetPanel); + element.appendChild(displayPanel); + element.appendChild(updatePanel); + element.appendChild(loggingPanel); + element.appendChild(tileDebugLabelsPanel); + element.appendChild(stylePanel); + element.appendChild(optimizationPanel); + + knockout.applyBindings(viewModel, element); + } + + defineProperties(Cesium3DTilesInspector.prototype, { + /** + * Gets the parent container. + * @memberof Cesium3DTilesInspector.prototype + * + * @type {Element} + */ + container : { + get : function() { + return this._container; + } + }, + + /** + * Gets the view model. + * @memberof Cesium3DTilesInspector.prototype + * + * @type {Cesium3DTilesInspectorViewModel} + */ + viewModel : { + get : function() { + return this._viewModel; + } + } + }); + + /** + * @returns {Boolean} true if the object has been destroyed, false otherwise. + */ + Cesium3DTilesInspector.prototype.isDestroyed = function() { + return false; + }; + + /** + * Destroys the widget. Should be called if permanently + * removing the widget from layout. + */ + Cesium3DTilesInspector.prototype.destroy = function() { + knockout.cleanNode(this._element); + this._container.removeChild(this._element); + this.viewModel.destroy(); + + return destroyObject(this); + }; + + function makeSection(name, visibleProp, toggleProp, contents) { + var toggle = document.createElement('span'); + toggle.className = 'cesium-cesiumInspector-toggleSwitch'; + toggle.setAttribute('data-bind', 'text: ' + visibleProp + ' ? "-" : "+", click: ' + toggleProp); + + var header = document.createElement('div'); + header.className = 'cesium-cesiumInspector-sectionHeader'; + header.appendChild(toggle); + header.appendChild(document.createTextNode(name)); + + var section = document.createElement('div'); + section.className = 'cesium-cesiumInspector-section'; + section.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : ' + visibleProp + ', "cesium-cesiumInspector-hide" : !' + visibleProp + '}'); + section.appendChild(contents); + + var panel = document.createElement('div'); + panel.className = 'cesium-cesiumInspector-dropDown'; + panel.appendChild(header); + panel.appendChild(section); + + return panel; + } + + function makeCheckbox(property, text) { + var checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.setAttribute('data-bind', 'checked: ' + property); + + var container = document.createElement('div'); + container.appendChild(checkbox); + container.appendChild(document.createTextNode(text)); + + return container; + } + + function makeRangeInput(property, min, max, step, text, displayProperty) { + displayProperty = defaultValue(displayProperty, property); + var input = document.createElement('input'); + input.setAttribute('data-bind', 'value: ' + displayProperty); + input.type = 'number'; + + var slider = document.createElement('input'); + slider.type = 'range'; + slider.min = min; + slider.max = max; + slider.step = step; + slider.setAttribute('data-bind', 'valueUpdate: "input", value: ' + property); + + var wrapper = document.createElement('div'); + wrapper.appendChild(slider); + + var container = document.createElement('div'); + container.className = 'cesium-cesiumInspector-slider'; + container.appendChild(document.createTextNode(text)); + container.appendChild(input); + container.appendChild(wrapper); + + return container; + } + + function makeButton(action, text, active) { + var button = document.createElement('button'); + button.type = 'button'; + button.textContent = text; + button.className = 'cesium-cesiumInspector-pickButton'; + var binding = 'click: ' + action; + if (defined(active)) { + binding += ', css: {"cesium-cesiumInspector-pickButtonHighlight" : ' + active + '}'; + } + button.setAttribute('data-bind', binding); + + return button; + } + + return Cesium3DTilesInspector; +}); diff --git a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js new file mode 100644 index 000000000000..37b71bafdc8a --- /dev/null +++ b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js @@ -0,0 +1,1238 @@ +/*global define*/ +define([ + '../../Scene/Cesium3DTileFeature', + '../../Scene/Cesium3DTileset', + '../../Scene/Cesium3DTileStyle', + '../../Scene/Cesium3DTileColorBlendMode', + '../../Core/Check', + '../../Core/Color', + '../../Core/defined', + '../../Core/defineProperties', + '../../Core/destroyObject', + '../../ThirdParty/knockout', + '../../Scene/PerformanceDisplay', + '../../Core/ScreenSpaceEventHandler', + '../../Core/ScreenSpaceEventType' +], function( + Cesium3DTileFeature, + Cesium3DTileset, + Cesium3DTileStyle, + Cesium3DTileColorBlendMode, + Check, + Color, + defined, + defineProperties, + destroyObject, + knockout, + PerformanceDisplay, + ScreenSpaceEventHandler, + ScreenSpaceEventType) { + 'use strict'; + + function getPickTileset(viewModel) { + return function(e) { + var pick = viewModel._scene.pick(e.position); + if (defined(pick) && pick.primitive instanceof Cesium3DTileset) { + viewModel.tileset = pick.primitive; + } + viewModel.pickActive = false; + }; + } + + var stringOptions = { + maximumFractionDigits : 3 + }; + + function formatMemoryString(memorySizeInBytes) { + var memoryInMegabytes = memorySizeInBytes / 1048576; + if (memoryInMegabytes < 1.0) { + return memoryInMegabytes.toLocaleString(undefined, stringOptions); + } + return Math.round(memoryInMegabytes).toLocaleString(); + } + + function getStatistics(tileset, isPick) { + if (!defined(tileset)) { + return ''; + } + + var statistics = tileset.statistics; + + // Since the pick pass uses a smaller frustum around the pixel of interest, + // the statistics will be different than the normal render pass. + var s = '
    '; + s += + // --- Rendering statistics + '
  • Visited: ' + statistics.visited.toLocaleString() + '
  • ' + + // Number of commands returned is likely to be higher than the number of tiles selected + // because of tiles that create multiple commands. + '
  • Selected: ' + tileset._selectedTiles.length.toLocaleString() + '
  • ' + + // Number of commands executed is likely to be higher because of commands overlapping + // multiple frustums. + '
  • Commands: ' + statistics.numberOfCommands.toLocaleString() + '
  • '; + s += '
'; + if (!isPick) { + s += '
    '; + s += + // --- Cache/loading statistics + '
  • Requests: ' + statistics.numberOfPendingRequests.toLocaleString() + '
  • ' + + '
  • Attempted: ' + statistics.numberOfAttemptedRequests.toLocaleString() + '
  • ' + + '
  • Processing: ' + statistics.numberOfTilesProcessing.toLocaleString() + '
  • ' + + '
  • Content Ready: ' + statistics.numberOfTilesWithContentReady.toLocaleString() + '
  • ' + + // Total number of tiles includes tiles without content, so "Ready" may never reach + // "Total." Total also will increase when a tile with a tileset.json content is loaded. + '
  • Total: ' + statistics.numberOfTilesTotal.toLocaleString() + '
  • '; + s += '
'; + s += '
    '; + s += + // --- Features statistics + '
  • Features Selected: ' + statistics.numberOfFeaturesSelected.toLocaleString() + '
  • ' + + '
  • Features Loaded: ' + statistics.numberOfFeaturesLoaded.toLocaleString() + '
  • ' + + '
  • Points Selected: ' + statistics.numberOfPointsSelected.toLocaleString() + '
  • ' + + '
  • Points Loaded: ' + statistics.numberOfPointsLoaded.toLocaleString() + '
  • ' + + '
  • Triangles Selected: ' + statistics.numberOfTrianglesSelected.toLocaleString() + '
  • '; + s += '
'; + s += '
    '; + s += + // --- Styling statistics + '
  • Tiles styled: ' + statistics.numberOfTilesStyled.toLocaleString() + '
  • ' + + '
  • Features styled: ' + statistics.numberOfFeaturesStyled.toLocaleString() + '
  • '; + s += '
'; + s += '
    '; + s += + // --- Optimization statistics + '
  • Children Union Culled: ' + statistics.numberOfTilesCulledWithChildrenUnion.toLocaleString() + '
  • '; + s += '
'; + s += '
    '; + s += + // --- Memory statistics + '
  • Geometry Memory (MB): ' + formatMemoryString(statistics.geometryByteLength) + '
  • ' + + '
  • Texture Memory (MB): ' + formatMemoryString(statistics.texturesByteLength) + '
  • ' + + '
  • Batch Table Memory (MB): ' + formatMemoryString(statistics.batchTableByteLength) + '
  • '; + s += '
'; + } + return s; + } + + var colorBlendModes = [{ + text : 'Highlight', + value : Cesium3DTileColorBlendMode.HIGHLIGHT + }, { + text : 'Replace', + value : Cesium3DTileColorBlendMode.REPLACE + }, { + text : 'Mix', + value : Cesium3DTileColorBlendMode.MIX + }]; + + var highlightColor = new Color(1.0, 1.0, 0.0, 0.4); + var scratchColor = new Color(); + var oldColor = new Color(); + + /** + * The view model for {@link Cesium3DTilesInspector}. + * @alias Cesium3DTilesInspectorViewModel + * @constructor + * + * @param {Scene} scene The scene instance to use. + * @param {HTMLElement} performanceContainer The container for the performance display + */ + function Cesium3DTilesInspectorViewModel(scene, performanceContainer) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('scene', scene); + Check.typeOf.object('performanceContainer', performanceContainer); + //>>includeEnd('debug'); + + var that = this; + var canvas = scene.canvas; + this._eventHandler = new ScreenSpaceEventHandler(canvas); + this._scene = scene; + this._performanceContainer = performanceContainer; + this._canvas = canvas; + + this._performanceDisplay = new PerformanceDisplay({ + container : performanceContainer + }); + + this._statisticsText = ''; + this._pickStatisticsText = ''; + this._editorError = ''; + + /** + * Gets or sets the flag to enable performance display. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.performance = false; + + /** + * Gets or sets the flag to show statistics. This property is observable. + * + * @type {Boolean} + * @default true + */ + this.showStatistics = true; + + /** + * Gets or sets the flag to show pick statistics. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.showPickStatistics = true; + + /** + * Gets or sets the flag to show the inspector. This property is observable. + * + * @type {Boolean} + * @default true + */ + this.inspectorVisible = true; + + /** + * Gets or sets the flag to show the tileset section. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.tilesetVisible = false; + + /** + * Gets or sets the flag to show the display section. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.displayVisible = false; + + /** + * Gets or sets the flag to show the update section. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.updateVisible = false; + + /** + * Gets or sets the flag to show the logging section. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.loggingVisible = false; + + /** + * Gets or sets the flag to show the style section. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.styleVisible = false; + + /** + * Gets or sets the flag to show the tile info section. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.tileDebugLabelsVisible = false; + + /** + * Gets or sets the flag to show the optimization info section. This property is observable. + * + * @type {Boolean} + * @default false; + */ + this.optimizationVisible = false; + + /** + * Gets or sets the JSON for the tileset style. This property is observable. + * + * @type {String} + * @default '{}' + */ + this.styleString = '{}'; + + this._tileset = undefined; + this._feature = undefined; + this._tile = undefined; + + knockout.track(this, ['performance', 'inspectorVisible', '_statisticsText', '_pickStatisticsText', '_editorError', 'showPickStatistics', 'showStatistics', + 'tilesetVisible', 'displayVisible', 'updateVisible', 'loggingVisible', 'styleVisible', 'optimizationVisible', + 'tileDebugLabelsVisible', 'styleString', '_feature', '_tile']); + + this._properties = knockout.observable({}); + /** + * Gets the names of the properties in the tileset. This property is observable. + * @type {String[]} + * @readonly + */ + this.properties = []; + knockout.defineProperty(this, 'properties', function() { + var names = []; + var properties = that._properties(); + for (var prop in properties) { + if (properties.hasOwnProperty(prop)) { + names.push(prop); + } + } + return names; + }); + + var dynamicScreenSpaceError = knockout.observable(); + knockout.defineProperty(this, 'dynamicScreenSpaceError', { + get : function() { + return dynamicScreenSpaceError(); + }, + set : function(value) { + dynamicScreenSpaceError(value); + if (defined(that._tileset)) { + that._tileset.dynamicScreenSpaceError = value; + } + } + }); + /** + * Gets or sets the flag to enable dynamic screen space error. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.dynamicScreenSpaceError = false; + + var colorBlendMode = knockout.observable(); + knockout.defineProperty(this, 'colorBlendMode', { + get : function() { + return colorBlendMode(); + }, + set : function(value) { + colorBlendMode(value); + if (defined(that._tileset)) { + that._tileset.colorBlendMode = value; + } + } + }); + /** + * Gets or sets the color blend mode. This property is observable. + * + * @type {Cesium3DTileColorBlendMode} + * @default Cesium3DTileColorBlendMode.HIGHLIGHT + */ + this.colorBlendMode = Cesium3DTileColorBlendMode.HIGHLIGHT; + + var picking = knockout.observable(); + knockout.defineProperty(this, 'picking', { + get : function() { + return picking(); + }, + set : function(value) { + picking(value); + if (value) { + that._eventHandler.setInputAction(function(e) { + var picked = scene.pick(e.endPosition); + if (picked instanceof Cesium3DTileFeature) { + // Picked a feature + that.feature = picked; + that.tile = picked.content.tile; + } else if (defined(picked) && defined(picked.content)) { + // Picked a tile + that.feature = undefined; + that.tile = picked.content.tile; + } else { + // Picked nothing + that.feature = undefined; + that.tile = undefined; + } + if (!defined(that._tileset)) { + return; + } + if (showOnlyPickedTileDebugLabel && defined(picked) && defined(picked.content)) { + var position; + if (scene.pickPositionSupported) { + position = scene.pickPosition(e.endPosition); + if (defined(position)) { + that._tileset.debugPickPosition = position; + } + } + that._tileset.debugPickedTile = picked.content.tile; + } else { + that._tileset.debugPickedTile = undefined; + } + }, ScreenSpaceEventType.MOUSE_MOVE); + } else { + that.feature = undefined; + that.tile = undefined; + that._eventHandler.removeInputAction(ScreenSpaceEventType.MOUSE_MOVE); + } + } + }); + /** + * Gets or sets the flag to enable picking. This property is observable. + * + * @type {Boolean} + * @default true + */ + this.picking = true; + + var colorize = knockout.observable(); + knockout.defineProperty(this, 'colorize', { + get : function() { + return colorize(); + }, + set : function(value) { + colorize(value); + if (defined(that._tileset)) { + that._tileset.debugColorizeTiles = value; + } + } + }); + /** + * Gets or sets the flag to colorize tiles. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.colorize = false; + + var wireframe = knockout.observable(); + knockout.defineProperty(this, 'wireframe', { + get : function() { + return wireframe(); + }, + set : function(value) { + wireframe(value); + if (defined(that._tileset)) { + that._tileset.debugWireframe = value; + } + } + }); + /** + * Gets or sets the flag to draw with wireframe. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.wireframe = false; + + var showBoundingVolumes = knockout.observable(); + knockout.defineProperty(this, 'showBoundingVolumes', { + get : function() { + return showBoundingVolumes(); + }, + set : function(value) { + showBoundingVolumes(value); + if (defined(that._tileset)) { + that._tileset.debugShowBoundingVolume = value; + } + } + }); + /** + * Gets or sets the flag to show bounding volumes. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.showBoundingVolumes = false; + + var showContentBoundingVolumes = knockout.observable(); + knockout.defineProperty(this, 'showContentBoundingVolumes', { + get : function() { + return showContentBoundingVolumes(); + }, + set : function(value) { + showContentBoundingVolumes(value); + if (defined(that._tileset)) { + that._tileset.debugShowContentBoundingVolume = value; + } + } + }); + /** + * Gets or sets the flag to show content volumes. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.showContentBoundingVolumes = false; + + var showRequestVolumes = knockout.observable(); + knockout.defineProperty(this, 'showRequestVolumes', { + get : function() { + return showRequestVolumes(); + }, + set : function(value) { + showRequestVolumes(value); + if (defined(that._tileset)) { + that._tileset.debugShowViewerRequestVolume = value; + } + } + }); + /** + * Gets or sets the flag to show request volumes. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.showRequestVolumes = false; + + var freezeFrame = knockout.observable(); + knockout.defineProperty(this, 'freezeFrame', { + get : function() { + return freezeFrame(); + }, + set : function(value) { + freezeFrame(value); + if (defined(that._tileset)) { + that._tileset.debugFreezeFrame = value; + that._scene.debugShowFrustumPlanes = value; + } + } + }); + /** + * Gets or sets the flag to suspend updates. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.freezeFrame = false; + + var showOnlyPickedTileDebugLabel = knockout.observable(); + knockout.defineProperty(this, 'showOnlyPickedTileDebugLabel', { + get : function() { + return showOnlyPickedTileDebugLabel(); + }, + set : function(value) { + showOnlyPickedTileDebugLabel(value); + if (defined(that._tileset)) { + that._tileset.debugPickedTileLabelOnly = value; + } + } + }); + /** + * Gets or sets the flag to show debug labels only for the currently picked tile. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.showOnlyPickedTileDebugLabel = false; + + var showGeometricError = knockout.observable(); + knockout.defineProperty(this, 'showGeometricError', { + get : function() { + return showGeometricError(); + }, + set : function(value) { + showGeometricError(value); + if (defined(that._tileset)) { + that._tileset.debugShowGeometricError = value; + } + } + }); + /** + * Gets or sets the flag to show tile geometric error. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.showGeometricError = false; + + var showRenderingStatistics = knockout.observable(); + knockout.defineProperty(this, 'showRenderingStatistics', { + get : function() { + return showRenderingStatistics(); + }, + set : function(value) { + showRenderingStatistics(value); + if (defined(that._tileset)) { + that._tileset.debugShowRenderingStatistics = value; + } + } + }); + /** + * Displays the number of commands, points, triangles and features used per tile. This property is observable. + * @memberof Cesium3DTilesInspectorViewModel.prototype + * + * @type {Boolean} + * @default false + */ + this.showRenderingStatistics = false; + + var showMemoryUsage = knockout.observable(); + knockout.defineProperty(this, 'showMemoryUsage', { + get : function() { + return showMemoryUsage(); + }, + set : function(value) { + showMemoryUsage(value); + if (defined(that._tileset)) { + that._tileset.debugShowMemoryUsage = value; + } + } + }); + /** + * Displays the memory used per tile. This property is observable. + * @memberof Cesium3DTilesInspectorViewModel.prototype + * + * @type {Boolean} + * @default false + */ + this.showMemoryUsage = false; + + var maximumScreenSpaceError = knockout.observable(); + knockout.defineProperty(this, 'maximumScreenSpaceError', { + get : function() { + return maximumScreenSpaceError(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + maximumScreenSpaceError(value); + if (defined(that._tileset)) { + that._tileset.maximumScreenSpaceError = value; + } + } + } + }); + /** + * Gets or sets the maximum screen space error. This property is observable. + * + * @type {Number} + * @default 16 + */ + this.maximumScreenSpaceError = 16; + + var dynamicScreenSpaceErrorDensity = knockout.observable(); + knockout.defineProperty(this, 'dynamicScreenSpaceErrorDensity', { + get : function() { + return dynamicScreenSpaceErrorDensity(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + dynamicScreenSpaceErrorDensity(value); + if (defined(that._tileset)) { + that._tileset.dynamicScreenSpaceErrorDensity = value; + } + } + } + }); + /** + * Gets or sets the dynamic screen space error density. This property is observable. + * + * @type {Number} + * @default 0.00278 + */ + this.dynamicScreenSpaceErrorDensity = 0.00278; + + /** + * Gets or sets the dynamic screen space error density slider value. + * This allows the slider to be exponential because values tend to be closer to 0 than 1. + * This property is observable. + * + * @type {Number} + * @default 0.00278 + */ + this.dynamicScreenSpaceErrorDensitySliderValue = undefined; + knockout.defineProperty(this, 'dynamicScreenSpaceErrorDensitySliderValue', { + get : function() { + return Math.pow(dynamicScreenSpaceErrorDensity(), 1 / 6); + }, + set : function(value) { + dynamicScreenSpaceErrorDensity(Math.pow(value, 6)); + } + }); + + var dynamicScreenSpaceErrorFactor = knockout.observable(); + knockout.defineProperty(this, 'dynamicScreenSpaceErrorFactor', { + get : function() { + return dynamicScreenSpaceErrorFactor(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + dynamicScreenSpaceErrorFactor(value); + if (defined(that._tileset)) { + that._tileset.dynamicScreenSpaceErrorFactor = value; + } + } + } + }); + /** + * Gets or sets the dynamic screen space error factor. This property is observable. + * + * @type {Number} + * @default 4.0 + */ + this.dynamicScreenSpaceErrorFactor = 4.0; + + var pickTileset = getPickTileset(this); + var pickActive = knockout.observable(); + knockout.defineProperty(this, 'pickActive', { + get : function() { + return pickActive(); + }, + set : function(value) { + pickActive(value); + if (value) { + that._eventHandler.setInputAction(pickTileset, ScreenSpaceEventType.LEFT_CLICK); + } else { + that._eventHandler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK); + } + } + }); + /** + * Gets or sets the pick state + * + * @type {Boolean} + * @default false + */ + this.pickActive = false; + + var skipLevelOfDetail = knockout.observable(); + knockout.defineProperty(this, 'skipLevelOfDetail', { + get : function() { + return skipLevelOfDetail(); + }, + set : function(value) { + skipLevelOfDetail(value); + if (defined(that._tileset)) { + that._tileset.skipLevelOfDetail = value; + } + } + }); + /** + * Gets or sets the flag to determine if level of detail skipping should be applied during the traversal. + * This property is observable. + * @type {Boolean} + * @default true + */ + this.skipLevelOfDetail = true; + + var skipScreenSpaceErrorFactor = knockout.observable(); + knockout.defineProperty(this, 'skipScreenSpaceErrorFactor', { + get : function() { + return skipScreenSpaceErrorFactor(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + skipScreenSpaceErrorFactor(value); + if (defined(that._tileset)) { + that._tileset.skipScreenSpaceErrorFactor = value; + } + } + } + }); + /** + * Gets or sets the multiplier defining the minimum screen space error to skip. This property is observable. + * @type {Number} + * @default 16 + */ + this.skipScreenSpaceErrorFactor = 16; + + var baseScreenSpaceError = knockout.observable(); + knockout.defineProperty(this, 'baseScreenSpaceError', { + get : function() { + return baseScreenSpaceError(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + baseScreenSpaceError(value); + if (defined(that._tileset)) { + that._tileset.baseScreenSpaceError = value; + } + } + } + }); + /** + * Gets or sets the screen space error that must be reached before skipping levels of detail. This property is observable. + * @type {Number} + * @default 1024 + */ + this.baseScreenSpaceError = 1024; + + var skipLevels = knockout.observable(); + knockout.defineProperty(this, 'skipLevels', { + get : function() { + return skipLevels(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + skipLevels(value); + if (defined(that._tileset)) { + that._tileset.skipLevels = value; + } + } + } + }); + /** + * Gets or sets the constant defining the minimum number of levels to skip when loading tiles. This property is observable. + * @type {Number} + * @default 1 + */ + this.skipLevels = 1; + + var immediatelyLoadDesiredLevelOfDetail = knockout.observable(); + knockout.defineProperty(this, 'immediatelyLoadDesiredLevelOfDetail', { + get : function() { + return immediatelyLoadDesiredLevelOfDetail(); + }, + set : function(value) { + immediatelyLoadDesiredLevelOfDetail(value); + if (defined(that._tileset)) { + that._tileset.immediatelyLoadDesiredLevelOfDetail = value; + } + } + }); + /** + * Gets or sets the flag which, when true, only tiles that meet the maximum screen space error will ever be downloaded. + * This property is observable. + * @type {Boolean} + * @default false + */ + this.immediatelyLoadDesiredLevelOfDetail = false; + + var loadSiblings = knockout.observable(); + knockout.defineProperty(this, 'loadSiblings', { + get : function() { + loadSiblings(); + }, + set : function(value) { + loadSiblings(value); + if (defined(that._tileset)) { + that._tileset.loadSiblings = value; + } + } + }); + /** + * Gets or sets the flag which determines whether siblings of visible tiles are always downloaded during traversal. + * This property is observable + * @type {Boolean} + * @default false + */ + this.loadSiblings = false; + + this._style = undefined; + this._shouldStyle = false; + this._definedProperties = ['properties', 'dynamicScreenSpaceError', 'colorBlendMode', 'picking', 'colorize', 'wireframe', 'showBoundingVolumes', + 'showContentBoundingVolumes', 'showRequestVolumes', 'freezeFrame', 'maximumScreenSpaceError', 'dynamicScreenSpaceErrorDensity', 'baseScreenSpaceError', + 'skipScreenSpaceErrorFactor', 'skipLevelOfDetail', 'skipLevels', 'immediatelyLoadDesiredLevelOfDetail', 'loadSiblings', 'dynamicScreenSpaceErrorDensitySliderValue', + 'dynamicScreenSpaceErrorFactor', 'pickActive', 'showOnlyPickedTileDebugLabel', 'showGeometricError', 'showRenderingStatistics', 'showMemoryUsage']; + this._removePostRenderEvent = scene.postRender.addEventListener(function() { + that._update(); + }); + } + + defineProperties(Cesium3DTilesInspectorViewModel.prototype, { + /** + * Gets the scene + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {Scene} + * @readonly + */ + scene: { + get: function() { + return this._scene; + } + }, + /** + * Gets the performance container + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {HTMLElement} + * @readonly + */ + performanceContainer: { + get: function() { + return this._performanceContainer; + } + }, + + /** + * Gets the statistics text. This property is observable. + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {String} + * @readonly + */ + statisticsText : { + get : function() { + return this._statisticsText; + } + }, + /** + * Gets the pick statistics text. This property is observable. + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {String} + * @readonly + */ + pickStatisticsText : { + get : function() { + return this._pickStatisticsText; + } + }, + + /** + * Gets the available blend modes + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {Object[]} + * @readonly + */ + colorBlendModes : { + get : function() { + return colorBlendModes; + } + }, + + /** + * Gets the editor error message + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {String} + * @readonly + */ + editorError : { + get : function() { + return this._editorError; + } + }, + + /** + * Gets or sets the tileset of the view model. + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {Cesium3DTileset} + */ + tileset : { + get : function() { + return this._tileset; + }, + set : function(tileset) { + this._tileset = tileset; + this._style = undefined; + this.styleString = '{}'; + this.feature = undefined; + this.tile = undefined; + + if (defined(tileset)) { + var that = this; + tileset.readyPromise.then(function(t) { + if (!that.isDestroyed()) { + that._properties(t.properties); + } + }); + + // update tileset with existing settings + var settings = ['colorize', + 'wireframe', + 'showBoundingVolumes', + 'showContentBoundingVolumes', + 'showRequestVolumes', + 'freezeFrame', + 'showOnlyPickedTileDebugLabel', + 'showGeometricError', + 'showRenderingStatistics', + 'showMemoryUsage']; + var length = settings.length; + for (var i = 0; i < length; ++i) { + var setting = settings[i]; + this[setting] = this[setting]; + } + + // update view model with existing tileset settings + this.maximumScreenSpaceError = tileset.maximumScreenSpaceError; + this.dynamicScreenSpaceError = tileset.dynamicScreenSpaceError; + this.dynamicScreenSpaceErrorDensity = tileset.dynamicScreenSpaceErrorDensity; + this.dynamicScreenSpaceErrorFactor = tileset.dynamicScreenSpaceErrorFactor; + this.colorBlendMode = tileset.colorBlendMode; + this.skipLevelOfDetail = tileset.skipLevelOfDetail; + this.skipScreenSpaceErrorFactor = tileset.skipScreenSpaceErrorFactor; + this.baseScreenSpaceError = tileset.baseScreenSpaceError; + this.skipLevels = tileset.skipLevels; + this.immediatelyLoadDesiredLevelOfDetail = tileset.immediatelyLoadDesiredLevelOfDetail; + this.loadSiblings = tileset.loadSiblings; + } else { + this._properties({}); + } + + this._statisticsText = getStatistics(tileset, false); + this._pickStatisticsText = getStatistics(tileset, true); + } + }, + + /** + * Gets the current feature of the view model. + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {Cesium3DTileFeature} + */ + feature : { + get : function() { + return this._feature; + }, + set : function(feature) { + if (this._feature === feature) { + return; + } + var currentFeature = this._feature; + if (defined(currentFeature)) { + // Restore original color to feature that is no longer selected + var frameState = this._scene.frameState; + if (!this.colorize && defined(this._style)) { + currentFeature.color = this._style.color.evaluateColor(frameState, currentFeature, scratchColor); + } else { + currentFeature.color = oldColor; + } + } + if (defined(feature)) { + // Highlight new feature + Color.clone(feature.color, oldColor); + feature.color = highlightColor; + } + this._feature = feature; + } + }, + + /** + * Gets the current tile of the view model + * @memberof Cesium3DTilesInspectorViewModel.prototype + * @type {Cesium3DTile} + */ + tile : { + get : function() { + return this._tile; + }, + set : function(tile) { + if (this._tile === tile) { + return; + } + var currentTile = this._tile; + + if (defined(currentTile) && !hasFeatures(currentTile.content)) { + // Restore original color to tile that is no longer selected + currentTile.color = oldColor; + } + + if (defined(tile) && !hasFeatures(tile.content)) { + // Highlight new tile + Color.clone(tile.color, oldColor); + tile.color = highlightColor; + } + this._tile = tile; + } + } + }); + + function hasFeatures(content) { + if (content.featuresLength > 0) { + return true; + } + var innerContents = content.innerContents; + if (defined(innerContents)) { + var length = innerContents.length; + for (var i = 0; i < length; ++i) { + if (!hasFeatures(innerContents[i])) { + return false; + } + } + return true; + } + return false; + } + + /** + * Toggles the pick tileset mode + */ + Cesium3DTilesInspectorViewModel.prototype.togglePickTileset = function() { + this.pickActive = !this.pickActive; + }; + + /** + * Toggles the inspector visibility + */ + Cesium3DTilesInspectorViewModel.prototype.toggleInspector = function() { + this.inspectorVisible = !this.inspectorVisible; + }; + + /** + * Toggles the visibility of the tileset section + */ + Cesium3DTilesInspectorViewModel.prototype.toggleTileset = function() { + this.tilesetVisible = !this.tilesetVisible; + }; + + /** + * Toggles the visibility of the display section + */ + Cesium3DTilesInspectorViewModel.prototype.toggleDisplay = function() { + this.displayVisible = !this.displayVisible; + }; + + /** + * Toggles the visibility of the update section + */ + Cesium3DTilesInspectorViewModel.prototype.toggleUpdate = function() { + this.updateVisible = !this.updateVisible; + }; + + /** + * Toggles the visibility of the logging section + */ + Cesium3DTilesInspectorViewModel.prototype.toggleLogging = function() { + this.loggingVisible = !this.loggingVisible; + }; + + /** + * Toggles the visibility of the style section + */ + Cesium3DTilesInspectorViewModel.prototype.toggleStyle = function() { + this.styleVisible = !this.styleVisible; + }; + + /** + * Toggles the visibility of the tile Debug Info section + */ + Cesium3DTilesInspectorViewModel.prototype.toggleTileDebugLabels = function() { + this.tileDebugLabelsVisible = !this.tileDebugLabelsVisible; + }; + + /** + * Toggles the visibility of the optimization section + */ + Cesium3DTilesInspectorViewModel.prototype.toggleOptimization = function() { + this.optimizationVisible = !this.optimizationVisible; + }; + + /** + * Trims tile cache + */ + Cesium3DTilesInspectorViewModel.prototype.trimTilesCache = function() { + if (defined(this._tileset)) { + this._tileset.trimLoadedTiles(); + } + }; + + /** + * Compiles the style in the style editor. + */ + Cesium3DTilesInspectorViewModel.prototype.compileStyle = function() { + var tileset = this._tileset; + if (!defined(tileset) || this.styleString === JSON.stringify(tileset.style)) { + return; + } + this._editorError = ''; + try { + if (this.styleString.length === 0) { + this.styleString = '{}'; + } + this._style = new Cesium3DTileStyle(JSON.parse(this.styleString)); + this._shouldStyle = true; + } catch (err) { + this._editorError = err.toString(); + } + + // set feature again so pick coloring is set + this.feature = this._feature; + this.tile = this._tile; + }; + + /** + * Handles key press events on the style editor. + */ + Cesium3DTilesInspectorViewModel.prototype.styleEditorKeyPress = function(sender, event) { + if (event.keyCode === 9) { //tab + event.preventDefault(); + var textArea = event.target; + var start = textArea.selectionStart; + var end = textArea.selectionEnd; + var newEnd = end; + var selected = textArea.value.slice(start, end); + var lines = selected.split('\n'); + var length = lines.length; + var i; + if (!event.shiftKey) { + for (i = 0; i < length; ++i) { + lines[i] = ' ' + lines[i]; + newEnd += 2; + } + } else { + for (i = 0; i < length; ++i) { + if (lines[i][0] === ' ') { + if (lines[i][1] === ' ') { + lines[i] = lines[i].substr(2); + newEnd -= 2; + } else { + lines[i] = lines[i].substr(1); + newEnd -= 1; + } + } + } + } + var newText = lines.join('\n'); + textArea.value = textArea.value.slice(0, start) + newText + textArea.value.slice(end); + textArea.selectionStart = start !== end ? start : newEnd; + textArea.selectionEnd = newEnd; + } else if (event.ctrlKey && (event.keyCode === 10 || event.keyCode === 13)) { //ctrl + enter + this.compileStyle(); + } + return true; + }; + + /** + * Updates the values of view model + * @private + */ + Cesium3DTilesInspectorViewModel.prototype._update = function() { + var tileset = this._tileset; + + if (this.performance) { + this._performanceDisplay.update(); + } + + if (defined(tileset)) { + var style = tileset.style; + if (this._style !== tileset.style) { + if (this._shouldStyle) { + tileset.style = this._style; + this._shouldStyle = false; + } else { + this._style = style; + this.styleString = JSON.stringify(style.style, null, ' '); + } + } + } + if (this.showStatistics) { + this._statisticsText = getStatistics(tileset, false); + this._pickStatisticsText = getStatistics(tileset, true); + } + }; + + /** + * @returns {Boolean} true if the object has been destroyed, false otherwise. + */ + Cesium3DTilesInspectorViewModel.prototype.isDestroyed = function() { + return false; + }; + + /** + * Destroys the widget. Should be called if permanently + * removing the widget from layout. + */ + Cesium3DTilesInspectorViewModel.prototype.destroy = function() { + this._eventHandler.destroy(); + this._removePostRenderEvent(); + + var that = this; + this._definedProperties.forEach(function(property) { + knockout.getObservable(that, property).dispose(); + }); + + return destroyObject(this); + }; + + /** + * Generates an HTML string of the statistics + * @param {Cesium3DTileset} tileset The tileset + * @param {Boolean} isPick Whether this is getting the statistics for the pick pass + * @returns {String} The formatted statistics + */ + Cesium3DTilesInspectorViewModel.getStatistics = getStatistics; + + return Cesium3DTilesInspectorViewModel; +}); diff --git a/Source/Widgets/CesiumInspector/CesiumInspector.css b/Source/Widgets/CesiumInspector/CesiumInspector.css index b62d4a28e692..89b8b66ee65e 100644 --- a/Source/Widgets/CesiumInspector/CesiumInspector.css +++ b/Source/Widgets/CesiumInspector/CesiumInspector.css @@ -55,7 +55,7 @@ width: 185px; } -.cesium-cesiumInspector-frustumStats { +.cesium-cesiumInspector-frustumStatistics { padding-left: 10px; padding: 5px; background-color: rgba(80, 80, 80, 0.75); diff --git a/Source/Widgets/CesiumInspector/CesiumInspector.js b/Source/Widgets/CesiumInspector/CesiumInspector.js index b04ddae222f9..d9dd5d9d360d 100644 --- a/Source/Widgets/CesiumInspector/CesiumInspector.js +++ b/Source/Widgets/CesiumInspector/CesiumInspector.js @@ -80,15 +80,15 @@ define([ var debugShowFrustums = document.createElement('div'); generalSection.appendChild(debugShowFrustums); - var frustumStats = document.createElement('div'); - frustumStats.className = 'cesium-cesiumInspector-frustumStats'; - frustumStats.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : frustums, "cesium-cesiumInspector-hide" : !frustums}, html: frustumStatisticText'); + var frustumStatistics = document.createElement('div'); + frustumStatistics.className = 'cesium-cesiumInspector-frustumStatistics'; + frustumStatistics.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : frustums, "cesium-cesiumInspector-hide" : !frustums}, html: frustumStatisticText'); var frustumsCheckbox = document.createElement('input'); frustumsCheckbox.type = 'checkbox'; frustumsCheckbox.setAttribute('data-bind', 'checked: frustums'); debugShowFrustums.appendChild(frustumsCheckbox); debugShowFrustums.appendChild(document.createTextNode('Show Frustums')); - debugShowFrustums.appendChild(frustumStats); + debugShowFrustums.appendChild(frustumStatistics); var debugShowFrustumPlanes = document.createElement('div'); generalSection.appendChild(debugShowFrustumPlanes); @@ -268,7 +268,7 @@ define([ var tileText = document.createElement('div'); tileText.className = 'cesium-cesiumInspector-tileText'; - tileInfo.className = 'cesium-cesiumInspector-frustumStats'; + tileInfo.className = 'cesium-cesiumInspector-frustumStatistics'; tileInfo.appendChild(tileText); tileInfo.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : hasPickedTile, "cesium-cesiumInspector-hide" : !hasPickedTile}'); tileText.setAttribute('data-bind', 'html: tileText'); diff --git a/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js b/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js index cf1c0b650e6f..7082697ef435 100644 --- a/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js +++ b/Source/Widgets/CesiumInspector/CesiumInspectorViewModel.js @@ -27,11 +27,11 @@ define([ createCommand) { 'use strict'; - function frustumStatsToString(stats) { + function frustumStatisticsToString(statistics) { var str; - if (defined(stats)) { + if (defined(statistics)) { str = 'Command Statistics'; - var com = stats.commandsInFrustums; + var com = statistics.commandsInFrustums; for (var n in com) { if (com.hasOwnProperty(n)) { var num = parseInt(n, 10); @@ -52,7 +52,7 @@ define([ str += '
    ' + com[n] + ' in frustum ' + s; } } - str += '
Total: ' + stats.totalCommands; + str += '
Total: ' + statistics.totalCommands; } return str; @@ -921,7 +921,7 @@ define([ */ CesiumInspectorViewModel.prototype._update = function() { if (this.frustums) { - this.frustumStatisticText = frustumStatsToString(this._scene.debugFrustumStatistics); + this.frustumStatisticText = frustumStatisticsToString(this._scene.debugFrustumStatistics); } // Determine the number of frustums being used. diff --git a/Source/Widgets/ProjectionPicker/ProjectionPickerViewModel.js b/Source/Widgets/ProjectionPicker/ProjectionPickerViewModel.js index 31d13f044631..47efdeb637a2 100644 --- a/Source/Widgets/ProjectionPicker/ProjectionPickerViewModel.js +++ b/Source/Widgets/ProjectionPicker/ProjectionPickerViewModel.js @@ -1,35 +1,21 @@ /*global define*/ define([ - '../../Core/Cartesian2', - '../../Core/Cartesian3', - '../../Core/defaultValue', '../../Core/defined', '../../Core/defineProperties', '../../Core/destroyObject', '../../Core/DeveloperError', '../../Core/EventHelper', - '../../Core/Math', - '../../Core/Matrix4', - '../../Core/Ray', '../../Scene/OrthographicFrustum', - '../../Scene/PerspectiveFrustum', '../../Scene/SceneMode', '../../ThirdParty/knockout', '../createCommand' ], function( - Cartesian2, - Cartesian3, - defaultValue, defined, defineProperties, destroyObject, DeveloperError, EventHelper, - CesiumMath, - Matrix4, - Ray, OrthographicFrustum, - PerspectiveFrustum, SceneMode, knockout, createCommand) { diff --git a/Source/Widgets/Viewer/Viewer.css b/Source/Widgets/Viewer/Viewer.css index 9276afb714e1..8904dea9d882 100644 --- a/Source/Widgets/Viewer/Viewer.css +++ b/Source/Widgets/Viewer/Viewer.css @@ -95,3 +95,14 @@ display: inline-block; margin: 0 3px; } + +.cesium-viewer-cesium3DTilesInspectorContainer { + display: block; + position: absolute; + top: 50px; + right: 10px; + max-height: 100%; + padding-bottom: 70px; + box-sizing: border-box; + overflow: auto; +} diff --git a/Source/Widgets/Viewer/viewerCesium3DTilesInspectorMixin.js b/Source/Widgets/Viewer/viewerCesium3DTilesInspectorMixin.js new file mode 100644 index 000000000000..069189bc5f3b --- /dev/null +++ b/Source/Widgets/Viewer/viewerCesium3DTilesInspectorMixin.js @@ -0,0 +1,44 @@ +/*global define*/ +define([ + '../../Core/Check', + '../../Core/defineProperties', + '../Cesium3DTilesInspector/Cesium3DTilesInspector' + ], function( + Check, + defineProperties, + Cesium3DTilesInspector) { + 'use strict'; + + /** + * A mixin which adds the {@link Cesium3DTilesInspector} widget to the {@link Viewer} widget. + * Rather than being called directly, this function is normally passed as + * a parameter to {@link Viewer#extend}, as shown in the example below. + * @exports viewerCesium3DTilesInspectorMixin + * + * @param {Viewer} viewer The viewer instance. + * + * @example + * var viewer = new Cesium.Viewer('cesiumContainer'); + * viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin); + */ + function viewerCesium3DTilesInspectorMixin(viewer) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('viewer', viewer); + //>>includeEnd('debug'); + + var container = document.createElement('div'); + container.className = 'cesium-viewer-cesium3DTilesInspectorContainer'; + viewer.container.appendChild(container); + var cesium3DTilesInspector = new Cesium3DTilesInspector(container, viewer.scene); + + defineProperties(viewer, { + cesium3DTilesInspector : { + get : function() { + return cesium3DTilesInspector; + } + } + }); + } + + return viewerCesium3DTilesInspectorMixin; +}); diff --git a/Source/Widgets/widgets.css b/Source/Widgets/widgets.css index f291135b644a..b6584d2e5cfd 100644 --- a/Source/Widgets/widgets.css +++ b/Source/Widgets/widgets.css @@ -3,6 +3,7 @@ @import url(./BaseLayerPicker/BaseLayerPicker.css); @import url(./CesiumWidget/CesiumWidget.css); @import url(./CesiumInspector/CesiumInspector.css); +@import url(./Cesium3DTilesInspector/Cesium3DTilesInspector.css); @import url(./FullscreenButton/FullscreenButton.css); @import url(./VRButton/VRButton.css); @import url(./Geocoder/Geocoder.css); diff --git a/Specs/Cesium3DTilesTester.js b/Specs/Cesium3DTilesTester.js new file mode 100644 index 000000000000..07bff2877fe1 --- /dev/null +++ b/Specs/Cesium3DTilesTester.js @@ -0,0 +1,348 @@ +/*global define*/ +define([ + 'Core/arrayFill', + 'Core/Color', + 'Core/defaultValue', + 'Core/defined', + 'Scene/Cesium3DTileContentFactory', + 'Scene/Cesium3DTileContentState', + 'Scene/Cesium3DTileset', + 'Scene/TileBoundingSphere', + 'Specs/pollToPromise' + ], function( + arrayFill, + Color, + defaultValue, + defined, + Cesium3DTileContentFactory, + Cesium3DTileContentState, + Cesium3DTileset, + TileBoundingSphere, + pollToPromise) { + 'use strict'; + + var mockTile = { + contentBoundingVolume : new TileBoundingSphere(), + _contentBoundingVolume : new TileBoundingSphere(), + _header : { + content : { + boundingVolume : { + sphere : [0.0, 0.0, 0.0, 1.0] + } + } + } + }; + + function Cesium3DTilesTester() { + } + + function padStringToByteAlignment(string, byteAlignment) { + var length = string.length; + var paddedLength = Math.ceil(length / byteAlignment) * byteAlignment; // Round up to the required alignment + var padding = paddedLength - length; + var whitespace = ''; + for (var i = 0; i < padding; ++i) { + whitespace += ' '; + } + return string + whitespace; + } + + Cesium3DTilesTester.expectRender = function(scene, tileset, callback) { + tileset.show = false; + expect(scene).toRender([0, 0, 0, 255]); + tileset.show = true; + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba).not.toEqual([0, 0, 0, 255]); + if (defined(callback)) { + callback(rgba); + } + }); + }; + + Cesium3DTilesTester.expectRenderBlank = function(scene, tileset) { + tileset.show = false; + expect(scene).toRender([0, 0, 0, 255]); + tileset.show = true; + expect(scene).toRender([0, 0, 0, 255]); + }; + + Cesium3DTilesTester.expectRenderTileset = function(scene, tileset) { + // Verify render before being picked + Cesium3DTilesTester.expectRender(scene, tileset); + + // Pick a feature + expect(scene).toPickAndCall(function(result) { + expect(result).toBeDefined(); + + // Change the color of the picked feature to yellow + result.color = Color.clone(Color.YELLOW, result.color); + + // Expect the pixel color to be some shade of yellow + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Turn show off and on + result.show = false; + Cesium3DTilesTester.expectRenderBlank(scene, tileset); + result.show = true; + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }; + + Cesium3DTilesTester.waitForTilesLoaded = function(scene, tileset) { + return pollToPromise(function() { + scene.renderForSpecs(); + return tileset.tilesLoaded; + }).then(function() { + return tileset; + }); + }; + + Cesium3DTilesTester.waitForReady = function(scene, tileset) { + return pollToPromise(function() { + scene.renderForSpecs(); + return tileset.ready; + }).then(function() { + return tileset; + }); + }; + + Cesium3DTilesTester.loadTileset = function(scene, url, options) { + options = defaultValue(options, {}); + options.url = url; + // Load all visible tiles + var tileset = scene.primitives.add(new Cesium3DTileset(options)); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset); + }; + + Cesium3DTilesTester.loadTileExpectError = function(scene, arrayBuffer, type) { + var tileset = {}; + var url = ''; + expect(function() { + return Cesium3DTileContentFactory[type](tileset, mockTile, url, arrayBuffer, 0); + }).toThrowRuntimeError(); + }; + + Cesium3DTilesTester.loadTile = function(scene, arrayBuffer, type) { + var tileset = {}; + var url = ''; + var content = Cesium3DTileContentFactory[type](tileset, mockTile, url, arrayBuffer, 0); + content.update(tileset, scene.frameState); + return content; + }; + + // Use counter to prevent models from sharing the same cache key, + // this fixes tests that load a model with the same invalid url + var counter = 0; + Cesium3DTilesTester.rejectsReadyPromiseOnError = function(scene, arrayBuffer, type) { + var tileset = { + basePath : counter++ + }; + var url = ''; + var content = Cesium3DTileContentFactory[type](tileset, mockTile, url, arrayBuffer, 0); + content.update(tileset, scene.frameState); + + return content.readyPromise.then(function(content) { + fail('should not resolve'); + }).otherwise(function(error) { + expect(error).toBeDefined(); + }); + }; + + Cesium3DTilesTester.resolvesReadyPromise = function(scene, url) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + var content = tileset._root.content; + return content.readyPromise.then(function(content) { + expect(content).toBeDefined(); + }); + }); + }; + + Cesium3DTilesTester.tileDestroys = function(scene, url) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + var content = tileset._root.content; + expect(content.isDestroyed()).toEqual(false); + scene.primitives.remove(tileset); + expect(content.isDestroyed()).toEqual(true); + }); + }; + + Cesium3DTilesTester.generateBatchedTileBuffer = function(options) { + // Procedurally generate the tile array buffer for testing purposes + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var magic = defaultValue(options.magic, [98, 51, 100, 109]); + var version = defaultValue(options.version, 1); + var featuresLength = defaultValue(options.featuresLength, 1); + var featureTableJson = { + BATCH_LENGTH : featuresLength + }; + var featureTableJsonString = JSON.stringify(featureTableJson); + var featureTableJsonByteLength = featureTableJsonString.length; + + var headerByteLength = 28; + var byteLength = headerByteLength + featureTableJsonByteLength; + var buffer = new ArrayBuffer(byteLength); + var view = new DataView(buffer); + view.setUint8(0, magic[0]); + view.setUint8(1, magic[1]); + view.setUint8(2, magic[2]); + view.setUint8(3, magic[3]); + view.setUint32(4, version, true); // version + view.setUint32(8, byteLength, true); // byteLength + view.setUint32(12, featureTableJsonByteLength, true); // featureTableJsonByteLength + view.setUint32(16, 0, true); // featureTableBinaryByteLength + view.setUint32(20, 0, true); // batchTableJsonByteLength + view.setUint32(24, 0, true); // batchTableBinaryByteLength + + var i; + var byteOffset = headerByteLength; + for (i = 0; i < featureTableJsonByteLength; i++) { + view.setUint8(byteOffset, featureTableJsonString.charCodeAt(i)); + byteOffset++; + } + + return buffer; + }; + + Cesium3DTilesTester.generateInstancedTileBuffer = function(options) { + // Procedurally generate the tile array buffer for testing purposes + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var magic = defaultValue(options.magic, [105, 51, 100, 109]); + var version = defaultValue(options.version, 1); + + var gltfFormat = defaultValue(options.gltfFormat, 1); + var gltfUri = defaultValue(options.gltfUri, ''); + var gltfUriByteLength = gltfUri.length; + + var featuresLength = defaultValue(options.featuresLength, 1); + var featureTableJson = { + INSTANCES_LENGTH : featuresLength, + POSITION : arrayFill(new Array(featuresLength * 3), 0) + }; + var featureTableJsonString = JSON.stringify(featureTableJson); + var featureTableJsonByteLength = featureTableJsonString.length; + + var headerByteLength = 32; + var uriByteLength = gltfUri.length; + var byteLength = headerByteLength + featureTableJsonByteLength + uriByteLength; + var buffer = new ArrayBuffer(byteLength); + var view = new DataView(buffer); + view.setUint8(0, magic[0]); + view.setUint8(1, magic[1]); + view.setUint8(2, magic[2]); + view.setUint8(3, magic[3]); + view.setUint32(4, version, true); // version + view.setUint32(8, byteLength, true); // byteLength + view.setUint32(12, featureTableJsonByteLength, true); // featureTableJsonByteLength + view.setUint32(16, 0, true); // featureTableBinaryByteLength + view.setUint32(20, 0, true); // batchTableJsonByteLength + view.setUint32(24, 0, true); // batchTableBinaryByteLength + view.setUint32(28, gltfFormat, true); // gltfFormat + + var i; + var byteOffset = headerByteLength; + for (i = 0; i < featureTableJsonByteLength; i++) { + view.setUint8(byteOffset, featureTableJsonString.charCodeAt(i)); + byteOffset++; + } + for (i = 0; i < gltfUriByteLength; i++) { + view.setUint8(byteOffset, gltfUri.charCodeAt(i)); + byteOffset++; + } + return buffer; + }; + + Cesium3DTilesTester.generatePointCloudTileBuffer = function(options) { + // Procedurally generate the tile array buffer for testing purposes + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var magic = defaultValue(options.magic, [112, 110, 116, 115]); + var version = defaultValue(options.version, 1); + var featureTableJson = options.featureTableJson; + if (!defined(featureTableJson)) { + featureTableJson = { + POINTS_LENGTH : 1, + POSITIONS : { + byteOffset : 0 + } + }; + } + + var featureTableJsonString = JSON.stringify(featureTableJson); + featureTableJsonString = padStringToByteAlignment(featureTableJsonString, 4); + var featureTableJsonByteLength = defaultValue(options.featureTableJsonByteLength, featureTableJsonString.length); + + var featureTableBinary = new ArrayBuffer(12); // Enough space to hold 3 floats + var featureTableBinaryByteLength = featureTableBinary.byteLength; + + var headerByteLength = 28; + var byteLength = headerByteLength + featureTableJsonByteLength + featureTableBinaryByteLength; + var buffer = new ArrayBuffer(byteLength); + var view = new DataView(buffer); + view.setUint8(0, magic[0]); + view.setUint8(1, magic[1]); + view.setUint8(2, magic[2]); + view.setUint8(3, magic[3]); + view.setUint32(4, version, true); // version + view.setUint32(8, byteLength, true); // byteLength + view.setUint32(12, featureTableJsonByteLength, true); // featureTableJsonByteLength + view.setUint32(16, featureTableBinaryByteLength, true); // featureTableBinaryByteLength + view.setUint32(20, 0, true); // batchTableJsonByteLength + view.setUint32(24, 0, true); // batchTableBinaryByteLength + + var i; + var byteOffset = headerByteLength; + for (i = 0; i < featureTableJsonByteLength; i++) { + view.setUint8(byteOffset, featureTableJsonString.charCodeAt(i)); + byteOffset++; + } + for (i = 0; i < featureTableBinaryByteLength; i++) { + view.setUint8(byteOffset, featureTableBinary[i]); + byteOffset++; + } + return buffer; + }; + + Cesium3DTilesTester.generateCompositeTileBuffer = function(options) { + // Procedurally generate the tile array buffer for testing purposes + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var magic = defaultValue(options.magic, [99, 109, 112, 116]); + var version = defaultValue(options.version, 1); + var tiles = defaultValue(options.tiles, []); + var tilesLength = tiles.length; + + var i; + var tilesByteLength = 0; + for (i = 0; i < tilesLength; ++i) { + tilesByteLength += tiles[i].byteLength; + } + + var headerByteLength = 16; + var byteLength = headerByteLength + tilesByteLength; + var buffer = new ArrayBuffer(byteLength); + var uint8Array = new Uint8Array(buffer); + var view = new DataView(buffer); + view.setUint8(0, magic[0]); + view.setUint8(1, magic[1]); + view.setUint8(2, magic[2]); + view.setUint8(3, magic[3]); + view.setUint32(4, version, true); // version + view.setUint32(8, byteLength, true); // byteLength + view.setUint32(12, tilesLength, true); // tilesLength + + var byteOffset = headerByteLength; + for (i = 0; i < tilesLength; ++i) { + var tile = new Uint8Array(tiles[i]); + uint8Array.set(tile, byteOffset); + byteOffset += tile.byteLength; + } + + return buffer; + }; + + return Cesium3DTilesTester; +}); diff --git a/Specs/Core/CesiumTerrainProviderSpec.js b/Specs/Core/CesiumTerrainProviderSpec.js index c15e00977923..04cb24bff555 100644 --- a/Specs/Core/CesiumTerrainProviderSpec.js +++ b/Specs/Core/CesiumTerrainProviderSpec.js @@ -417,7 +417,6 @@ defineSuite([ }); }); - it('uses the proxy if one is supplied', function() { var baseUrl = 'made/up/url'; diff --git a/Specs/Core/DoublyLinkedListSpec.js b/Specs/Core/DoublyLinkedListSpec.js new file mode 100644 index 000000000000..7ee82223d8b5 --- /dev/null +++ b/Specs/Core/DoublyLinkedListSpec.js @@ -0,0 +1,368 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/DoublyLinkedList' + ], function( + DoublyLinkedList) { + 'use strict'; + + it('constructs', function() { + var list = new DoublyLinkedList(); + expect(list.head).not.toBeDefined(); + expect(list.tail).not.toBeDefined(); + expect(list.length).toEqual(0); + }); + + it('adds items', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + + // node + // ^ ^ + // | | + // head tail + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node); + expect(list.length).toEqual(1); + + expect(node).toBeDefined(); + expect(node.item).toEqual(1); + expect(node.previous).not.toBeDefined(); + expect(node.next).not.toBeDefined(); + + var node2 = list.add(2); + + // node <-> node2 + // ^ ^ + // | | + // head tail + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node2); + expect(list.length).toEqual(2); + + expect(node2).toBeDefined(); + expect(node2.item).toEqual(2); + expect(node2.previous).toEqual(node); + expect(node2.next).not.toBeDefined(); + + expect(node.next).toEqual(node2); + + var node3 = list.add(3); + + // node <-> node2 <-> node3 + // ^ ^ + // | | + // head tail + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node3); + expect(list.length).toEqual(3); + + expect(node3).toBeDefined(); + expect(node3.item).toEqual(3); + expect(node3.previous).toEqual(node2); + expect(node3.next).not.toBeDefined(); + + expect(node2.next).toEqual(node3); + }); + + it('removes from a list with one item', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + + list.remove(node); + + expect(list.head).not.toBeDefined(); + expect(list.tail).not.toBeDefined(); + expect(list.length).toEqual(0); + }); + + it('removes head of list', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + + list.remove(node); + + expect(list.head).toEqual(node2); + expect(list.tail).toEqual(node2); + expect(list.length).toEqual(1); + }); + + it('removes tail of list', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + + list.remove(node2); + + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node); + expect(list.length).toEqual(1); + }); + + it('removes middle of list', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + + list.remove(node2); + + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node3); + expect(list.length).toEqual(2); + }); + + it('removes nothing', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + + list.remove(undefined); + + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node); + expect(list.length).toEqual(1); + }); + + function expectOrder(list, nodes) { + // Assumes at least one node is in the list + var length = nodes.length; + + expect(list.length).toEqual(length); + + // Verify head and tail pointers + expect(list.head).toEqual(nodes[0]); + expect(list.tail).toEqual(nodes[length - 1]); + + // Verify that linked list has nodes in the expected order + var node = list.head; + for (var i = 0; i < length; ++i) { + var nextNode = (i === length - 1) ? undefined : nodes[i + 1]; + var previousNode = (i === 0) ? undefined : nodes[i - 1]; + + expect(node).toEqual(nodes[i]); + expect(node.next).toEqual(nextNode); + expect(node.previous).toEqual(previousNode); + + node = node.next; + } + } + + it('splices nextNode before node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + var node5 = list.add(5); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 <-> node5 + // ^ ^ ^ ^ + // | | | | + // head nextNode node tail + + // After: + // + // node <-> node3 <-> node4 <-> node2 <-> node5 + // ^ ^ + // | | + // head tail + + // Move node2 after node4 + list.splice(node4, node2); + expectOrder(list, [node, node3, node4, node2, node5]); + }); + + it('splices nextNode after node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + var node5 = list.add(5); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 <-> node5 + // ^ ^ ^ ^ + // | | | | + // head node nextNode tail + + // After: + // + // node <-> node2 <-> node4 <-> node3 <-> node5 + // ^ ^ + // | | + // head tail + + // Move node4 after node2 + list.splice(node2, node4); + expectOrder(list, [node, node2, node4, node3, node5]); + }); + + it('splices nextNode immediately before node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ ^ + // | | | | + // head nextNode node tail + + // After: + // + // node <-> node3 <-> node2 <-> node4 + // ^ ^ + // | | + // head tail + + // Move node2 after node4 + list.splice(node3, node2); + expectOrder(list, [node, node3, node2, node4]); + }); + + it('splices nextNode immediately after node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ ^ + // | | | | + // head node nextNode tail + + // After: does not change + + list.splice(node2, node3); + expectOrder(list, [node, node2, node3, node4]); + }); + + it('splices node === nextNode', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + + // Before: + // + // node <-> node2 <-> node3 + // ^ ^ ^ + // | | | + // head node/nextNode tail + + // After: does not change + + list.splice(node2, node2); + expectOrder(list, [node, node2, node3]); + }); + + it('splices when nextNode was tail', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head node tail/nextNode + + // After: + // + // node <-> node2 <-> node4 <-> node3 + // ^ ^ + // | | + // head tail + + list.splice(node2, node4); + expectOrder(list, [node, node2, node4, node3]); + }); + + it('splices when node was tail', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head nextNode tail/node + + // After: + // + // node <-> node3 <-> node4 <-> node2 + // ^ ^ + // | | + // head tail/node + + list.splice(node4, node2); + expectOrder(list, [node, node3, node4, node2]); + }); + + it('splices when nextNode was head', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head/nextNode node tail + + // After: + // + // node2 <-> node3 <-> node <-> node4 + // ^ ^ + // | | + // head tail + + list.splice(node3, node); + expectOrder(list, [node2, node3, node, node4]); + }); + + it('splices when node was head', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head/node nextNode tail + + // After: + // + // node <-> node3 <-> node2 <-> node4 + // ^ ^ + // | | + // head tail + + list.splice(node, node3); + expectOrder(list, [node, node3, node2, node4]); + }); +}); diff --git a/Specs/Core/ManagedArraySpec.js b/Specs/Core/ManagedArraySpec.js new file mode 100644 index 000000000000..67d138fa540a --- /dev/null +++ b/Specs/Core/ManagedArraySpec.js @@ -0,0 +1,141 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/ManagedArray' +], function( + ManagedArray) { + 'use strict'; + + it('constructor has expected default values', function() { + var array = new ManagedArray(); + expect(array.length).toEqual(0); + }); + + it('constructor initializes length', function() { + var array = new ManagedArray(10); + expect(array.length).toEqual(10); + expect(array.values.length).toEqual(10); + }); + + it('can get and set values', function() { + var length = 10; + var array = new ManagedArray(length); + var i; + for (i = 0; i < length; ++i) { + array.set(i, i*i); + } + for (i = 0; i < length; ++i) { + expect(array.get(i)).toEqual(i*i); + expect(array.values[i]).toEqual(i*i); + } + }); + + it('get throws if index does not exist', function() { + var array = new ManagedArray(); + array.reserve(5); + expect(array.values.length).toEqual(5); + expect(function() { + array.get(5); + }).toThrowDeveloperError(); + }); + + it('set throws if index invalid', function() { + var array = new ManagedArray(); + array.resize(10); + expect(function() { + array.set(undefined, 5); + }).toThrowDeveloperError(); + }); + + it('set resizes array', function() { + var array = new ManagedArray(); + array.set(0, 'a'); + expect(array.length).toEqual(1); + array.set(5, 'b'); + expect(array.length).toEqual(6); + array.set(2, 'c'); + expect(array.length).toEqual(6); + }); + + it('can push values', function() { + var array = new ManagedArray(); + var length = 10; + for (var i = 0; i < length; ++i) { + var val = Math.random(); + array.push(val); + expect(array.length).toEqual(i+1); + expect(array.values.length).toEqual(i+1); + expect(array.get(i)).toEqual(val); + expect(array.values[i]).toEqual(val); + } + }); + + it('can pop values', function() { + var length = 10; + var array = new ManagedArray(length); + var i; + for (i = 0; i < length; ++i) { + array.set(i, Math.random()); + } + for (i = length - 1; i >= 0; --i) { + var val = array.get(i); + expect(array.pop()).toEqual(val); + expect(array.length).toEqual(i); + expect(array.values.length).toEqual(length); + } + }); + + it('reserve throws if length is less than 0', function() { + var array = new ManagedArray(); + expect(function() { + array.reserve(-1); + }).toThrowDeveloperError(); + }); + + it('reserve', function() { + var array = new ManagedArray(2); + array.reserve(10); + expect(array.values.length).toEqual(10); + expect(array.length).toEqual(2); + array.reserve(20); + expect(array.values.length).toEqual(20); + expect(array.length).toEqual(2); + array.reserve(5); + expect(array.values.length).toEqual(20); + expect(array.length).toEqual(2); + }); + + it('resize throws if length is less than 0', function() { + var array = new ManagedArray(); + expect(function() { + array.resize(-1); + }).toThrowDeveloperError(); + }); + + it('resize', function() { + var array = new ManagedArray(2); + array.resize(10); + expect(array.values.length).toEqual(10); + expect(array.length).toEqual(10); + array.resize(20); + expect(array.values.length).toEqual(20); + expect(array.length).toEqual(20); + array.resize(5); + expect(array.values.length).toEqual(20); + expect(array.length).toEqual(5); + }); + + it('trim', function() { + var array = new ManagedArray(2); + array.reserve(10); + expect(array.length).toEqual(2); + expect(array.values.length).toEqual(10); + array.trim(); + expect(array.values.length).toEqual(2); + array.trim(5); + expect(array.length).toEqual(2); + expect(array.values.length).toEqual(5); + array.trim(3); + expect(array.length).toEqual(2); + expect(array.values.length).toEqual(3); + }); +}); diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedColors/batchedColors.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedColors/batchedColors.b3dm new file mode 100644 index 000000000000..55c6d3bd55b2 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedColors/batchedColors.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedColors/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedColors/tileset.json new file mode 100644 index 000000000000..44733c7f654c --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedColors/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196683129064435 + }, + "Latitude": { + "minimum": 0.698861998722264, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.929546581581235, + "maximum": 13.581844886764884 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedColors.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedColorsMix/batchedColorsMix.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsMix/batchedColorsMix.b3dm new file mode 100644 index 000000000000..cb47684b88d2 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsMix/batchedColorsMix.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedColorsMix/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsMix/tileset.json new file mode 100644 index 000000000000..6864219c8e59 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsMix/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196683129064435 + }, + "Latitude": { + "minimum": 0.698861998722264, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.929546581581235, + "maximum": 13.581844886764884 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedColorsMix.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedColorsTranslucent/batchedColorsTranslucent.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsTranslucent/batchedColorsTranslucent.b3dm new file mode 100644 index 000000000000..4a671a9c4ebe Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsTranslucent/batchedColorsTranslucent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedColorsTranslucent/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsTranslucent/tileset.json new file mode 100644 index 000000000000..7346380cc8a8 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedColorsTranslucent/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196683129064435 + }, + "Latitude": { + "minimum": 0.698861998722264, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.929546581581235, + "maximum": 13.581844886764884 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedColorsTranslucent.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedCompressedTextures/batchedCompressedTextures.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedCompressedTextures/batchedCompressedTextures.b3dm new file mode 100644 index 000000000000..0957d8b497ca Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedCompressedTextures/batchedCompressedTextures.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedCompressedTextures/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedCompressedTextures/tileset.json new file mode 100644 index 000000000000..b7b37b18bacd --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedCompressedTextures/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedCompressedTextures.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated1/batchedDeprecated1.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated1/batchedDeprecated1.b3dm new file mode 100644 index 000000000000..35083a6eb6df Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated1/batchedDeprecated1.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated1/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated1/tileset.json new file mode 100644 index 000000000000..d40f3a9aa03b --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated1/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedDeprecated1.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated2/batchedDeprecated2.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated2/batchedDeprecated2.b3dm new file mode 100644 index 000000000000..c47a552d8581 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated2/batchedDeprecated2.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated2/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated2/tileset.json new file mode 100644 index 000000000000..e104f9661a07 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedDeprecated2/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedDeprecated2.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/batchedExpiration.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/batchedExpiration.b3dm new file mode 100644 index 000000000000..b3a7e3cb36ce Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/batchedExpiration.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/tileset.json new file mode 100644 index 000000000000..9682054f143d --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedExpiration/tileset.json @@ -0,0 +1,44 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "expire": { + "duration": 5 + }, + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedExpiration.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedGltfZUp/batchedGltfZUp.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedGltfZUp/batchedGltfZUp.b3dm new file mode 100644 index 000000000000..88114c3d080b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedGltfZUp/batchedGltfZUp.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedGltfZUp/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedGltfZUp/tileset.json new file mode 100644 index 000000000000..9d366cf2e0bb --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedGltfZUp/tileset.json @@ -0,0 +1,42 @@ +{ + "asset": { + "version": "0.0", + "gltfUpAxis": "Z" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedGltfZUp.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedNoBatchIds/batchedNoBatchIds.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedNoBatchIds/batchedNoBatchIds.b3dm new file mode 100644 index 000000000000..b55063113359 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedNoBatchIds/batchedNoBatchIds.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedNoBatchIds/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedNoBatchIds/tileset.json new file mode 100644 index 000000000000..e782cc06759e --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedNoBatchIds/tileset.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedNoBatchIds.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedTextured/batchedTextured.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedTextured/batchedTextured.b3dm new file mode 100644 index 000000000000..d5636e9602d7 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedTextured/batchedTextured.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedTextured/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedTextured/tileset.json new file mode 100644 index 000000000000..e1377db0ef66 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedTextured/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedTextured.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucent/batchedTranslucent.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucent/batchedTranslucent.b3dm new file mode 100644 index 000000000000..eb572f2bad4d Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucent/batchedTranslucent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucent/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucent/tileset.json new file mode 100644 index 000000000000..4909113dcd85 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucent/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedTranslucent.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/batchedTranslucentOpaqueMix.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/batchedTranslucentOpaqueMix.b3dm new file mode 100644 index 000000000000..70bb4eedc755 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/batchedTranslucentOpaqueMix.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/tileset.json new file mode 100644 index 000000000000..dd19decf470e --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedTranslucentOpaqueMix.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWGS84/batchedWGS84.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWGS84/batchedWGS84.b3dm new file mode 100644 index 000000000000..0398cb823b4d Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWGS84/batchedWGS84.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWGS84/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWGS84/tileset.json new file mode 100644 index 000000000000..8a2fb4674e7c --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWGS84/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWGS84.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTable/batchedWithBatchTable.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTable/batchedWithBatchTable.b3dm new file mode 100644 index 000000000000..2d2bc50db10d Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTable/batchedWithBatchTable.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTable/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTable/tileset.json new file mode 100644 index 000000000000..9d27e6783e42 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTable/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithBatchTable.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTableBinary/batchedWithBatchTableBinary.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTableBinary/batchedWithBatchTableBinary.b3dm new file mode 100644 index 000000000000..f2c6d4051eb6 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTableBinary/batchedWithBatchTableBinary.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTableBinary/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTableBinary/tileset.json new file mode 100644 index 000000000000..d0b76fc4bb40 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBatchTableBinary/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithBatchTableBinary.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/batchedWithBoundingSphere.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/batchedWithBoundingSphere.b3dm new file mode 100644 index 000000000000..dc8a294dda64 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/batchedWithBoundingSphere.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/tileset.json new file mode 100644 index 000000000000..6e095bb737b2 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/tileset.json @@ -0,0 +1,39 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215011.9317263428, + -4736309.3434217675, + 4081612.0044800863, + 141.4214 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithBoundingSphere.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithKHRMaterialsCommon/batchedWithKHRMaterialsCommon.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithKHRMaterialsCommon/batchedWithKHRMaterialsCommon.b3dm new file mode 100644 index 000000000000..cca0b3919c67 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithKHRMaterialsCommon/batchedWithKHRMaterialsCommon.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithKHRMaterialsCommon/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithKHRMaterialsCommon/tileset.json new file mode 100644 index 000000000000..28e0ad85e8e9 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithKHRMaterialsCommon/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithKHRMaterialsCommon.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithQuantization/batchedWithQuantization.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithQuantization/batchedWithQuantization.b3dm new file mode 100644 index 000000000000..0df33684eabc Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithQuantization/batchedWithQuantization.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithQuantization/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithQuantization/tileset.json new file mode 100644 index 000000000000..7e55f3ab9ece --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithQuantization/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithQuantization.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformBox/batchedWithTransformBox.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformBox/batchedWithTransformBox.b3dm new file mode 100644 index 000000000000..01b0e171c5c9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformBox/batchedWithTransformBox.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformBox/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformBox/tileset.json new file mode 100644 index 000000000000..ee1b0dcc8a70 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformBox/tileset.json @@ -0,0 +1,65 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "add", + "boundingVolume": { + "box": [ + 0, + 0, + 10, + 100, + 0, + 0, + 0, + 100, + 0, + 0, + 0, + 10 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithTransformBox.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/batchedWithTransformRegion.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/batchedWithTransformRegion.b3dm new file mode 100644 index 000000000000..01b0e171c5c9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/batchedWithTransformRegion.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/tileset.json new file mode 100644 index 000000000000..5d4f361d7c03 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/tileset.json @@ -0,0 +1,59 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithTransformRegion.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/batchedWithTransformSphere.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/batchedWithTransformSphere.b3dm new file mode 100644 index 000000000000..01b0e171c5c9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/batchedWithTransformSphere.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/tileset.json new file mode 100644 index 000000000000..d98b14ec7368 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/tileset.json @@ -0,0 +1,57 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 12.83180232718587 + } + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "add", + "boundingVolume": { + "sphere": [ + 0, + 0, + 10, + 141.4214 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithTransformSphere.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/batchedWithoutBatchTable.b3dm b/Specs/Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/batchedWithoutBatchTable.b3dm new file mode 100644 index 000000000000..115b4f07e164 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/batchedWithoutBatchTable.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/tileset.json b/Specs/Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/tileset.json new file mode 100644 index 000000000000..510bf187f33e --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/tileset.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "batchedWithoutBatchTable.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Composite/Composite/composite.cmpt b/Specs/Data/Cesium3DTiles/Composite/Composite/composite.cmpt new file mode 100644 index 000000000000..acd029a236af Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Composite/Composite/composite.cmpt differ diff --git a/Specs/Data/Cesium3DTiles/Composite/Composite/tileset.json b/Specs/Data/Cesium3DTiles/Composite/Composite/tileset.json new file mode 100644 index 000000000000..eb6aa226c12a --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Composite/Composite/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "composite.cmpt" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Composite/CompositeOfComposite/compositeOfComposite.cmpt b/Specs/Data/Cesium3DTiles/Composite/CompositeOfComposite/compositeOfComposite.cmpt new file mode 100644 index 000000000000..2ae795d67ebf Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Composite/CompositeOfComposite/compositeOfComposite.cmpt differ diff --git a/Specs/Data/Cesium3DTiles/Composite/CompositeOfComposite/tileset.json b/Specs/Data/Cesium3DTiles/Composite/CompositeOfComposite/tileset.json new file mode 100644 index 000000000000..d5465d72d802 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Composite/CompositeOfComposite/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3196972173766555, + "maximum": -1.3196718547473905 + }, + "Latitude": { + "minimum": 0.6988624606923348, + "maximum": 0.6988888301460953 + }, + "Height": { + "minimum": 6.2074098233133554, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "compositeOfComposite.cmpt" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tile.b3dm b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tile.b3dm new file mode 100644 index 000000000000..1f7b54d4f418 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tile.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json new file mode 100644 index 000000000000..ed31f390fa80 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json @@ -0,0 +1,47 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "add", + "boundingVolume": { + "box": [ + 0, + 0, + 10, + 50, + 0, + 0, + 0, + 50, + 0, + 0, + 0, + 10 + ] + }, + "geometricError": 0, + "content": { + "url": "tile.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/tile.b3dm b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/tile.b3dm new file mode 100644 index 000000000000..a58039a483bf Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/tile.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/tileset.json b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/tileset.json new file mode 100644 index 000000000000..ed31f390fa80 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/tileset.json @@ -0,0 +1,47 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "add", + "boundingVolume": { + "box": [ + 0, + 0, + 10, + 50, + 0, + 0, + 0, + 50, + 0, + 0, + 0, + 10 + ] + }, + "geometricError": 0, + "content": { + "url": "tile.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/tile.b3dm b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/tile.b3dm new file mode 100644 index 000000000000..983d0e56dee0 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/tile.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/tileset.json b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/tileset.json new file mode 100644 index 000000000000..ed31f390fa80 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/tileset.json @@ -0,0 +1,47 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "add", + "boundingVolume": { + "box": [ + 0, + 0, + 10, + 50, + 0, + 0, + 0, + 50, + 0, + 0, + 0, + 10 + ] + }, + "geometricError": 0, + "content": { + "url": "tile.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/tile.b3dm b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/tile.b3dm new file mode 100644 index 000000000000..362a919adabd Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/tile.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/tileset.json b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/tileset.json new file mode 100644 index 000000000000..ed31f390fa80 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/tileset.json @@ -0,0 +1,47 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "refine": "add", + "boundingVolume": { + "box": [ + 0, + 0, + 10, + 50, + 0, + 0, + 0, + 50, + 0, + 0, + 0, + 10 + ] + }, + "geometricError": 0, + "content": { + "url": "tile.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedCompressedTextures/instancedCompressedTextures.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedCompressedTextures/instancedCompressedTextures.i3dm new file mode 100644 index 000000000000..7e1251532041 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedCompressedTextures/instancedCompressedTextures.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedCompressedTextures/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedCompressedTextures/tileset.json new file mode 100644 index 000000000000..090c238793f5 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedCompressedTextures/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedCompressedTextures.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/Box.glb b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/Box.glb new file mode 100644 index 000000000000..2bde3c4ee98e Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/Box.glb differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/instancedGltfExternal.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/instancedGltfExternal.i3dm new file mode 100644 index 000000000000..afcfc8739afa Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/instancedGltfExternal.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/tileset.json new file mode 100644 index 000000000000..f54857135d9a --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedGltfExternal.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfZUp/instancedGltfZUp.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfZUp/instancedGltfZUp.i3dm new file mode 100644 index 000000000000..c7d534542685 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfZUp/instancedGltfZUp.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfZUp/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfZUp/tileset.json new file mode 100644 index 000000000000..a60d5e450505 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfZUp/tileset.json @@ -0,0 +1,30 @@ +{ + "asset": { + "version": "0.0", + "gltfUpAxis": "Z" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedGltfZUp.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedOct32POrientation/instancedOct32POrientation.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedOct32POrientation/instancedOct32POrientation.i3dm new file mode 100644 index 000000000000..87e5ee55f518 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedOct32POrientation/instancedOct32POrientation.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedOct32POrientation/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedOct32POrientation/tileset.json new file mode 100644 index 000000000000..c52f33490b81 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedOct32POrientation/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedOct32POrientation.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedOrientation/instancedOrientation.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedOrientation/instancedOrientation.i3dm new file mode 100644 index 000000000000..fc780b4cf18e Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedOrientation/instancedOrientation.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedOrientation/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedOrientation/tileset.json new file mode 100644 index 000000000000..29e3cb2a488e --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedOrientation/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedOrientation.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantized/instancedQuantized.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantized/instancedQuantized.i3dm new file mode 100644 index 000000000000..3d64093f7aae Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantized/instancedQuantized.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantized/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantized/tileset.json new file mode 100644 index 000000000000..0a1d5acc31e6 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantized/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedQuantized.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantizedOct32POrientation/instancedQuantizedOct32POrientation.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantizedOct32POrientation/instancedQuantizedOct32POrientation.i3dm new file mode 100644 index 000000000000..2e47d0e0455e Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantizedOct32POrientation/instancedQuantizedOct32POrientation.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantizedOct32POrientation/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantizedOct32POrientation/tileset.json new file mode 100644 index 000000000000..c41b07b335f2 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedQuantizedOct32POrientation/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedQuantizedOct32POrientation.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedRTC/instancedRTC.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedRTC/instancedRTC.i3dm new file mode 100644 index 000000000000..6f5b6b99807d Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedRTC/instancedRTC.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedRTC/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedRTC/tileset.json new file mode 100644 index 000000000000..9c5d59deb4ce --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedRTC/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedRTC.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedRedMaterial/instancedRedMaterial.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedRedMaterial/instancedRedMaterial.i3dm new file mode 100644 index 000000000000..78f082bf121e Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedRedMaterial/instancedRedMaterial.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedRedMaterial/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedRedMaterial/tileset.json new file mode 100644 index 000000000000..1543d7c05bc2 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedRedMaterial/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedRedMaterial.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedScale/instancedScale.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedScale/instancedScale.i3dm new file mode 100644 index 000000000000..22c2e0bc0223 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedScale/instancedScale.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedScale/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedScale/tileset.json new file mode 100644 index 000000000000..611188681761 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedScale/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedScale.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedScaleNonUniform/instancedScaleNonUniform.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedScaleNonUniform/instancedScaleNonUniform.i3dm new file mode 100644 index 000000000000..1f48b103a413 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedScaleNonUniform/instancedScaleNonUniform.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedScaleNonUniform/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedScaleNonUniform/tileset.json new file mode 100644 index 000000000000..03f2136ba036 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedScaleNonUniform/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedScaleNonUniform.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedTextured/instancedTextured.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedTextured/instancedTextured.i3dm new file mode 100644 index 000000000000..e62725639bdf Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedTextured/instancedTextured.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedTextured/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedTextured/tileset.json new file mode 100644 index 000000000000..ae42528eb3b5 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedTextured/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedTextured.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchIds/instancedWithBatchIds.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchIds/instancedWithBatchIds.i3dm new file mode 100644 index 000000000000..7cd6274ff15f Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchIds/instancedWithBatchIds.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchIds/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchIds/tileset.json new file mode 100644 index 000000000000..5214b77061f9 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchIds/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedWithBatchIds.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/instancedWithBatchTable.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/instancedWithBatchTable.i3dm new file mode 100644 index 000000000000..4a641e7a8a64 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/instancedWithBatchTable.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/tileset.json new file mode 100644 index 000000000000..51047647dad5 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedWithBatchTable.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTableBinary/instancedWithBatchTableBinary.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTableBinary/instancedWithBatchTableBinary.i3dm new file mode 100644 index 000000000000..4a641e7a8a64 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTableBinary/instancedWithBatchTableBinary.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTableBinary/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTableBinary/tileset.json new file mode 100644 index 000000000000..77f564e44e67 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithBatchTableBinary/tileset.json @@ -0,0 +1,29 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedWithBatchTableBinary.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithTransform/instancedWithTransform.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithTransform/instancedWithTransform.i3dm new file mode 100644 index 000000000000..8543f9d265fc Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithTransform/instancedWithTransform.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithTransform/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithTransform/tileset.json new file mode 100644 index 000000000000..9c550fbc3e82 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithTransform/tileset.json @@ -0,0 +1,53 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "Height": { + "minimum": 20, + "maximum": 20 + } + }, + "geometricError": 70, + "root": { + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.1598646084383285, + 0.6231776156741929, + 0.7655670880409422, + 0, + 0.19023226536042928, + -0.7415555619825982, + 0.6433560707015301, + 0, + 1215013.8340490046, + -4736316.75897742, + 4081608.4380407534, + 1 + ], + "refine": "add", + "boundingVolume": { + "box": [ + 0, + 0, + 0, + 100, + 0, + 0, + 0, + 100, + 0, + 0, + 0, + 15 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedWithTransform.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithoutBatchTable/instancedWithoutBatchTable.i3dm b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithoutBatchTable/instancedWithoutBatchTable.i3dm new file mode 100644 index 000000000000..77b74aab6c31 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithoutBatchTable/instancedWithoutBatchTable.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedWithoutBatchTable/tileset.json b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithoutBatchTable/tileset.json new file mode 100644 index 000000000000..23eae57d525b --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Instanced/InstancedWithoutBatchTable/tileset.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "refine": "add", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 30 + ] + }, + "geometricError": 0, + "content": { + "url": "instancedWithoutBatchTable.i3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudBatched/pointCloudBatched.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudBatched/pointCloudBatched.pnts new file mode 100644 index 000000000000..9f37709b1616 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudBatched/pointCloudBatched.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudBatched/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudBatched/tileset.json new file mode 100644 index 000000000000..7da76d80e07d --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudBatched/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudBatched.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/pointCloudConstantColor.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/pointCloudConstantColor.pnts new file mode 100644 index 000000000000..4f095df7e9da Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/pointCloudConstantColor.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/tileset.json new file mode 100644 index 000000000000..86f287b67821 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudConstantColor.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNoColor/pointCloudNoColor.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNoColor/pointCloudNoColor.pnts new file mode 100644 index 000000000000..53a38bcc6291 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNoColor/pointCloudNoColor.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNoColor/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNoColor/tileset.json new file mode 100644 index 000000000000..1ba137876a77 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNoColor/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudNoColor.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormals/pointCloudNormals.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormals/pointCloudNormals.pnts new file mode 100644 index 000000000000..af2e25ae9c71 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormals/pointCloudNormals.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormals/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormals/tileset.json new file mode 100644 index 000000000000..cbd112148fe4 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormals/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudNormals.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/pointCloudNormalsOctEncoded.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/pointCloudNormalsOctEncoded.pnts new file mode 100644 index 000000000000..f11dbc159f44 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/pointCloudNormalsOctEncoded.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/tileset.json new file mode 100644 index 000000000000..dda4b23c12b1 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudNormalsOctEncoded.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantized/pointCloudQuantized.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantized/pointCloudQuantized.pnts new file mode 100644 index 000000000000..a0494c1b9717 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantized/pointCloudQuantized.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantized/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantized/tileset.json new file mode 100644 index 000000000000..fd46571f8aec --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantized/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudQuantized.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/pointCloudQuantizedOctEncoded.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/pointCloudQuantizedOctEncoded.pnts new file mode 100644 index 000000000000..01566ef5c565 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/pointCloudQuantizedOctEncoded.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/tileset.json new file mode 100644 index 000000000000..21a1a017e1ed --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudQuantizedOctEncoded.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB/pointCloudRGB.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB/pointCloudRGB.pnts new file mode 100644 index 000000000000..455d3b84065b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB/pointCloudRGB.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB/tileset.json new file mode 100644 index 000000000000..166b270b98be --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudRGB.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB565/pointCloudRGB565.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB565/pointCloudRGB565.pnts new file mode 100644 index 000000000000..64e59eed46c2 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB565/pointCloudRGB565.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB565/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB565/tileset.json new file mode 100644 index 000000000000..e5862c523b4f --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGB565/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudRGB565.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGBA/pointCloudRGBA.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGBA/pointCloudRGBA.pnts new file mode 100644 index 000000000000..ac30c6af8c75 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGBA/pointCloudRGBA.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGBA/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGBA/tileset.json new file mode 100644 index 000000000000..8aca46b846aa --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudRGBA/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudRGBA.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWGS84/pointCloudWGS84.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWGS84/pointCloudWGS84.pnts new file mode 100644 index 000000000000..db140097ba4c Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWGS84/pointCloudWGS84.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWGS84/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWGS84/tileset.json new file mode 100644 index 000000000000..e07802ddb35c --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWGS84/tileset.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "refine": "add", + "boundingVolume": { + "sphere": [ + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudWGS84.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/pointCloudWithPerPointProperties.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/pointCloudWithPerPointProperties.pnts new file mode 100644 index 000000000000..3b30ca452503 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/pointCloudWithPerPointProperties.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/tileset.json new file mode 100644 index 000000000000..903f7739add9 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/tileset.json @@ -0,0 +1,39 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "transform": [ + 0.968635634376879, + 0.24848542777253735, + 0, + 0, + -0.15986460794399626, + 0.6231776137472074, + 0.7655670897127491, + 0, + 0.190232265775849, + -0.7415555636019701, + 0.6433560687121489, + 0, + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 1 + ], + "refine": "add", + "boundingVolume": { + "sphere": [ + 0, + 0, + 0, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudWithPerPointProperties.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithTransform/pointCloudWithTransform.pnts b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithTransform/pointCloudWithTransform.pnts new file mode 100644 index 000000000000..ae4664c63feb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithTransform/pointCloudWithTransform.pnts differ diff --git a/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithTransform/tileset.json b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithTransform/tileset.json new file mode 100644 index 000000000000..16d86434b967 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/PointCloud/PointCloudWithTransform/tileset.json @@ -0,0 +1,39 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 17.32, + "root": { + "transform": [ + 0.968635634376879, + 0.24848542777253735, + 0, + 0, + -0.15986460794399626, + 0.6231776137472074, + 0.7655670897127491, + 0, + 0.190232265775849, + -0.7415555636019701, + 0.6433560687121489, + 0, + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 1 + ], + "refine": "add", + "boundingVolume": { + "sphere": [ + 0, + 0, + 0, + 5 + ] + }, + "geometricError": 0, + "content": { + "url": "pointCloudWithTransform.pnts" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Style/style.json b/Specs/Data/Cesium3DTiles/Style/style.json new file mode 100644 index 000000000000..6bd92818892d --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Style/style.json @@ -0,0 +1,5 @@ +{ + "color" : "color('red')", + "show" : "${id} < 100", + "pointSize" : "${id} / 100" +} \ No newline at end of file diff --git a/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/Tileset/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/Tileset/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/parent.b3dm new file mode 100644 index 000000000000..81db7f15757b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/Tileset/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/tileset.json new file mode 100644 index 000000000000..b2ede34c3f38 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/tileset.json @@ -0,0 +1,118 @@ +{ + "asset": { + "version": "0.0", + "tilesetVersion": "1.2.3" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/Tileset/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/tileset.json new file mode 100644 index 000000000000..8d9760a740ba --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/tileset.json @@ -0,0 +1,104 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 13.992324123159051 + } + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/parent.b3dm new file mode 100644 index 000000000000..81db7f15757b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json new file mode 100644 index 000000000000..c3f31982179f --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "tileset2.json" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json new file mode 100644 index 000000000000..bb2342e10ee7 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json @@ -0,0 +1,89 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "parent.b3dm" + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "tileset3/tileset3.json" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/tileset3.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/tileset3.json new file mode 100644 index 000000000000..9cb6e6d8927d --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/tileset3.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 0, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "refine": "add", + "content": { + "url": "ll.b3dm" + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/parent.b3dm new file mode 100644 index 000000000000..81db7f15757b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/tileset.json new file mode 100644 index 000000000000..a267e65f5edc --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/tileset.json @@ -0,0 +1,159 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "replace", + "content": { + "url": "parent.b3dm" + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "refine": "add", + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "refine": "replace", + "content": { + "url": "ur.b3dm" + } + } + ] + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "parent.b3dm" + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "refine": "add", + "content": { + "url": "ul.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "refine": "replace", + "content": { + "url": "lr.b3dm" + } + } + ] + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/parent.b3dm new file mode 100644 index 000000000000..81db7f15757b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/tileset.json new file mode 100644 index 000000000000..94d831ec611f --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/tileset.json @@ -0,0 +1,149 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "replace", + "content": { + "url": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + } + ] + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement1/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/parent.b3dm new file mode 100644 index 000000000000..81db7f15757b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/tileset.json new file mode 100644 index 000000000000..0f386d533338 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/tileset.json @@ -0,0 +1,117 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197180493677987, + "maximum": -1.3196513750495065 + }, + "Latitude": { + "minimum": 0.6988530761634713, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "replace", + "content": { + "url": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 7, + "refine": "replace", + "children": [ + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + } + ] + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + } + ] + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement2/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/parent.b3dm new file mode 100644 index 000000000000..81db7f15757b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset.json new file mode 100644 index 000000000000..21564b776b9b --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset.json @@ -0,0 +1,70 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "replace", + "content": { + "url": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "tileset2.json" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset2.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset2.json new file mode 100644 index 000000000000..30f0a8e37191 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset2.json @@ -0,0 +1,86 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "replace", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacement3/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/parent.b3dm new file mode 100644 index 000000000000..81db7f15757b Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/tileset.json new file mode 100644 index 000000000000..b625126b1b2e --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/tileset.json @@ -0,0 +1,173 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 240, + "refine": "replace", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "replace", + "content": { + "url": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "viewerRequestVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 50 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "viewerRequestVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 50 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "viewerRequestVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 50 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "viewerRequestVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 50 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ll.b3dm new file mode 100644 index 000000000000..b97c4367854c Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/lr.b3dm new file mode 100644 index 000000000000..641627aafc62 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/parent.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/parent.b3dm new file mode 100644 index 000000000000..8f1a47aee7e3 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/parent.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json new file mode 100644 index 000000000000..1451c6355150 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json @@ -0,0 +1,86 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/tileset.json new file mode 100644 index 000000000000..e9099172b26b --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/tileset.json @@ -0,0 +1,72 @@ +{ + "asset": { + "version": "0.0" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + }, + "url": "parent.b3dm" + }, + "children": [ + { + "expire": { + "duration": 5 + }, + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "content": { + "url": "subtree.json" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ul.b3dm new file mode 100644 index 000000000000..30c904d18b81 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ur.b3dm new file mode 100644 index 000000000000..e4ea60dcf5c1 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/ur.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/embed.i3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/embed.i3dm new file mode 100644 index 000000000000..54c54da85479 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/embed.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/external.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/external.b3dm new file mode 100644 index 000000000000..37068256bb46 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/external.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/external.i3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/external.i3dm new file mode 100644 index 000000000000..5da1db5fe54d Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/external.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/Cesium_Logo_Flat.png b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/Cesium_Logo_Flat.png new file mode 100644 index 000000000000..3b8baee1bce0 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/Cesium_Logo_Flat.png differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/fragmentShader0.glsl b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/fragmentShader0.glsl new file mode 100644 index 000000000000..3b357000a18f --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/fragmentShader0.glsl @@ -0,0 +1,33 @@ +precision highp float; +uniform vec4 u_ambient; +uniform sampler2D u_diffuse; +uniform vec4 u_emission; +uniform vec4 u_specular; +uniform float u_shininess; +uniform float u_transparency; +varying vec3 v_positionEC; +varying vec3 v_normal; +varying vec2 v_texcoord_0; +void main(void) { + vec3 normal = normalize(v_normal); + vec4 diffuse = texture2D(u_diffuse, v_texcoord_0); + vec3 diffuseLight = vec3(0.0, 0.0, 0.0); + vec3 specular = u_specular.rgb; + vec3 specularLight = vec3(0.0, 0.0, 0.0); + vec3 emission = u_emission.rgb; + vec3 ambient = u_ambient.rgb; + vec3 viewDir = -normalize(v_positionEC); + vec3 ambientLight = vec3(0.0, 0.0, 0.0); + ambientLight += vec3(0.2, 0.2, 0.2); + vec3 l = vec3(0.0, 0.0, 1.0); + diffuseLight += vec3(1.0, 1.0, 1.0) * max(dot(normal,l), 0.); + vec3 reflectDir = reflect(-l, normal); + float specularIntensity = max(0., pow(max(dot(reflectDir, viewDir), 0.), u_shininess)); + specularLight += vec3(1.0, 1.0, 1.0) * specularIntensity; + vec3 color = vec3(0.0, 0.0, 0.0); + color += diffuse.rgb * diffuseLight; + color += specular * specularLight; + color += emission; + color += ambient * ambientLight; + gl_FragColor = vec4(color * diffuse.a * u_transparency, diffuse.a * u_transparency); +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/textured_box.glb b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/textured_box.glb new file mode 100644 index 000000000000..5f65357c9eee Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/textured_box.glb differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/vertexShader0.glsl b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/vertexShader0.glsl new file mode 100644 index 000000000000..c489a5595ff8 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/textured_box_separate/vertexShader0.glsl @@ -0,0 +1,17 @@ +precision highp float; +uniform mat4 u_modelViewMatrix; +uniform mat4 u_projectionMatrix; +uniform mat3 u_normalMatrix; +attribute vec3 a_position; +varying vec3 v_positionEC; +attribute vec3 a_normal; +varying vec3 v_normal; +attribute vec2 a_texcoord_0; +varying vec2 v_texcoord_0; +void main(void) { + vec4 pos = u_modelViewMatrix * vec4(a_position,1.0); + v_positionEC = pos.xyz; + gl_Position = u_projectionMatrix * pos; + v_normal = u_normalMatrix * a_normal; + v_texcoord_0 = a_texcoord_0; +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset.json new file mode 100644 index 000000000000..572a43b44cfa --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset.json @@ -0,0 +1,145 @@ +{ + "asset": { + "version": "0.0", + "tilesetVersion": "1.2.3" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "tileset2/tileset2.json" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "external.b3dm" + }, + "transform": [ + 0.9686325809759725, + 0.24849733011005554, + 0, + 0, + -0.15987226587942635, + 0.6231756512501834, + 0.7655670880409421, + 0, + 0.190241377398304, + -0.7415532243993574, + 0.6433560707015301, + 0, + 1215072.0326519238, + -4736301.828828896, + 4081608.4380407534, + 1 + ] + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "external.i3dm" + }, + "transform": [ + 0.9686335987925251, + 0.24849336266838487, + 0, + 0, + -0.15986971340174239, + 0.6231763060686413, + 0.7655670880409422, + 0, + 0.1902383400555372, + -0.7415540036062118, + 0.6433560707015301, + 0, + 1215052.6331380012, + -4736306.80562453, + 4081608.4380407534, + 1 + ] + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "embed.i3dm" + }, + "transform": [ + 0.9686346165928273, + 0.24848939522254557, + 0, + 0, + -0.15986716092137648, + 0.6231769608766445, + 0.7655670880409424, + 0, + 0.19023530270957903, + -0.7415547828006255, + 0.6433560707015301, + 0, + 1215033.2336036952, + -4736311.782340704, + 4081608.4380407534, + 1 + ] + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/embed.i3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/embed.i3dm new file mode 100644 index 000000000000..855968339418 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/embed.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/external.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/external.b3dm new file mode 100644 index 000000000000..9c37a679cf1c Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/external.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/external.i3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/external.i3dm new file mode 100644 index 000000000000..169a87d21900 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/external.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/tileset2.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/tileset2.json new file mode 100644 index 000000000000..8a51f4353085 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset2/tileset2.json @@ -0,0 +1,127 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "external.b3dm" + }, + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.1598646084383285, + 0.6231776156741929, + 0.7655670880409422, + 0, + 0.19023226536042928, + -0.7415555619825982, + 0.6433560707015301, + 0, + 1215013.8340490046, + -4736316.75897742, + 4081608.4380407534, + 1 + ] + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "external.i3dm" + }, + "transform": [ + 0.9686366521446808, + 0.2484814603183605, + 0, + 0, + -0.1598620559525986, + 0.6231782704612867, + 0.7655670880409423, + 0, + 0.19022922800808817, + -0.7415563411521302, + 0.6433560707015301, + 0, + 1214994.4344739306, + -4736321.735534675, + 4081608.4380407534, + 1 + ] + }, + { + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "content": { + "url": "embed.i3dm" + }, + "transform": [ + 0.9686376698962317, + 0.24847749286001491, + 0, + 0, + -0.1598595034641867, + 0.6231789252379254, + 0.7655670880409422, + 0, + 0.19022619065255567, + -0.7415571203092216, + 0.6433560707015301, + 0, + 1214975.0348784733, + -4736326.712012473, + 4081608.4380407534, + 1 + ] + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/buildings.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/buildings.b3dm new file mode 100644 index 000000000000..01b0e171c5c9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/buildings.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/instances.i3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/instances.i3dm new file mode 100644 index 000000000000..8543f9d265fc Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/instances.i3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/tileset.json new file mode 100644 index 000000000000..094dd679bdf7 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/tileset.json @@ -0,0 +1,89 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "box": [ + 0, + 0, + 10, + 100, + 0, + 0, + 0, + 100, + 0, + 0, + 0, + 10 + ] + }, + "transform": [ + 0.9686356343768792, + 0.24848542777253735, + 0, + 0, + -0.15986460744966327, + 0.623177611820219, + 0.765567091384559, + 0, + 0.19023226619126932, + -0.7415555652213445, + 0.6433560667227647, + 0, + 1215011.9317263428, + -4736309.3434217675, + 4081602.0044800863, + 1 + ], + "geometricError": 70, + "refine": "add", + "content": { + "url": "buildings.b3dm" + }, + "children": [ + { + "boundingVolume": { + "box": [ + 0, + 0, + 0, + 100, + 0, + 0, + 0, + 100, + 0, + 0, + 0, + 15 + ] + }, + "transform": [ + 0.35355339059327373, + 0.3535533905932738, + 0, + 0, + -0.3535533905932738, + 0.35355339059327373, + 0, + 0, + 0, + 0, + 0.5, + 0, + 0, + 0, + 5, + 1 + ], + "geometricError": 0, + "content": { + "url": "instances.i3dm" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ll.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ll.b3dm new file mode 100644 index 000000000000..dbd0d2142ca9 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ll.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/lr.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/lr.b3dm new file mode 100644 index 000000000000..f052e259f078 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/lr.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/points.pnts b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/points.pnts new file mode 100644 index 000000000000..84e78cf66c18 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/points.pnts differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/tileset.json new file mode 100644 index 000000000000..067e2d15d12e --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/tileset.json @@ -0,0 +1,126 @@ +{ + "asset": { + "version": "0.0" + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "add", + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "url": "ul.b3dm" + } + }, + { + "transform": [ + 0.9686356343768793, + 0.24848542777253738, + 0, + 0, + -0.1598646089326599, + 0.6231776176011753, + 0.7655670863691378, + 0, + 0.19023226494501025, + -0.7415555603632288, + 0.643356072690908, + 0, + 1215014.7852103356, + -4736320.466755246, + 4081611.654821087, + 1 + ], + "viewerRequestVolume": { + "sphere": [ + 0, + 0, + 0, + 1000 + ] + }, + "boundingVolume": { + "sphere": [ + 0, + 0, + 0, + 10 + ] + }, + "geometricError": 0, + "content": { + "url": "points.pnts" + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ul.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ul.b3dm new file mode 100644 index 000000000000..a386047150bb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ul.b3dm differ diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ur.b3dm b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ur.b3dm new file mode 100644 index 000000000000..24a6c3e4abdb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/ur.b3dm differ diff --git a/Specs/Data/Models/moving-box/moving-box.gltf b/Specs/Data/Models/moving-box/moving-box.gltf new file mode 100644 index 000000000000..68690184fcd3 --- /dev/null +++ b/Specs/Data/Models/moving-box/moving-box.gltf @@ -0,0 +1,324 @@ +{ + "accessors": { + "accessor_16": { + "bufferView": "bufferView_34", + "byteOffset": 0, + "byteStride": 0, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + "accessor_18": { + "bufferView": "bufferView_35", + "byteOffset": 0, + "byteStride": 12, + "componentType": 5126, + "count": 24, + "max": [ + 1, + 1, + 1 + ], + "min": [ + -1, + -1, + -1 + ], + "type": "VEC3" + }, + "accessor_20": { + "bufferView": "bufferView_35", + "byteOffset": 288, + "byteStride": 12, + "componentType": 5126, + "count": 24, + "max": [ + 1, + 1, + 1 + ], + "min": [ + -1, + -1, + -1 + ], + "type": "VEC3" + }, + "animAccessor_0": { + "bufferView": "bufferView_33", + "byteOffset": 0, + "componentType": 5126, + "count": 2, + "type": "SCALAR" + }, + "animAccessor_1": { + "bufferView": "bufferView_33", + "byteOffset": 8, + "componentType": 5126, + "count": 2, + "type": "VEC3" + } + }, + "animations": { + "animation_0": { + "channels": [ + { + "sampler": "animation_0_translation_sampler", + "target": { + "id": "Cube", + "path": "translation" + } + } + ], + "parameters": { + "TIME": "animAccessor_0", + "translation": "animAccessor_1" + }, + "samplers": { + "animation_0_translation_sampler": { + "input": "TIME", + "interpolation": "LINEAR", + "output": "translation" + } + } + }, + "animation_1": { + "channels": [], + "parameters": { + "TIME": "animAccessor_0" + }, + "samplers": {} + }, + "animation_2": { + "channels": [], + "parameters": { + "TIME": "animAccessor_0" + }, + "samplers": {} + } + }, + "asset": { + "generator": "collada2gltf@", + "premultipliedAlpha": true, + "profile": { + "api": "WebGL", + "version": "1.0.2" + }, + "version": 1 + }, + "bufferViews": { + "bufferView_33": { + "buffer": "input", + "byteLength": 32, + "byteOffset": 0 + }, + "bufferView_34": { + "buffer": "input", + "byteLength": 72, + "byteOffset": 32, + "target": 34963 + }, + "bufferView_35": { + "buffer": "input", + "byteLength": 576, + "byteOffset": 104, + "target": 34962 + } + }, + "buffers": { + "input": { + "byteLength": 680, + "type": "arraybuffer", + "uri": "data:application/octet-stream;base64,maoqPT9VVT4AAAAAAAAAAAAAAAAAAAAA76dTQQAAAAAAAAEAAgADAAQABQAGAAcACAAJAAoACwAMAA0ADgAPABAAEQASAAAAAgATAAMABQAUAAYACAAVAAkACwAWAAwADgAXAA8AEQAAAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAvwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAPwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgL8AAIA/AACAvwAAgL8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIA/AACAvwAAgD8AAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIC/AAAAAAAAAAAAAAAAAACAPwAAAAAAAIA/AAAAAAAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8=" + } + }, + "materials": { + "Material_001-effect": { + "name": "Material_001", + "technique": "technique0", + "values": { + "ambient": [ + 0, + 0, + 0, + 1 + ], + "diffuse": [ + 0, + 1, + 0.0019188299775123596, + 1 + ], + "emission": [ + 0, + 0, + 0, + 1 + ], + "shininess": 50, + "specular": [ + 0.5, + 0.5, + 0.5, + 1 + ] + } + } + }, + "meshes": { + "Cube_001-mesh": { + "name": "Cube.001", + "primitives": [ + { + "attributes": { + "NORMAL": "accessor_20", + "POSITION": "accessor_18" + }, + "indices": "accessor_16", + "material": "Material_001-effect", + "mode": 4 + } + ] + } + }, + "nodes": { + "Cube": { + "children": [], + "meshes": [ + "Cube_001-mesh" + ], + "name": "Cube", + "rotation": [ + 0, + 0, + 0, + 1 + ], + "scale": [ + 1, + 1, + 1 + ], + "translation": [ + 0, + 0, + 0 + ] + }, + "node_1": { + "children": [ + "Cube" + ], + "matrix": [ + 1, + 0, + 0, + 0, + 0, + 0, + -1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1 + ], + "name": "Y_UP_Transform" + } + }, + "programs": { + "program_0": { + "attributes": [ + "a_normal", + "a_position" + ], + "fragmentShader": "input0FS", + "vertexShader": "input0VS" + } + }, + "scene": "defaultScene", + "scenes": { + "defaultScene": { + "nodes": [ + "node_1" + ] + } + }, + "shaders": { + "input0FS": { + "type": 35632, + "uri": "data:text/plain;base64,cHJlY2lzaW9uIGhpZ2hwIGZsb2F0Owp2YXJ5aW5nIHZlYzMgdl9ub3JtYWw7CnVuaWZvcm0gdmVjNCB1X2FtYmllbnQ7CnVuaWZvcm0gdmVjNCB1X2RpZmZ1c2U7CnVuaWZvcm0gdmVjNCB1X2VtaXNzaW9uOwp1bmlmb3JtIHZlYzQgdV9zcGVjdWxhcjsKdW5pZm9ybSBmbG9hdCB1X3NoaW5pbmVzczsKdm9pZCBtYWluKHZvaWQpIHsKdmVjMyBub3JtYWwgPSBub3JtYWxpemUodl9ub3JtYWwpOwp2ZWM0IGNvbG9yID0gdmVjNCgwLiwgMC4sIDAuLCAwLik7CnZlYzQgZGlmZnVzZSA9IHZlYzQoMC4sIDAuLCAwLiwgMS4pOwp2ZWM0IGVtaXNzaW9uOwp2ZWM0IGFtYmllbnQ7CnZlYzQgc3BlY3VsYXI7CmFtYmllbnQgPSB1X2FtYmllbnQ7CmRpZmZ1c2UgPSB1X2RpZmZ1c2U7CmVtaXNzaW9uID0gdV9lbWlzc2lvbjsKc3BlY3VsYXIgPSB1X3NwZWN1bGFyOwpkaWZmdXNlLnh5eiAqPSBtYXgoZG90KG5vcm1hbCx2ZWMzKDAuLDAuLDEuKSksIDAuKTsKY29sb3IueHl6ICs9IGRpZmZ1c2UueHl6Owpjb2xvci54eXogKz0gZW1pc3Npb24ueHl6Owpjb2xvciA9IHZlYzQoY29sb3IucmdiICogZGlmZnVzZS5hLCBkaWZmdXNlLmEpOwpnbF9GcmFnQ29sb3IgPSBjb2xvcjsKfQo=" + }, + "input0VS": { + "type": 35633, + "uri": "data:text/plain;base64,cHJlY2lzaW9uIGhpZ2hwIGZsb2F0OwphdHRyaWJ1dGUgdmVjMyBhX3Bvc2l0aW9uOwphdHRyaWJ1dGUgdmVjMyBhX25vcm1hbDsKdmFyeWluZyB2ZWMzIHZfbm9ybWFsOwp1bmlmb3JtIG1hdDMgdV9ub3JtYWxNYXRyaXg7CnVuaWZvcm0gbWF0NCB1X21vZGVsVmlld01hdHJpeDsKdW5pZm9ybSBtYXQ0IHVfcHJvamVjdGlvbk1hdHJpeDsKdm9pZCBtYWluKHZvaWQpIHsKdmVjNCBwb3MgPSB1X21vZGVsVmlld01hdHJpeCAqIHZlYzQoYV9wb3NpdGlvbiwxLjApOwp2X25vcm1hbCA9IHVfbm9ybWFsTWF0cml4ICogYV9ub3JtYWw7CmdsX1Bvc2l0aW9uID0gdV9wcm9qZWN0aW9uTWF0cml4ICogcG9zOwp9Cg==" + } + }, + "skins": {}, + "techniques": { + "technique0": { + "attributes": { + "a_normal": "normal", + "a_position": "position" + }, + "parameters": { + "ambient": { + "type": 35666 + }, + "diffuse": { + "type": 35666 + }, + "emission": { + "type": 35666 + }, + "modelViewMatrix": { + "semantic": "MODELVIEW", + "type": 35676 + }, + "normal": { + "semantic": "NORMAL", + "type": 35665 + }, + "normalMatrix": { + "semantic": "MODELVIEWINVERSETRANSPOSE", + "type": 35675 + }, + "position": { + "semantic": "POSITION", + "type": 35665 + }, + "projectionMatrix": { + "semantic": "PROJECTION", + "type": 35676 + }, + "shininess": { + "type": 5126 + }, + "specular": { + "type": 35666 + } + }, + "program": "program_0", + "states": { + "enable": [ + 2929, + 2884 + ] + }, + "uniforms": { + "u_ambient": "ambient", + "u_diffuse": "diffuse", + "u_emission": "emission", + "u_modelViewMatrix": "modelViewMatrix", + "u_normalMatrix": "normalMatrix", + "u_projectionMatrix": "projectionMatrix", + "u_shininess": "shininess", + "u_specular": "specular" + } + } + } +} \ No newline at end of file diff --git a/Specs/Scene/Batched3DModel3DTileContentSpec.js b/Specs/Scene/Batched3DModel3DTileContentSpec.js new file mode 100644 index 000000000000..54122006f0c1 --- /dev/null +++ b/Specs/Scene/Batched3DModel3DTileContentSpec.js @@ -0,0 +1,296 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Batched3DModel3DTileContent', + 'Core/Cartesian3', + 'Core/Color', + 'Core/deprecationWarning', + 'Core/HeadingPitchRange', + 'Core/HeadingPitchRoll', + 'Core/Transforms', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene' + ], function( + Batched3DModel3DTileContent, + Cartesian3, + Color, + deprecationWarning, + HeadingPitchRange, + HeadingPitchRoll, + Transforms, + Cesium3DTilesTester, + createScene) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + var withBatchTableUrl = './Data/Cesium3DTiles/Batched/BatchedWithBatchTable/'; + var withBatchTableBinaryUrl = './Data/Cesium3DTiles/Batched/BatchedWithBatchTableBinary/'; + var withoutBatchTableUrl = './Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/'; + var translucentUrl = './Data/Cesium3DTiles/Batched/BatchedTranslucent/'; + var translucentOpaqueMixUrl = './Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/'; + var withKHRMaterialsCommonUrl = './Data/Cesium3DTiles/Batched/BatchedWithKHRMaterialsCommon/'; + var withTransformBoxUrl = './Data/Cesium3DTiles/Batched/BatchedWithTransformBox/'; + var withTransformSphereUrl = './Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/'; + var withTransformRegionUrl = './Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/'; + var texturedUrl = './Data/Cesium3DTiles/Batched/BatchedTextured/'; + var compressedTexturesUrl = './Data/Cesium3DTiles/Batched/BatchedCompressedTextures/'; + var deprecated1Url = './Data/Cesium3DTiles/Batched/BatchedDeprecated1/'; + var deprecated2Url = './Data/Cesium3DTiles/Batched/BatchedDeprecated2/'; + var gltfZUpUrl = './Data/Cesium3DTiles/Batched/BatchedGltfZUp'; + + function setCamera(longitude, latitude) { + // One feature is located at the center, point the camera there + var center = Cartesian3.fromRadians(longitude, latitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 15.0)); + } + + beforeAll(function() { + scene = createScene(); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + beforeEach(function() { + setCamera(centerLongitude, centerLatitude); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + it('throws with invalid version', function() { + var arrayBuffer = Cesium3DTilesTester.generateBatchedTileBuffer({ + version : 2 + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'b3dm'); + }); + + it('recognizes the legacy 20-byte header', function() { + spyOn(Batched3DModel3DTileContent, '_deprecationWarning'); + return Cesium3DTilesTester.loadTileset(scene, deprecated1Url) + .then(function(tileset) { + expect(Batched3DModel3DTileContent._deprecationWarning).toHaveBeenCalled(); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + var batchTable = tileset._root._content.batchTable; + expect(batchTable.batchTableJson).toBeDefined(); + expect(batchTable.batchTableBinary).toBeUndefined(); + }); + }); + + it('recognizes the legacy 24-byte header', function() { + spyOn(Batched3DModel3DTileContent, '_deprecationWarning'); + return Cesium3DTilesTester.loadTileset(scene, deprecated2Url) + .then(function(tileset) { + expect(Batched3DModel3DTileContent._deprecationWarning).toHaveBeenCalled(); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + var batchTable = tileset._root._content.batchTable; + expect(batchTable.batchTableJson).toBeDefined(); + expect(batchTable.batchTableBinary).toBeUndefined(); + }); + }); + + it('logs deprecation warning for use of BATCHID without prefixed underscore', function() { + spyOn(Batched3DModel3DTileContent, '_deprecationWarning'); + return Cesium3DTilesTester.loadTileset(scene, deprecated1Url) + .then(function(tileset) { + expect(Batched3DModel3DTileContent._deprecationWarning).toHaveBeenCalled(); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('throws with empty gltf', function() { + // Expect to throw DeveloperError in Model due to invalid gltf magic + var arrayBuffer = Cesium3DTilesTester.generateBatchedTileBuffer(); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'b3dm'); + }); + + it('resolves readyPromise', function() { + return Cesium3DTilesTester.resolvesReadyPromise(scene, withoutBatchTableUrl); + }); + + it('renders with batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with batch table binary', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableBinaryUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders without batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with all features translucent', function() { + return Cesium3DTilesTester.loadTileset(scene, translucentUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with a mix of opaque and translucent features', function() { + return Cesium3DTilesTester.loadTileset(scene, translucentOpaqueMixUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with KHR_materials_common extension', function() { + // Tests that the batchId attribute and CESIUM_RTC extension are handled correctly + return Cesium3DTilesTester.loadTileset(scene, withKHRMaterialsCommonUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with textures', function() { + return Cesium3DTilesTester.loadTileset(scene, texturedUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders with compressed textures', function() { + return Cesium3DTilesTester.loadTileset(scene, compressedTexturesUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders with a gltf z-up axis', function() { + return Cesium3DTilesTester.loadTileset(scene, gltfZUpUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + function expectRenderWithTransform(url) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + + var newLongitude = -1.31962; + var newLatitude = 0.698874; + var newCenter = Cartesian3.fromRadians(newLongitude, newLatitude, 0.0); + var newHPR = new HeadingPitchRoll(); + var newTransform = Transforms.headingPitchRollToFixedFrame(newCenter, newHPR); + + // Update tile transform + tileset._root.transform = newTransform; + scene.renderForSpecs(); + + // Move the camera to the new location + setCamera(newLongitude, newLatitude); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + } + + it('renders with a tile transform and box bounding volume', function() { + return expectRenderWithTransform(withTransformBoxUrl); + }); + + it('renders with a tile transform and sphere bounding volume', function() { + return expectRenderWithTransform(withTransformSphereUrl); + }); + + it('renders with a tile transform and region bounding volume', function() { + return Cesium3DTilesTester.loadTileset(scene, withTransformRegionUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('picks with batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + tileset.show = false; + expect(scene).toPickPrimitive(undefined); + tileset.show = true; + expect(scene).toPickAndCall(function(result) { + expect(result).toBeDefined(); + expect(result.primitive).toBe(tileset); + expect(result.content).toBe(content); + }); + }); + }); + + it('picks without batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + tileset.show = false; + expect(scene).toPickPrimitive(undefined); + tileset.show = true; + expect(scene).toPickAndCall(function(result) { + expect(result).toBeDefined(); + expect(result.primitive).toBe(tileset); + expect(result.content).toBe(content); + }); + }); + }); + + it('can get features and properties', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + expect(content.featuresLength).toBe(10); + expect(content.innerContents).toBeUndefined(); + expect(content.hasProperty(0, 'id')).toBe(true); + expect(content.getFeature(0)).toBeDefined(); + }); + }); + + it('throws when calling getFeature with invalid index', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + expect(function(){ + content.getFeature(-1); + }).toThrowDeveloperError(); + expect(function(){ + content.getFeature(1000); + }).toThrowDeveloperError(); + expect(function(){ + content.getFeature(); + }).toThrowDeveloperError(); + }); + }); + + it('gets memory usage', function() { + return Cesium3DTilesTester.loadTileset(scene, texturedUrl).then(function(tileset) { + var content = tileset._root.content; + + // 10 buildings, 32 ushort indices and 24 vertices per building, 8 float components (position, normal, uv) and 1 ushort component (batchId) per vertex. + // 10 * ((24 * (8 * 4 + 1 * 2)) + (36 * 2)) = 8880 + var geometryByteLength = 8880; + + // Texture is 128x128 RGBA bytes, not mipmapped + var texturesByteLength = 65536; + + // One RGBA byte pixel per feature + var batchTexturesByteLength = content.featuresLength * 4; + var pickTexturesByteLength = content.featuresLength * 4; + + // Features have not been picked or colored yet, so the batch table contribution is 0. + expect(content.geometryByteLength).toEqual(geometryByteLength); + expect(content.texturesByteLength).toEqual(texturesByteLength); + expect(content.batchTableByteLength).toEqual(0); + + // Color a feature and expect the texture memory to increase + content.getFeature(0).color = Color.RED; + scene.renderForSpecs(); + expect(content.geometryByteLength).toEqual(geometryByteLength); + expect(content.texturesByteLength).toEqual(texturesByteLength); + expect(content.batchTableByteLength).toEqual(batchTexturesByteLength); + + // Pick the tile and expect the texture memory to increase + scene.pickForSpecs(); + expect(content.geometryByteLength).toEqual(geometryByteLength); + expect(content.texturesByteLength).toEqual(texturesByteLength); + expect(content.batchTableByteLength).toEqual(batchTexturesByteLength + pickTexturesByteLength); + }); + }); + + it('destroys', function() { + return Cesium3DTilesTester.tileDestroys(scene, withoutBatchTableUrl); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/Cesium3DTileBatchTableSpec.js b/Specs/Scene/Cesium3DTileBatchTableSpec.js new file mode 100644 index 000000000000..2db529bb5692 --- /dev/null +++ b/Specs/Scene/Cesium3DTileBatchTableSpec.js @@ -0,0 +1,1103 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Cesium3DTileBatchTable', + 'Core/Cartesian2', + 'Core/Cartesian3', + 'Core/Cartesian4', + 'Core/Color', + 'Core/defined', + 'Core/HeadingPitchRange', + 'Core/Matrix2', + 'Core/Matrix3', + 'Core/Matrix4', + 'Renderer/ContextLimits', + 'Scene/Cesium3DTileStyle', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene' + ], function( + Cesium3DTileBatchTable, + Cartesian2, + Cartesian3, + Cartesian4, + Color, + defined, + HeadingPitchRange, + Matrix2, + Matrix3, + Matrix4, + ContextLimits, + Cesium3DTileStyle, + Cesium3DTilesTester, + createScene) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + var withBatchTableUrl = './Data/Cesium3DTiles/Batched/BatchedWithBatchTable/'; + var withoutBatchTableUrl = './Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/'; + var noBatchIdsUrl = './Data/Cesium3DTiles/Batched/BatchedNoBatchIds/'; + var batchTableHierarchyUrl = './Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/'; + var batchTableHierarchyBinaryUrl = './Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/'; + var batchTableHierarchyMultipleParentsUrl = './Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/'; + var batchTableHierarchyNoParentsUrl = './Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/'; + + var result = new Color(); + + var mockTileset = { + _statistics : { + texturesByteLength : 0 + } + }; + + beforeAll(function() { + scene = createScene(); + + // One feature is located at the center, point the camera there + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 20.0)); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + it('setShow throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setShow(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.setShow(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.setShow(2); + }).toThrowDeveloperError(); + }); + + it('setShow throws with undefined value', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setShow(0); + }).toThrowDeveloperError(); + }); + + it('setShow', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + + // Batch table resources are undefined by default + expect(batchTable._batchValues).toBeUndefined(); + expect(batchTable._batchTexture).toBeUndefined(); + + // Check that batch table resources are still undefined because value is true by default + batchTable.setShow(0, true); + batchTable.update(mockTileset, scene.frameState); + expect(batchTable._batchValues).toBeUndefined(); + expect(batchTable._batchTexture).toBeUndefined(); + expect(batchTable.getShow(0)).toEqual(true); + + // Check that batch values are dirty and resources are created when value changes + batchTable.setShow(0, false); + expect(batchTable._batchValuesDirty).toEqual(true); + batchTable.update(mockTileset, scene.frameState); + expect(batchTable._batchValues).toBeDefined(); + expect(batchTable._batchTexture).toBeDefined(); + expect(batchTable._batchValuesDirty).toEqual(false); + expect(batchTable.getShow(0)).toEqual(false); + + // Check that dirty stays false when value is the same + batchTable.setShow(0, false); + expect(batchTable._batchValuesDirty).toEqual(false); + expect(batchTable.getShow(0)).toEqual(false); + }); + + it('getShow throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.getShow(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getShow(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getShow(2); + }).toThrowDeveloperError(); + }); + + it('getShow', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + // Show is true by default + expect(batchTable.getShow(0)).toEqual(true); + batchTable.setShow(0, false); + expect(batchTable.getShow(0)).toEqual(false); + }); + + it('setColor throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setColor(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.setColor(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.setColor(2); + }).toThrowDeveloperError(); + }); + + it('setColor throws with undefined value', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setColor(0); + }).toThrowDeveloperError(); + }); + + it('setColor', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + + // Batch table resources are undefined by default + expect(batchTable._batchValues).toBeUndefined(); + expect(batchTable._batchTexture).toBeUndefined(); + + // Check that batch table resources are still undefined because value is true by default + batchTable.setColor(0, Color.WHITE); + batchTable.update(mockTileset, scene.frameState); + expect(batchTable._batchValues).toBeUndefined(); + expect(batchTable._batchTexture).toBeUndefined(); + expect(batchTable.getColor(0, result)).toEqual(Color.WHITE); + + // Check that batch values are dirty and resources are created when value changes + batchTable.setColor(0, Color.YELLOW); + expect(batchTable._batchValuesDirty).toEqual(true); + batchTable.update(mockTileset, scene.frameState); + expect(batchTable._batchValues).toBeDefined(); + expect(batchTable._batchTexture).toBeDefined(); + expect(batchTable._batchValuesDirty).toEqual(false); + expect(batchTable.getColor(0, result)).toEqual(Color.YELLOW); + + // Check that dirty stays false when value is the same + batchTable.setColor(0, Color.YELLOW); + expect(batchTable._batchValuesDirty).toEqual(false); + expect(batchTable.getColor(0, result)).toEqual(Color.YELLOW); + }); + + it('setAllColor throws with undefined value', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setAllColor(); + }).toThrowDeveloperError(); + }); + + it('setAllColor', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 2); + batchTable.setAllColor(Color.YELLOW); + expect(batchTable.getColor(0, result)).toEqual(Color.YELLOW); + expect(batchTable.getColor(1, result)).toEqual(Color.YELLOW); + }); + + it('setAllShow throws with undefined value', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setAllShow(); + }).toThrowDeveloperError(); + }); + + it('setAllShow', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 2); + batchTable.setAllShow(false); + expect(batchTable.getShow(0)).toBe(false); + expect(batchTable.getShow(1)).toBe(false); + }); + + it('getColor throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.getColor(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getColor(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getColor(2); + }).toThrowDeveloperError(); + }); + + it('getColor throws with undefined result', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.getColor(0); + }).toThrowDeveloperError(); + }); + + it('getColor', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + // Color is true by default + expect(batchTable.getColor(0, result)).toEqual(Color.WHITE); + batchTable.setColor(0, Color.YELLOW); + expect(batchTable.getColor(0, result)).toEqual(Color.YELLOW); + }); + + it('hasProperty throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.hasProperty(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.hasProperty(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.hasProperty(2); + }).toThrowDeveloperError(); + }); + + it('hasProperty throws with undefined name', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.hasProperty(0); + }).toThrowDeveloperError(); + }); + + it('hasProperty', function() { + var batchTableJson = { + height: [0.0] + }; + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1, batchTableJson); + expect(batchTable.hasProperty(0, 'height')).toEqual(true); + expect(batchTable.hasProperty(0, 'id')).toEqual(false); + }); + + it('getPropertyNames throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.getPropertyNames(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getPropertyNames(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getPropertyNames(2); + }).toThrowDeveloperError(); + }); + + it('getPropertyNames', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(batchTable.getPropertyNames(0)).toEqual([]); + + var batchTableJson = { + height: [0.0], + id : [0] + }; + batchTable = new Cesium3DTileBatchTable(mockTileset, 1, batchTableJson); + expect(batchTable.getPropertyNames(0)).toEqual(['height', 'id']); + }); + + it('getPropertyNames works with results argument', function() { + var batchTableJson = { + height: [0.0], + id : [0] + }; + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1, batchTableJson); + var results = []; + var names = batchTable.getPropertyNames(0, results); + expect(names).toBe(results); + expect(names).toEqual(['height', 'id']); + }); + + it('getProperty throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.getProperty(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getProperty(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getProperty(2); + }).toThrowDeveloperError(); + }); + + it('getProperty throws with undefined name', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.getProperty(0); + }).toThrowDeveloperError(); + }); + + it('getProperty', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(batchTable.getProperty(0, 'height')).toBeUndefined(); + + var batchTableJson = { + height: [1.0] + }; + batchTable = new Cesium3DTileBatchTable(mockTileset, 1, batchTableJson); + expect(batchTable.getProperty(0, 'height')).toEqual(1.0); + expect(batchTable.getProperty(0, 'id')).toBeUndefined(); + }); + + it('setProperty throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setProperty(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.setProperty(-1); + }).toThrowDeveloperError(); + expect(function() { + batchTable.setProperty(2); + }).toThrowDeveloperError(); + }); + + it('setProperty throws with undefined name', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.setProperty(0); + }).toThrowDeveloperError(); + }); + + it('setProperty without existing batch table', function() { + // Check that a batch table is created with a height of 1.0 for the first resource and undefined for the others + var batchTable = new Cesium3DTileBatchTable(mockTileset, 3); + batchTable.setProperty(0, 'height', 1.0); + + expect(batchTable.batchTableJson.height.length).toEqual(3); + expect(batchTable.getProperty(0, 'height')).toEqual(1.0); + expect(batchTable.getProperty(1, 'height')).toBeUndefined(); + expect(batchTable.getProperty(2, 'height')).toBeUndefined(); + }); + + it('setProperty with existing batch table', function() { + var batchTableJson = { + height : [1.0, 2.0] + }; + var batchTable = new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson); + batchTable.setProperty(0, 'height', 3.0); + + expect(batchTable.getProperty(0, 'height')).toEqual(3.0); + expect(batchTable.getProperty(1, 'height')).toEqual(2.0); + }); + + it('setProperty with object value', function() { + var batchTableJson = { + info : [{name : 'building0', year : 2000}, {name : 'building1', year : 2001}] + }; + var batchTable = new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson); + batchTable.setProperty(0, 'info', {name : 'building0_new', year : 2002}); + + expect(batchTable.getProperty(0, 'info')).toEqual({name : 'building0_new', year : 2002}); + expect(batchTable.getProperty(1, 'info')).toEqual({name : 'building1', year : 2001}); + }); + + it('setProperty with array value', function() { + var batchTableJson = { + rooms : [['room1', 'room2'], ['room3', 'room4']] + }; + var batchTable = new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson); + batchTable.setProperty(0, 'rooms', ['room1_new', 'room2']); + + expect(batchTable.getProperty(0, 'rooms')).toEqual(['room1_new', 'room2']); + expect(batchTable.getProperty(1, 'rooms')).toEqual(['room3', 'room4']); + }); + + it('throws if the binary property does not specify a componentType', function() { + var batchTableJson = { + propertyScalar : { + byteOffset : 0, + type : 'SCALAR' + } + }; + var batchTableBinary = new Float64Array([0, 1]); + expect(function() { + return new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson, batchTableBinary); + }).toThrowRuntimeError(); + }); + + it('throws if the binary property does not specify a type', function() { + var batchTableJson = { + propertyScalar : { + byteOffset : 0, + componentType : 'DOUBLE' + } + }; + var batchTableBinary = new Float64Array([0, 1]); + expect(function() { + return new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson, batchTableBinary); + }).toThrowRuntimeError(); + }); + + it('throws if a binary property exists but there is no batchTableBinary', function() { + var batchTableJson = { + propertyScalar : { + byteOffset : 0, + componentType : 'DOUBLE', + type : 'SCALAR' + } + }; + expect(function() { + return new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson); + }).toThrowRuntimeError(); + }); + + function concatTypedArrays(arrays) { + var i; + var length = arrays.length; + + var byteLength = 0; + for (i = 0; i < length; ++i) { + byteLength += arrays[i].byteLength; + } + var buffer = new Uint8Array(byteLength); + + var byteOffset = 0; + for (i = 0; i < length; ++i) { + var data = new Uint8Array(arrays[i].buffer); + byteLength = data.length; + for (var j = 0; j < byteLength; ++j) { + buffer[byteOffset++] = data[j]; + } + } + return buffer; + } + + it('getProperty and setProperty work for binary properties', function() { + var propertyScalarBinary = new Float64Array([0, 1]); + var propertyVec2Binary = new Float32Array([2, 3, 4, 5]); + var propertyVec3Binary = new Int32Array([6, 7, 8, 9, 10, 11]); + var propertyVec4Binary = new Uint32Array([12, 13, 14, 15, 16, 17, 18, 19]); + var propertyMat2Binary = new Int16Array([20, 21, 22, 23, 24, 25, 26, 27]); + var propertyMat3Binary = new Uint16Array([28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45]); + var propertyMat4Binary = new Uint8Array([46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77]); + + var buffers = [propertyScalarBinary, propertyVec2Binary, propertyVec3Binary, propertyVec4Binary, propertyMat2Binary, propertyMat3Binary, propertyMat4Binary]; + var batchTableBinary = concatTypedArrays(buffers); + var batchTableJson = { + propertyScalar : { + byteOffset : 0, + componentType : 'DOUBLE', + type : 'SCALAR' + }, + propertyVec2 : { + byteOffset : 16, + componentType : 'FLOAT', + type : 'VEC2' + }, + propertyVec3 : { + byteOffset : 32, + componentType : 'INT', + type : 'VEC3' + }, + propertyVec4 : { + byteOffset : 56, + componentType : 'UNSIGNED_INT', + type : 'VEC4' + }, + propertyMat2 : { + byteOffset : 88, + componentType : 'SHORT', + type : 'MAT2' + }, + propertyMat3 : { + byteOffset : 104, + componentType : 'UNSIGNED_SHORT', + type : 'MAT3' + }, + propertyMat4 : { + byteOffset : 140, + componentType : 'UNSIGNED_BYTE', + type : 'MAT4' + } + }; + + var batchTable = new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson, batchTableBinary); + + expect(batchTable.getProperty(1, 'propertyScalar')).toEqual(1); + expect(batchTable.getProperty(1, 'propertyVec2')).toEqual(new Cartesian2(4, 5)); + expect(batchTable.getProperty(1, 'propertyVec3')).toEqual(new Cartesian3(9, 10, 11)); + expect(batchTable.getProperty(1, 'propertyVec4')).toEqual(new Cartesian4(16, 17, 18, 19)); + expect(batchTable.getProperty(1, 'propertyMat2')).toEqual(new Matrix2(24, 26, 25, 27)); // Constructor is row-major, data is column major + expect(batchTable.getProperty(1, 'propertyMat3')).toEqual(new Matrix3(37, 40, 43, 38, 41, 44, 39, 42, 45)); // Constructor is row-major, data is column major + expect(batchTable.getProperty(1, 'propertyMat4')).toEqual(new Matrix4(62, 66, 70, 74, 63, 67, 71, 75, 64, 68, 72, 76, 65, 69, 73, 77)); // Constructor is row-major, data is column major + + batchTable.setProperty(1, 'propertyScalar', 2); + batchTable.setProperty(1, 'propertyVec2', new Cartesian2(5, 6)); + batchTable.setProperty(1, 'propertyVec3', new Cartesian3(10, 11, 12)); + batchTable.setProperty(1, 'propertyVec4', new Cartesian4(17, 18, 19, 20)); + batchTable.setProperty(1, 'propertyMat2', new Matrix2(25, 27, 26, 28)); + batchTable.setProperty(1, 'propertyMat3', new Matrix3(38, 41, 44, 39, 42, 45, 40, 43, 46)); + batchTable.setProperty(1, 'propertyMat4', new Matrix4(63, 67, 71, 75, 64, 68, 72, 76, 65, 69, 73, 77, 66, 70, 74, 78)); + + expect(batchTable.getProperty(1, 'propertyScalar')).toEqual(2); + expect(batchTable.getProperty(1, 'propertyVec2')).toEqual(new Cartesian2(5, 6)); + expect(batchTable.getProperty(1, 'propertyVec3')).toEqual(new Cartesian3(10, 11, 12)); + expect(batchTable.getProperty(1, 'propertyVec4')).toEqual(new Cartesian4(17, 18, 19, 20)); + expect(batchTable.getProperty(1, 'propertyMat2')).toEqual(new Matrix2(25, 27, 26, 28)); + expect(batchTable.getProperty(1, 'propertyMat3')).toEqual(new Matrix3(38, 41, 44, 39, 42, 45, 40, 43, 46)); + expect(batchTable.getProperty(1, 'propertyMat4')).toEqual(new Matrix4(63, 67, 71, 75, 64, 68, 72, 76, 65, 69, 73, 77, 66, 70, 74, 78)); + }); + + it('renders tileset with batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + + // Each feature in the b3dm file has an id property from 0 to 9, + // check that the 2nd resource has an id of 2 + expect(content.getFeature(2).getProperty('id')).toEqual(2); + + // Check that a property can be an array + expect(content.getFeature(2).getProperty('rooms')).toEqual(['room2_a', 'room2_b', 'room2_c']); + + // Check that a property can be an object + expect(content.getFeature(2).getProperty('info')).toEqual({name : 'building2', year : 2}); + + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders tileset without batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + + expect(content.getFeature(2).getProperty('id')).toBeUndefined(); + + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders when vertex texture fetch is not supported', function() { + // Disable VTF + var maximumVertexTextureImageUnits = ContextLimits.maximumVertexTextureImageUnits; + ContextLimits._maximumVertexTextureImageUnits = 0; + + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + + // Re-enable VTF + ContextLimits._maximumVertexTextureImageUnits = maximumVertexTextureImageUnits; + }); + }); + + it('renders with featuresLength greater than maximumTextureSize', function() { + // Set maximum texture size to 4 temporarily. Batch length of b3dm file is 10. + var maximumTextureSize = ContextLimits.maximumTextureSize; + ContextLimits._maximumTextureSize = 4; + + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + expect(content.featuresLength).toBeGreaterThan(ContextLimits._maximumTextureSize); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + + // Reset maximum texture size + ContextLimits._maximumTextureSize = maximumTextureSize; + }); + }); + + it('renders with featuresLength of zero', function() { + return Cesium3DTilesTester.loadTileset(scene, noBatchIdsUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + + expect(scene).toPickAndCall(function(result) { + expect(result).toBeDefined(); + expect(result.primitive).toBe(tileset); + }); + }); + }); + + function expectRenderTranslucent(tileset) { + var batchTable = tileset._root.content.batchTable; + + // Get initial color + var opaqueColor; + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + opaqueColor = rgba; + }); + + // Render translucent + batchTable.setAllColor(new Color(1.0, 1.0, 1.0, 0.5)); + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba).not.toEqual(opaqueColor); + }); + + // Render restored to opaque + batchTable.setAllColor(Color.WHITE); + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba).toEqual(opaqueColor); + }); + + // Generate both translucent and opaque commands + batchTable.setColor(0, new Color(1.0, 1.0, 1.0, 0.5)); + Cesium3DTilesTester.expectRender(scene, tileset); + + // Fully transparent + batchTable.setAllColor(new Color(1.0, 1.0, 1.0, 0.0)); + Cesium3DTilesTester.expectRenderBlank(scene, tileset); + } + + it('renders translucent style', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + expectRenderTranslucent(tileset); + }); + }); + + it('renders translucent style when vertex texture fetch is not supported', function() { + // Disable VTF + var maximumVertexTextureImageUnits = ContextLimits.maximumVertexTextureImageUnits; + ContextLimits._maximumVertexTextureImageUnits = 0; + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + expectRenderTranslucent(tileset); + // Re-enable VTF + ContextLimits._maximumVertexTextureImageUnits = maximumVertexTextureImageUnits; + }); + }); + + it('isExactClass throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.isExactClass(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.isExactClass(2, 'door'); + }).toThrowDeveloperError(); + expect(function() { + batchTable.isExactClass(-1, 'door'); + }).toThrowDeveloperError(); + }); + + it('isExactClass throws with undefined className', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.isExactClass(0); + }).toThrowDeveloperError(); + }); + + it('isClass throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.isClass(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.isClass(2, 'door'); + }).toThrowDeveloperError(); + expect(function() { + batchTable.isClass(-1, 'door'); + }).toThrowDeveloperError(); + }); + + it('isClass throws with undefined className', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.isClass(0); + }).toThrowDeveloperError(); + }); + + it('getExactClassName throws with invalid batchId', function() { + var batchTable = new Cesium3DTileBatchTable(mockTileset, 1); + expect(function() { + batchTable.getExactClassName(); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getExactClassName(1000); + }).toThrowDeveloperError(); + expect(function() { + batchTable.getExactClassName(-1); + }).toThrowDeveloperError(); + }); + + function checkHierarchyStyling(tileset) { + // Check that a feature is colored from a generic batch table property. + tileset.style = new Cesium3DTileStyle({color : "${height} === 6.0 ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check that a feature is colored from a class property. + tileset.style = new Cesium3DTileStyle({color : "${roof_name} === 'roof2' ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check that a feature is colored from an inherited property. + tileset.style = new Cesium3DTileStyle({color : "${building_name} === 'building2' ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check isExactClass + tileset.style = new Cesium3DTileStyle({color : "isExactClass('roof') ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + tileset.style = new Cesium3DTileStyle({color : "isExactClass('door') ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[1]).toBeGreaterThan(1); // Expect green + }); + + // Check isClass + tileset.style = new Cesium3DTileStyle({color : "isClass('roof') ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + tileset.style = new Cesium3DTileStyle({color : "isClass('zone') ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check getExactClassName + tileset.style = new Cesium3DTileStyle({color : "getExactClassName() === 'roof' ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + tileset.style = new Cesium3DTileStyle({color : "getExactClassName() === 'zone' ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[1]).toBeGreaterThan(0); // Expect green + }); + } + + function checkHierarchyStylingNoParents(tileset) { + // Check that a feature is colored from a generic batch table property. + tileset.style = new Cesium3DTileStyle({color : "${height} === 6.0 ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check that a feature is colored from a class property. + tileset.style = new Cesium3DTileStyle({color : "${roof_name} === 'roof2' ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check isExactClass + tileset.style = new Cesium3DTileStyle({color : "isExactClass('roof') ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check isClass + tileset.style = new Cesium3DTileStyle({color : "isClass('roof') ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + + // Check getExactClassName + tileset.style = new Cesium3DTileStyle({color : "getExactClassName() === 'roof' ? color('red') : color('green')"}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); // Expect red + }); + } + + function checkHierarchyProperties(tileset, multipleParents) { + // Check isExactClass, isClass, and getExactClassName in Cesium3DTileFeature + var content = tileset._root.content; + var batchTable = content.batchTable; + var hierarchy = batchTable._batchTableHierarchy; + + var doorFeature = content.getFeature(4); + var roofFeature = content.getFeature(8); + expect(doorFeature.isExactClass('door')).toBe(true); + expect(doorFeature.isExactClass('building')).toBe(false); + expect(doorFeature.isClass('door')).toBe(true); + expect(doorFeature.isClass('doorknob')).toBe(false); + expect(doorFeature.isClass('building')).toBe(true); + expect(doorFeature.getExactClassName()).toBe('door'); + expect(doorFeature.hasProperty('door_name')).toBe(true); + expect(doorFeature.hasProperty('height')).toBe(true); + + // Includes batch table properties and hierarchy properties from all inherited classes + var expectedPropertyNames = ['height', 'area', 'door_mass', 'door_width', 'door_name', 'building_area', 'building_name', 'zone_buildings', 'zone_name']; + + // door0 has two parents - building0 and classifier_old + // building0 has two parents - zone0 and classifier_new + if (multipleParents) { + expectedPropertyNames.push('year', 'color', 'name', 'architect'); // classier_new + expectedPropertyNames.push('description', 'inspection'); // classifier_old + } + + var propertyNames = doorFeature.getPropertyNames(); + expect(expectedPropertyNames.sort()).toEqual(propertyNames.sort()); + + expect(doorFeature.getProperty('height')).toBe(5.0); // Gets generic property + expect(doorFeature.getProperty('door_name')).toBe('door0'); // Gets class property + expect(doorFeature.getProperty('building_name')).toBe('building0'); // Gets inherited property + + // Sets generic property + doorFeature.setProperty('height', 10.0); + expect(doorFeature.getProperty('height')).toBe(10.0); + + // Sets class property + doorFeature.setProperty('door_name', 'new_door'); + expect(doorFeature.getProperty('door_name')).toBe('new_door'); + expect(roofFeature.getProperty('door_name')).toBeUndefined(); + + // Throws error when setting inherited property + expect(function() { + doorFeature.setProperty('building_name', 'new_building'); + }).toThrowDeveloperError(); + + // Check properties when there is no hierarchy + batchTable._batchTableHierarchy = undefined; + expect(doorFeature.isExactClass('door')).toBe(false); + expect(doorFeature.isClass('door')).toBe(false); + expect(doorFeature.getExactClassName()).toBeUndefined(); + expect(doorFeature.hasProperty('door_name')).toBe(false); + expect(doorFeature.hasProperty('height')).toBe(true); + expect(doorFeature.getPropertyNames()).toEqual(['height', 'area']); + expect(doorFeature.getProperty('height')).toBe(10.0); + expect(doorFeature.getProperty('door_name')).toBeUndefined(); + expect(doorFeature.getProperty('building_name')).toBeUndefined(); + batchTable._batchTableHierarchy = hierarchy; + } + + function checkHierarchyPropertiesNoParents(tileset) { + // Check isExactClass, isClass, and getExactClassName in Cesium3DTileFeature + var content = tileset._root.content; + var doorFeature = content.getFeature(4); + expect(doorFeature.isExactClass('door')).toBe(true); + expect(doorFeature.isExactClass('doorknob')).toBe(false); + expect(doorFeature.isClass('door')).toBe(true); + expect(doorFeature.isClass('doorknob')).toBe(false); + expect(doorFeature.getExactClassName()).toBe('door'); + expect(doorFeature.hasProperty('door_name')).toBe(true); + expect(doorFeature.hasProperty('height')).toBe(true); + + // Includes batch table properties and hierarchy properties from all inherited classes + var expectedPropertyNames = ['height', 'area', 'door_mass', 'door_width', 'door_name']; + + var propertyNames = doorFeature.getPropertyNames(); + expect(expectedPropertyNames.sort()).toEqual(propertyNames.sort()); + + expect(doorFeature.getProperty('height')).toBe(5.0); // Gets generic property + expect(doorFeature.getProperty('door_name')).toBe('door0'); // Gets class property + + // Sets generic property + doorFeature.setProperty('height', 10.0); + expect(doorFeature.getProperty('height')).toBe(10.0); + + // Sets class property + doorFeature.setProperty('door_name', 'new_door'); + expect(doorFeature.getProperty('door_name')).toBe('new_door'); + } + + function checkBatchTableHierarchy(url, multipleParents) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + checkHierarchyStyling(tileset); + checkHierarchyProperties(tileset, multipleParents); + }); + } + + function checkBatchTableHierarchyNoParents(url) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + checkHierarchyStylingNoParents(tileset); + checkHierarchyPropertiesNoParents(tileset); + }); + } + + it('renders tileset with batch table hierarchy', function() { + return checkBatchTableHierarchy(batchTableHierarchyUrl, false); + }); + + it('renders tileset with batch table hierarchy using binary properties', function() { + return checkBatchTableHierarchy(batchTableHierarchyBinaryUrl, true); + }); + + it('renders tileset with batch table hierarchy with multiple parent classes', function() { + return checkBatchTableHierarchy(batchTableHierarchyMultipleParentsUrl, true); + }); + + it('renders tileset with batch table hierarchy with no parents', function() { + return checkBatchTableHierarchyNoParents(batchTableHierarchyNoParentsUrl); + }); + + it('validates hierarchy with multiple parents', function() { + // building0 + // / \ + // door0 door1 + // \ / + // window0 + var batchTableJson = { + HIERARCHY : { + instancesLength : 4, + classIds : [0, 1, 1, 2], + parentCounts : [2, 1, 1, 0], + parentIds : [1, 2, 3, 3], + classes : [{ + name : 'window', + length : 1, + instances : { + window_name : ['window0'] + } + }, { + name : 'door', + length : 2, + instances : { + door_name : ['door0', 'door1'] + } + }, { + name : 'building', + length : 1, + instances : { + building_name : ['building0'] + } + }] + } + }; + var batchTable = new Cesium3DTileBatchTable(mockTileset, 4, batchTableJson); + expect(batchTable.getPropertyNames(0).sort()).toEqual(['building_name', 'door_name', 'window_name']); + }); + + it('validates hierarchy with multiple parents (2)', function() { + // zone + // / | \ + // building0 | \ + // / \ | \ + // door0 door1 / + // \ | / + // window0 + var batchTableJson = { + HIERARCHY : { + instancesLength : 4, + classIds : [0, 1, 1, 2, 3], + parentCounts : [3, 1, 2, 1, 0], + parentIds : [1, 2, 4, 3, 3, 4, 4], + classes : [{ + name : 'window', + length : 1, + instances : { + window_name : ['window0'] + } + }, { + name : 'door', + length : 2, + instances : { + door_name : ['door0', 'door1'] + } + }, { + name : 'building', + length : 1, + instances : { + building_name : ['building0'] + } + }, { + name : 'zone', + length : 1, + instances : { + zone_name : ['zone0'] + } + }] + } + }; + var batchTable = new Cesium3DTileBatchTable(mockTileset, 5, batchTableJson); + expect(batchTable.getPropertyNames(0).sort()).toEqual(['building_name', 'door_name', 'window_name', 'zone_name']); // check window + expect(batchTable.hasProperty(1, 'zone_name')).toEqual(true); // check door0 + expect(batchTable.hasProperty(2, 'zone_name')).toEqual(true); // check door1 + }); + + //>>includeStart('debug', pragmas.debug); + // Circular dependencies are only caught in debug builds. + it('throws if hierarchy has a circular dependency', function() { + // window0 -> door0 -> building0 -> window0 + var batchTableJson = { + HIERARCHY : { + instancesLength : 3, + classIds : [0, 1, 2], + parentIds : [1, 2, 0], + classes : [{ + name : 'window', + length : 1, + instances : { + window_name : ['window0'] + } + }, { + name : 'door', + length : 1, + instances : { + door_name : ['door0'] + } + }, { + name : 'building', + length : 1, + instances : { + building_name : ['building0'] + } + }] + } + }; + expect(function() { + return new Cesium3DTileBatchTable(mockTileset, 3, batchTableJson); + }).toThrowDeveloperError(); + }); + + it('throws if hierarchy has a circular dependency (2)', function() { + // window0 -> door0 -> building0 -> window1 -> door0 + var batchTableJson = { + HIERARCHY : { + instancesLength : 4, + classIds : [0, 1, 2, 0], + parentIds : [1, 2, 3, 1], + classes : [{ + name : 'window', + length : 2, + instances : { + window_name : ['window0', 'window1'] + } + }, { + name : 'door', + length : 1, + instances : { + door_name : ['door0'] + } + }, { + name : 'building', + length : 1, + instances : { + building_name : ['building0'] + } + }] + } + }; + expect(function() { + return new Cesium3DTileBatchTable(mockTileset, 4, batchTableJson); + }).toThrowDeveloperError(); + }); + //>>includeEnd('debug'); + + it('throws if an instance\'s parentId exceeds instancesLength', function() { + var batchTableJson = { + HIERARCHY : { + instancesLength : 2, + classIds : [0, 1], + parentIds : [1, 2], + classes : [{ + name : 'window', + length : 1, + instances : { + window_name : ['window0'] + } + }, { + name : 'door', + length : 1, + instances : { + door_name : ['door0'] + } + }] + } + }; + expect(function() { + return new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson); + }).toThrowDeveloperError(); + }); + + it('destroys', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + var batchTable = content.batchTable; + expect(batchTable.isDestroyed()).toEqual(false); + scene.primitives.remove(tileset); + expect(batchTable.isDestroyed()).toEqual(true); + }); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/Cesium3DTileContentSpec.js b/Specs/Scene/Cesium3DTileContentSpec.js new file mode 100644 index 000000000000..28b8d03e909f --- /dev/null +++ b/Specs/Scene/Cesium3DTileContentSpec.js @@ -0,0 +1,68 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Cesium3DTileContent' + ], function( + Cesium3DTileContent) { + 'use strict'; + + it('throws', function() { + var content = new Cesium3DTileContent(); + expect(function() { + return content.featuresLength; + }).toThrowDeveloperError(); + expect(function() { + return content.pointsLength; + }).toThrowDeveloperError(); + expect(function() { + return content.trianglesLength; + }).toThrowDeveloperError(); + expect(function() { + return content.geometryByteLength; + }).toThrowDeveloperError(); + expect(function() { + return content.texturesByteLength; + }).toThrowDeveloperError(); + expect(function() { + return content.batchTableByteLength; + }).toThrowDeveloperError(); + expect(function() { + return content.innerContents; + }).toThrowDeveloperError(); + expect(function() { + return content.readyPromise; + }).toThrowDeveloperError(); + expect(function() { + return content.tileset; + }).toThrowDeveloperError(); + expect(function() { + return content.tile; + }).toThrowDeveloperError(); + expect(function() { + return content.url; + }).toThrowDeveloperError(); + expect(function() { + return content.batchTable; + }).toThrowDeveloperError(); + expect(function() { + return content.hasProperty(0, 'height'); + }).toThrowDeveloperError(); + expect(function() { + return content.getFeature(0); + }).toThrowDeveloperError(); + expect(function() { + content.applyDebugSettings(); + }).toThrowDeveloperError(); + expect(function() { + content.applyStyle(); + }).toThrowDeveloperError(); + expect(function() { + content.update(); + }).toThrowDeveloperError(); + expect(function() { + content.isDestroyed(); + }).toThrowDeveloperError(); + expect(function() { + content.destroy(); + }).toThrowDeveloperError(); + }); +}); diff --git a/Specs/Scene/Cesium3DTileFeatureTableSpec.js b/Specs/Scene/Cesium3DTileFeatureTableSpec.js new file mode 100644 index 000000000000..200ba7a5b546 --- /dev/null +++ b/Specs/Scene/Cesium3DTileFeatureTableSpec.js @@ -0,0 +1,37 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Cesium3DTileFeatureTable', + 'Core/ComponentDatatype' + ], function( + Cesium3DTileFeatureTable, + ComponentDatatype) { + 'use strict'; + + it('loads from JSON', function() { + var featureTable = new Cesium3DTileFeatureTable({ + TEST : [0, 1, 2, 3, 4, 5] + }); + featureTable.featuresLength = 3; + var all = featureTable.getGlobalProperty('TEST', ComponentDatatype.UNSIGNED_BYTE); + expect(all).toEqual([0, 1, 2, 3, 4, 5]); + var feature = featureTable.getProperty('TEST', ComponentDatatype.UNSIGNED_BYTE, 2, 1, new Array(2)); + expect(feature).toEqual([2, 3]); + var properties = featureTable.getPropertyArray('TEST', ComponentDatatype.UNSIGNED_BYTE, 2); + expect(properties).toEqual([0, 1, 2, 3, 4, 5]); + }); + + it('loads from binary', function() { + var featureTable = new Cesium3DTileFeatureTable({ + TEST : { + byteOffset : 4 + } + }, new Uint8Array([0, 0, 0, 0, 0, 1, 2, 3, 4, 5])); + featureTable.featuresLength = 3; + var all = featureTable.getGlobalProperty('TEST', ComponentDatatype.UNSIGNED_BYTE, 6); + expect(all).toEqual([0, 1, 2, 3, 4, 5]); + var feature = featureTable.getProperty('TEST', ComponentDatatype.UNSIGNED_BYTE, 2, 1, new Array(2)); + expect(feature).toEqual([2, 3]); + var properties = featureTable.getPropertyArray('TEST', ComponentDatatype.UNSIGNED_BYTE, 2); + expect(properties).toEqual([0, 1, 2, 3, 4, 5]); + }); +}); diff --git a/Specs/Scene/Cesium3DTileSpec.js b/Specs/Scene/Cesium3DTileSpec.js new file mode 100644 index 000000000000..23b03bdb313e --- /dev/null +++ b/Specs/Scene/Cesium3DTileSpec.js @@ -0,0 +1,330 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Cesium3DTile', + 'Core/Cartesian3', + 'Core/clone', + 'Core/defined', + 'Core/HeadingPitchRoll', + 'Core/loadWithXhr', + 'Core/Math', + 'Core/Matrix3', + 'Core/Matrix4', + 'Core/Rectangle', + 'Core/SphereOutlineGeometry', + 'Core/Transforms', + 'Scene/TileBoundingRegion', + 'Scene/TileOrientedBoundingBox', + 'Specs/createScene' + ], function( + Cesium3DTile, + Cartesian3, + clone, + defined, + HeadingPitchRoll, + loadWithXhr, + CesiumMath, + Matrix3, + Matrix4, + Rectangle, + SphereOutlineGeometry, + Transforms, + TileBoundingRegion, + TileOrientedBoundingBox, + createScene) { + 'use strict'; + + var tileWithBoundingSphere = { + geometricError : 1, + refine : 'replace', + children : [], + boundingVolume : { + sphere: [0.0, 0.0, 0.0, 5.0] + } + }; + + var tileWithContentBoundingSphere = { + geometricError : 1, + refine : 'replace', + content : { + url : '0/0.b3dm', + boundingVolume : { + sphere: [0.0, 0.0, 1.0, 5.0] + } + }, + children : [], + boundingVolume : { + sphere: [0.0, 0.0, 1.0, 5.0] + } + }; + + var tileWithBoundingRegion = { + geometricError : 1, + refine : 'replace', + children : [], + boundingVolume: { + region : [-1.2, -1.2, 0.0, 0.0, -30, -34] + } + }; + + var tileWithContentBoundingRegion = { + geometricError : 1, + refine : 'replace', + children : [], + content : { + url : '0/0.b3dm', + boundingVolume : { + region : [-1.2, -1.2, 0, 0, -30, -34] + } + }, + boundingVolume: { + region : [-1.2, -1.2, 0, 0, -30, -34] + } + }; + + var tileWithBoundingBox = { + geometricError : 1, + refine : 'replace', + children : [], + boundingVolume: { + box : [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] + } + }; + + var tileWithContentBoundingBox = { + geometricError : 1, + refine : 'replace', + children : [], + content : { + url : '0/0.b3dm', + boundingVolume : { + box : [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0] + } + }, + boundingVolume: { + box : [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0] + } + }; + + var tileWithViewerRequestVolume = { + geometricError : 1, + refine : 'replace', + children : [], + boundingVolume: { + box : [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0] + }, + viewerRequestVolume : { + box : [0.0, 0.0, 1.0, 2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 2.0] + } + }; + + var mockTileset = { + debugShowBoundingVolume : true, + debugShowViewerRequestVolume : true, + modelMatrix : Matrix4.IDENTITY + }; + + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + function getTileTransform(longitude, latitude) { + var transformCenter = Cartesian3.fromRadians(longitude, latitude, 0.0); + var hpr = new HeadingPitchRoll(); + var transformMatrix = Transforms.headingPitchRollToFixedFrame(transformCenter, hpr); + return Matrix4.pack(transformMatrix, new Array(16)); + } + + it('destroys', function() { + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithBoundingSphere, undefined); + expect(tile.isDestroyed()).toEqual(false); + tile.destroy(); + expect(tile.isDestroyed()).toEqual(true); + }); + + it('throws if geometricError is undefined', function() { + var tileWithoutGeometricError = clone(tileWithBoundingSphere, true); + delete tileWithoutGeometricError.geometricError; + expect(function() { + return new Cesium3DTile(mockTileset, '/some_url', tileWithoutGeometricError, undefined); + }).toThrowRuntimeError(); + }); + + it('throws if boundingVolume is undefined', function() { + var tileWithoutBoundingVolume = clone(tileWithBoundingSphere, true); + delete tileWithoutBoundingVolume.boundingVolume; + expect(function() { + return new Cesium3DTile(mockTileset, '/some_url', tileWithoutBoundingVolume, undefined); + }).toThrowRuntimeError(); + }); + + it('throws if boundingVolume does not contain a sphere, region, or box', function() { + var tileWithoutBoundingVolume = clone(tileWithBoundingSphere, true); + delete tileWithoutBoundingVolume.boundingVolume.sphere; + expect(function() { + return new Cesium3DTile(mockTileset, '/some_url', tileWithoutBoundingVolume, undefined); + }).toThrowRuntimeError(); + }); + + describe('bounding volumes', function() { + it('can have a bounding sphere', function() { + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithBoundingSphere, undefined); + var radius = tileWithBoundingSphere.boundingVolume.sphere[3]; + expect(tile.contentBoundingVolume).toBeDefined(); + expect(tile.contentBoundingVolume.boundingVolume.radius).toEqual(radius); + expect(tile.contentBoundingVolume.boundingVolume.center).toEqual(Cartesian3.ZERO); + }); + + it('can have a content bounding sphere', function() { + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithContentBoundingSphere, undefined); + var radius = tileWithContentBoundingSphere.content.boundingVolume.sphere[3]; + expect(tile.contentBoundingVolume).toBeDefined(); + expect(tile.contentBoundingVolume.boundingVolume.radius).toEqual(radius); + expect(tile.contentBoundingVolume.boundingVolume.center).toEqual(new Cartesian3(0.0, 0.0, 1.0)); + }); + + it('can have a bounding region', function() { + var box = tileWithBoundingRegion.boundingVolume.region; + var rectangle = new Rectangle(box[0], box[1], box[2], box[3]); + var minimumHeight = tileWithBoundingRegion.boundingVolume.region[4]; + var maximumHeight = tileWithBoundingRegion.boundingVolume.region[5]; + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithBoundingRegion, undefined); + var tbr = new TileBoundingRegion({rectangle: rectangle, minimumHeight: minimumHeight, maximumHeight: maximumHeight}); + expect(tile.contentBoundingVolume).toBeDefined(); + expect(tile.contentBoundingVolume).toEqual(tbr); + }); + + it('can have a content bounding region', function() { + var region = tileWithContentBoundingRegion.content.boundingVolume.region; + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithContentBoundingRegion, undefined); + expect(tile._contentBoundingVolume).toBeDefined(); + var tbb = new TileBoundingRegion({ + rectangle: new Rectangle(region[0], region[1], region[2], region[3]), + minimumHeight: region[4], + maximumHeight: region[5] + }); + expect(tile._contentBoundingVolume).toEqual(tbb); + }); + + it('can have an oriented bounding box', function() { + var box = tileWithBoundingBox.boundingVolume.box; + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithBoundingBox, undefined); + expect(tile.contentBoundingVolume).toBeDefined(); + var center = new Cartesian3(box[0], box[1], box[2]); + var halfAxes = Matrix3.fromArray(box, 3); + var obb = new TileOrientedBoundingBox(center, halfAxes); + expect(tile.contentBoundingVolume).toEqual(obb); + }); + + it('can have a content oriented bounding box', function() { + var box = tileWithContentBoundingBox.boundingVolume.box; + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithContentBoundingBox, undefined); + expect(tile.contentBoundingVolume).toBeDefined(); + var center = new Cartesian3(box[0], box[1], box[2]); + var halfAxes = Matrix3.fromArray(box, 3); + var obb = new TileOrientedBoundingBox(center, halfAxes); + expect(tile.contentBoundingVolume).toEqual(obb); + }); + + it('tile transform affects bounding sphere', function() { + var header = clone(tileWithContentBoundingSphere, true); + header.transform = getTileTransform(centerLongitude, centerLatitude); + var tile = new Cesium3DTile(mockTileset, '/some_url', header, undefined); + var boundingSphere = tile._boundingVolume.boundingVolume; + var contentBoundingSphere = tile._contentBoundingVolume.boundingVolume; + + var boundingVolumeCenter = Cartesian3.fromRadians(centerLongitude, centerLatitude, 1.0); + expect(boundingSphere.center).toEqualEpsilon(boundingVolumeCenter, CesiumMath.EPSILON4); + expect(boundingSphere.radius).toEqual(5.0); // No change + + expect(contentBoundingSphere.center).toEqualEpsilon(boundingVolumeCenter, CesiumMath.EPSILON4); + expect(contentBoundingSphere.radius).toEqual(5.0); // No change + }); + + it('tile transform affects oriented bounding box', function() { + var header = clone(tileWithContentBoundingBox, true); + header.transform = getTileTransform(centerLongitude, centerLatitude); + var tile = new Cesium3DTile(mockTileset, '/some_url', header, undefined); + var boundingBox = tile._boundingVolume.boundingVolume; + var contentBoundingBox = tile._contentBoundingVolume.boundingVolume; + + var boundingVolumeCenter = Cartesian3.fromRadians(centerLongitude, centerLatitude, 1.0); + expect(boundingBox.center).toEqualEpsilon(boundingVolumeCenter, CesiumMath.EPSILON7); + expect(contentBoundingBox.center).toEqualEpsilon(boundingVolumeCenter, CesiumMath.EPSILON7); + }); + + it('tile transform does not affect bounding region', function() { + var header = clone(tileWithContentBoundingRegion, true); + header.transform = getTileTransform(centerLongitude, centerLatitude); + var tile = new Cesium3DTile(mockTileset, '/some_url', header, undefined); + var boundingRegion = tile._boundingVolume; + var contentBoundingRegion = tile._contentBoundingVolume; + + var region = header.boundingVolume.region; + var rectangle = Rectangle.unpack(region); + expect(boundingRegion.rectangle).toEqual(rectangle); + expect(contentBoundingRegion.rectangle).toEqual(rectangle); + }); + + it('tile transform affects viewer request volume', function() { + var header = clone(tileWithViewerRequestVolume, true); + header.transform = getTileTransform(centerLongitude, centerLatitude); + var tile = new Cesium3DTile(mockTileset, '/some_url', header, undefined); + var requestVolume = tile._viewerRequestVolume.boundingVolume; + var requestVolumeCenter = Cartesian3.fromRadians(centerLongitude, centerLatitude, 1.0); + expect(requestVolume.center).toEqualEpsilon(requestVolumeCenter, CesiumMath.EPSILON7); + }); + + it('tile transform changes', function() { + var mockTileset = { + modelMatrix : Matrix4.IDENTITY + }; + var header = clone(tileWithBoundingSphere, true); + header.transform = getTileTransform(centerLongitude, centerLatitude); + var tile = new Cesium3DTile(mockTileset, '/some_url', header, undefined); + var boundingSphere = tile._boundingVolume.boundingVolume; + + // Check the original transform + var boundingVolumeCenter = Cartesian3.fromRadians(centerLongitude, centerLatitude); + expect(boundingSphere.center).toEqualEpsilon(boundingVolumeCenter, CesiumMath.EPSILON7); + + // Change the transform + var newLongitude = -1.012; + var newLatitude = 0.698874; + tile.transform = getTileTransform(newLongitude, newLatitude); + tile.updateTransform(); + + // Check the new transform + var newCenter = Cartesian3.fromRadians(newLongitude, newLatitude); + expect(boundingSphere.center).toEqualEpsilon(newCenter, CesiumMath.EPSILON7); + }); + }); + + describe('debug bounding volumes', function() { + it('can be a bounding region', function() { + var scene = createScene(); + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithBoundingRegion, undefined); + tile.update(mockTileset, scene.frameState); + expect(tile._debugBoundingVolume).toBeDefined(); + }); + + it('can be an oriented bounding box', function() { + var scene = createScene(); + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithBoundingBox, undefined); + tile.update(mockTileset, scene.frameState); + expect(tile._debugBoundingVolume).toBeDefined(); + }); + + it('can be a bounding sphere', function() { + var scene = createScene(); + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithBoundingSphere, undefined); + tile.update(mockTileset, scene.frameState); + expect(tile._debugBoundingVolume).toBeDefined(); + }); + + it('creates debug bounding volume for viewer request volume', function() { + var scene = createScene(); + var tile = new Cesium3DTile(mockTileset, '/some_url', tileWithViewerRequestVolume, undefined); + tile.update(mockTileset, scene.frameState); + expect(tile._debugViewerRequestVolume).toBeDefined(); + }); + }); +}, 'WebGL'); diff --git a/Specs/Scene/Cesium3DTileStyleSpec.js b/Specs/Scene/Cesium3DTileStyleSpec.js new file mode 100644 index 000000000000..e6ca079fe116 --- /dev/null +++ b/Specs/Scene/Cesium3DTileStyleSpec.js @@ -0,0 +1,498 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Cesium3DTileStyle', + 'Core/Color', + 'Scene/ConditionsExpression', + 'Scene/Expression' + ], function( + Cesium3DTileStyle, + Color, + ConditionsExpression, + Expression) { + 'use strict'; + + var frameState = {}; + + function MockFeature() { + this._properties = {}; + } + + MockFeature.prototype.addProperty = function(name, value) { + this._properties[name] = value; + }; + + MockFeature.prototype.getProperty = function(name) { + return this._properties[name]; + }; + + var feature1 = new MockFeature(); + feature1.addProperty('ZipCode', '19341'); + feature1.addProperty('County', 'Chester'); + feature1.addProperty('YearBuilt', 1979); + feature1.addProperty('Temperature', 78); + feature1.addProperty('red', 38); + feature1.addProperty('green', 255); + feature1.addProperty('blue', 82); + feature1.addProperty('volume', 128); + feature1.addProperty('Height', 100); + feature1.addProperty('Width', 20); + feature1.addProperty('Depth', 20); + feature1.addProperty('id', 11); + feature1.addProperty('name', 'Hello'); + + var feature2 = new MockFeature(); + feature2.addProperty('ZipCode', '19342'); + feature2.addProperty('County', 'Delaware'); + feature2.addProperty('YearBuilt', 1979); + feature2.addProperty('Temperature', 92); + feature2.addProperty('red', 255); + feature2.addProperty('green', 30); + feature2.addProperty('blue', 30); + feature2.addProperty('volume', 50); + feature2.addProperty('Height', 38); + feature2.addProperty('id', 12); + + var styleUrl = './Data/Cesium3DTiles/Style/style.json'; + + it('rejects readyPromise with undefined url', function() { + var tileStyle = new Cesium3DTileStyle('invalid.json'); + + return tileStyle.readyPromise.then(function(style) { + fail('should not resolve'); + }).otherwise(function(error) { + expect(tileStyle.ready).toEqual(false); + expect(error.statusCode).toEqual(404); + }); + }); + + it('loads style from uri', function() { + var tileStyle = new Cesium3DTileStyle(styleUrl); + + return tileStyle.readyPromise.then(function(style) { + expect(style.style).toEqual({ + show : '${id} < 100', + color : "color('red')", + pointSize : '${id} / 100' + }); + expect(style.show).toEqual(new Expression('${id} < 100')); + expect(style.color).toEqual(new Expression("color('red')")); + expect(style.pointSize).toEqual(new Expression('${id} / 100')); + expect(tileStyle.ready).toEqual(true); + }).otherwise(function() { + fail('should load style.json'); + }); + }); + + it('sets show value to default expression', function() { + var style = new Cesium3DTileStyle({}); + expect(style.show).toEqual(new Expression('true')); + + style = new Cesium3DTileStyle(); + expect(style.show).toEqual(new Expression('true')); + }); + + it('sets color value to default expression', function() { + var style = new Cesium3DTileStyle({}); + expect(style.color).toEqual(new Expression('color("#ffffff")')); + + style = new Cesium3DTileStyle(); + expect(style.color).toEqual(new Expression('color("#ffffff")')); + }); + + it('sets pointSize value to default expression', function() { + var style = new Cesium3DTileStyle({}); + expect(style.pointSize).toEqual(new Expression('1')); + + style = new Cesium3DTileStyle(); + expect(style.pointSize).toEqual(new Expression('1')); + }); + + it('sets show value to expression', function() { + var style = new Cesium3DTileStyle({ + show : 'true' + }); + expect(style.show).toEqual(new Expression('true')); + + style = new Cesium3DTileStyle({ + show : 'false' + }); + expect(style.show).toEqual(new Expression('false')); + + style = new Cesium3DTileStyle({ + show : '${height} * 10 >= 1000' + }); + expect(style.show).toEqual(new Expression('${height} * 10 >= 1000')); + + style = new Cesium3DTileStyle({ + show : true + }); + expect(style.show).toEqual(new Expression('true')); + + style = new Cesium3DTileStyle({ + show : false + }); + expect(style.show).toEqual(new Expression('false')); + }); + + it('sets show value to conditional', function() { + var jsonExp = { + conditions : [ + ['${height} > 2', 'false'], + ['true', 'true'] + ] + }; + + var style = new Cesium3DTileStyle({ + show : jsonExp + }); + expect(style.show).toEqual(new ConditionsExpression(jsonExp)); + }); + + it('sets show to undefined if not a string, boolean, or conditional', function() { + var style = new Cesium3DTileStyle({ + show : 1 + }); + expect(style.show).toEqual(undefined); + }); + + it('sets color value to expression', function() { + var style = new Cesium3DTileStyle({ + color : 'color("red")' + }); + expect(style.color).toEqual(new Expression('color("red")')); + + style = new Cesium3DTileStyle({ + color : 'rgba(30, 30, 30, 0.5)' + }); + expect(style.color).toEqual(new Expression('rgba(30, 30, 30, 0.5)')); + + style = new Cesium3DTileStyle({ + color : '(${height} * 10 >= 1000) ? rgba(0.0, 0.0, 1.0, 0.5) : color("blue")' + }); + expect(style.color).toEqual(new Expression('(${height} * 10 >= 1000) ? rgba(0.0, 0.0, 1.0, 0.5) : color("blue")')); + }); + + it('sets color value to conditional', function() { + var jsonExp = { + conditions : [ + ['${height} > 2', 'color("cyan")'], + ['true', 'color("blue")'] + ] + }; + + var style = new Cesium3DTileStyle({ + color : jsonExp + }); + expect(style.color).toEqual(new ConditionsExpression(jsonExp)); + }); + + it('sets color to undefined if not a string or conditional', function() { + var style = new Cesium3DTileStyle({ + color : 1 + }); + expect(style.color).toEqual(undefined); + }); + + it('sets pointSize value to expression', function() { + var style = new Cesium3DTileStyle({ + pointSize : '2' + }); + expect(style.pointSize).toEqual(new Expression('2')); + + style = new Cesium3DTileStyle({ + pointSize : '${height} / 10' + }); + expect(style.pointSize).toEqual(new Expression('${height} / 10')); + + style = new Cesium3DTileStyle({ + pointSize : 2 + }); + expect(style.pointSize).toEqual(new Expression('2')); + }); + + it('sets pointSize value to conditional', function() { + var jsonExp = { + conditions : [ + ['${height} > 2', '1.0'], + ['true', '2.0'] + ] + }; + + var style = new Cesium3DTileStyle({ + pointSize : jsonExp + }); + expect(style.pointSize).toEqual(new ConditionsExpression(jsonExp)); + }); + + it('sets pointSize to undefined if not a number, string, or conditional', function() { + var style = new Cesium3DTileStyle({ + pointSize : true + }); + expect(style.pointSize).toEqual(undefined); + }); + + it('throws on accessing style if not ready', function() { + var style = new Cesium3DTileStyle({}); + style._ready = false; + + expect(function() { + return style.style; + }).toThrowDeveloperError(); + }); + + it('throws on accessing color if not ready', function() { + var style = new Cesium3DTileStyle({}); + style._ready = false; + + expect(function() { + return style.color; + }).toThrowDeveloperError(); + }); + + it('throws on accessing show if not ready', function() { + var style = new Cesium3DTileStyle({}); + style._ready = false; + + expect(function() { + return style.show; + }).toThrowDeveloperError(); + }); + + it('throws on accessing pointSize if not ready', function() { + var style = new Cesium3DTileStyle({}); + style._ready = false; + + expect(function() { + return style.pointSize; + }).toThrowDeveloperError(); + }); + + it('sets meta properties', function() { + var style = new Cesium3DTileStyle({ + meta : { + description : '"Hello, ${name}"' + } + }); + expect(style.meta.description.evaluate(frameState, feature1)).toEqual("Hello, Hello"); + + style = new Cesium3DTileStyle({ + meta : { + featureColor : 'rgb(${red}, ${green}, ${blue})', + volume : '${Height} * ${Width} * ${Depth}' + } + }); + expect(style.meta.featureColor.evaluateColor(frameState, feature1)).toEqual(Color.fromBytes(38, 255, 82)); + expect(style.meta.volume.evaluate(frameState, feature1)).toEqual(20 * 20 * 100); + }); + + it('default meta has no properties', function() { + var style = new Cesium3DTileStyle({}); + expect(style.meta).toEqual({}); + + style = new Cesium3DTileStyle({ + meta: {} + }); + expect(style.meta).toEqual({}); + }); + + it('default meta has no properties', function() { + var style = new Cesium3DTileStyle({}); + expect(style.meta).toEqual({}); + + style = new Cesium3DTileStyle({ + meta: {} + }); + expect(style.meta).toEqual({}); + }); + + it('throws on accessing meta if not ready', function() { + var style = new Cesium3DTileStyle({}); + style._ready = false; + + expect(function() { + return style.meta; + }).toThrowDeveloperError(); + }); + + // Tests for examples from the style spec + + it('applies default style', function() { + var style = new Cesium3DTileStyle({ + "show" : "true", + "color" : "color('#ffffff')", + "pointSize" : "1.0" + }); + + expect(style.show.evaluate(frameState, undefined)).toEqual(true); + expect(style.color.evaluateColor(frameState, undefined)).toEqual(Color.WHITE); + expect(style.pointSize.evaluate(frameState, undefined)).toEqual(1.0); + }); + + it('applies show style with variable', function() { + var style = new Cesium3DTileStyle({ + "show" : "${ZipCode} === '19341'" + }); + + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.show.evaluate(frameState, feature2)).toEqual(false); + expect(style.color.evaluateColor(frameState, undefined)).toEqual(Color.WHITE); + }); + + it('applies show style with regexp and variables', function() { + var style = new Cesium3DTileStyle({ + "show" : "(regExp('^Chest').test(${County})) && (${YearBuilt} >= 1970)" + }); + + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.show.evaluate(frameState, feature2)).toEqual(false); + expect(style.color.evaluateColor(frameState, undefined)).toEqual(Color.WHITE); + }); + + it('applies show style with conditional', function() { + var style = new Cesium3DTileStyle({ + "show" : { + "conditions" : [ + ["(${Height} >= 100.0)", "false"], + ["(${Height} >= 70.0)", "true"], + ["(${Height} >= 50.0)", "false"], + ["(${Height} >= 30.0)", "true"], + ["(${Height} >= 10.0)", "false"], + ["(${Height} >= 1.0)", "true"] + ] + } + }); + expect(style.show.evaluate(frameState, feature1)).toEqual(false); + expect(style.show.evaluate(frameState, feature2)).toEqual(true); + }); + + it('applies color style variables', function() { + var style = new Cesium3DTileStyle({ + "color" : "(${Temperature} > 90) ? color('red') : color('white')" + }); + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.color.evaluateColor(frameState, feature1)).toEqual(Color.WHITE); + expect(style.color.evaluateColor(frameState, feature2)).toEqual(Color.RED); + }); + + it('applies color style with new color', function() { + var style = new Cesium3DTileStyle({ + "color" : "rgba(${red}, ${green}, ${blue}, (${volume} > 100 ? 0.5 : 1.0))" + }); + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.color.evaluateColor(frameState, feature1)).toEqual(new Color(38/255, 255/255, 82/255, 0.5)); + expect(style.color.evaluateColor(frameState, feature2)).toEqual(new Color(255/255, 30/255, 30/255, 1.0)); + }); + + it('applies color style that maps id to color', function() { + var style = new Cesium3DTileStyle({ + "expressions" : { + "id" : "regExp('^1(\\d)').exec(String(${id}))" + }, + "color" : { + "conditions" : [ + ["${id} === '1'", "color('#FF0000')"], + ["${id} === '2'", "color('#00FF00')"], + ["true", "color('#FFFFFF')"] + ] + } + }); + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.color.evaluateColor(frameState, feature1)).toEqual(Color.RED); + expect(style.color.evaluateColor(frameState, feature2)).toEqual(Color.LIME); + }); + + it('applies color style with conditional', function() { + var style = new Cesium3DTileStyle({ + "color" : { + "conditions" : [ + ["(${Height} >= 100.0)", "color('#0000FF')"], + ["(${Height} >= 70.0)", "color('#00FFFF')"], + ["(${Height} >= 50.0)", "color('#00FF00')"], + ["(${Height} >= 30.0)", "color('#FFFF00')"], + ["(${Height} >= 10.0)", "color('#FF0000')"], + ["(${Height} >= 1.0)", "color('#FF00FF')"] + ] + } + }); + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.color.evaluateColor(frameState, feature1)).toEqual(Color.BLUE); + expect(style.color.evaluateColor(frameState, feature2)).toEqual(Color.YELLOW); + }); + + it('applies pointSize style with variable', function() { + var style = new Cesium3DTileStyle({ + "pointSize" : "${Temperature} / 10.0" + }); + + expect(style.pointSize.evaluate(frameState, feature1)).toEqual(7.8); + expect(style.pointSize.evaluate(frameState, feature2)).toEqual(9.2); + }); + + it('applies pointSize style with regexp and variables', function() { + var style = new Cesium3DTileStyle({ + "pointSize" : "(regExp('^Chest').test(${County})) ? 2.0 : 1.0" + }); + + expect(style.pointSize.evaluate(frameState, feature1)).toEqual(2.0); + expect(style.pointSize.evaluate(frameState, feature2)).toEqual(1.0); + }); + + it('applies pointSize style with conditional', function() { + var style = new Cesium3DTileStyle({ + "pointSize" : { + "conditions" : [ + ["(${Height} >= 100.0)", "6"], + ["(${Height} >= 70.0)", "5"], + ["(${Height} >= 50.0)", "4"], + ["(${Height} >= 30.0)", "3"], + ["(${Height} >= 10.0)", "2"], + ["(${Height} >= 1.0)", "1"] + ] + } + }); + expect(style.pointSize.evaluate(frameState, feature1)).toEqual(6); + expect(style.pointSize.evaluate(frameState, feature2)).toEqual(3); + }); + + it('applies with additional expressions', function() { + var style = new Cesium3DTileStyle({ + "expressions" : { + "halfHeight" : "${Height} / 2", + "quarterHeight" : "${Height} / 4", + "halfVolume" : "${volume} / 2" + }, + "color" : { + "conditions" : [ + ["(${halfHeight} >= 25.0)", "color('red')"], + ["(${Height} >= 1.0)", "color('blue')"] + ] + }, + "show" : "(${quarterHeight} >= 20.0)", + "pointSize" : "${halfVolume} + ${halfHeight}", + "meta" : { + "description" : "'Half height is ' + ${halfHeight}" + } + }); + + expect(style.color.evaluateColor(frameState, feature1)).toEqual(Color.RED); + expect(style.color.evaluateColor(frameState, feature2)).toEqual(Color.BLUE); + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.show.evaluate(frameState, feature2)).toEqual(false); + expect(style.pointSize.evaluate(frameState, feature1)).toEqual(114); + expect(style.pointSize.evaluate(frameState, feature2)).toEqual(44); + expect(style.meta.description.evaluate(frameState, feature1)).toEqual('Half height is 50'); + expect(style.meta.description.evaluate(frameState, feature2)).toEqual('Half height is 19'); + }); + + it('return undefined shader functions when the style is empty', function() { + // The default color style is white, the default show style is true, and the default pointSize is 1.0, + // but the generated generated shader functions should just be undefined. We don't want all the points to be white. + var style = new Cesium3DTileStyle({}); + var colorFunction = style.getColorShaderFunction('getColor', '', {}); + var showFunction = style.getShowShaderFunction('getShow', '', {}); + var pointSizeFunction = style.getPointSizeShaderFunction('getPointSize', '', {}); + expect(colorFunction).toBeUndefined(); + expect(showFunction).toBeUndefined(); + expect(pointSizeFunction).toBeUndefined(); + }); +}); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js new file mode 100644 index 000000000000..d4fb9300048d --- /dev/null +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -0,0 +1,2719 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Cesium3DTileset', + 'Core/Cartesian3', + 'Core/Color', + 'Core/defaultValue', + 'Core/defined', + 'Core/getStringFromTypedArray', + 'Core/HeadingPitchRange', + 'Core/JulianDate', + 'Core/loadWithXhr', + 'Core/Math', + 'Core/Matrix4', + 'Core/PrimitiveType', + 'Core/RequestScheduler', + 'Renderer/ClearCommand', + 'Renderer/ContextLimits', + 'Scene/Cesium3DTile', + 'Scene/Cesium3DTileColorBlendMode', + 'Scene/Cesium3DTileContentState', + 'Scene/Cesium3DTileOptimizations', + 'Scene/Cesium3DTileRefine', + 'Scene/Cesium3DTileStyle', + 'Scene/CullFace', + 'Scene/CullingVolume', + 'Scene/PerspectiveFrustum', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene', + 'Specs/pollToPromise', + 'ThirdParty/when' + ], function( + Cesium3DTileset, + Cartesian3, + Color, + defaultValue, + defined, + getStringFromTypedArray, + HeadingPitchRange, + JulianDate, + loadWithXhr, + CesiumMath, + Matrix4, + PrimitiveType, + RequestScheduler, + ClearCommand, + ContextLimits, + Cesium3DTile, + Cesium3DTileColorBlendMode, + Cesium3DTileContentState, + Cesium3DTileOptimizations, + Cesium3DTileRefine, + Cesium3DTileStyle, + CullFace, + CullingVolume, + PerspectiveFrustum, + Cesium3DTilesTester, + createScene, + pollToPromise, + when) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + // Parent tile with content and four child tiles with content + var tilesetUrl = './Data/Cesium3DTiles/Tilesets/Tileset/'; + + // Parent tile with no content and four child tiles with content + var tilesetEmptyRootUrl = './Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/'; + + var tilesetReplacement1Url = './Data/Cesium3DTiles/Tilesets/TilesetReplacement1/'; + var tilesetReplacement2Url = './Data/Cesium3DTiles/Tilesets/TilesetReplacement2/'; + var tilesetReplacement3Url = './Data/Cesium3DTiles/Tilesets/TilesetReplacement3/'; + + // 3 level tree with mix of additive and replacement refinement + var tilesetRefinementMix = './Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/'; + + // tileset.json : root content points to tiles2.json + // tiles2.json: root with b3dm content, three children with b3dm content, one child points to tiles3.json + // tiles3.json: root with b3dm content + var tilesetOfTilesetsUrl = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/'; + + var withoutBatchTableUrl = './Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/'; + var withBatchTableUrl = './Data/Cesium3DTiles/Batched/BatchedWithBatchTable/'; + var noBatchIdsUrl = './Data/Cesium3DTiles/Batched/BatchedNoBatchIds/'; + + var withTransformBoxUrl = './Data/Cesium3DTiles/Batched/BatchedWithTransformBox/'; + var withTransformSphereUrl = './Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/'; + var withTransformRegionUrl = './Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/'; + var withBoundingSphereUrl = './Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/'; + + var compositeUrl = './Data/Cesium3DTiles/Composite/Composite/'; + var instancedUrl = './Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/'; + var instancedRedMaterialUrl = './Data/Cesium3DTiles/Instanced/InstancedRedMaterial'; + + // 1 tile where each feature is a different source color + var colorsUrl = './Data/Cesium3DTiles/Batched/BatchedColors/'; + + // 1 tile where each feature has a reddish texture + var texturedUrl = './Data/Cesium3DTiles/Batched/BatchedTextured/'; + + // 1 tile with translucent features + var translucentUrl = './Data/Cesium3DTiles/Batched/BatchedTranslucent/'; + + // 1 tile with opaque and translucent features + var translucentOpaqueMixUrl = './Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/'; + + // Root tile is transformed from local space to wgs84, child tile is rotated, scaled, and translated locally + var tilesetWithTransformsUrl = './Data/Cesium3DTiles/Tilesets/TilesetWithTransforms'; + + // Root tile with 4 b3dm children and 1 pnts child with a viewer request volume + var tilesetWithViewerRequestVolumeUrl = './Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume'; + + // Parent tile with content and four child tiles with content with viewer request volume for each child + var tilesetReplacementWithViewerRequestVolumeUrl = './Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume'; + + var tilesetWithExternalResourcesUrl = './Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources'; + var tilesetSubtreeExpirationUrl = './Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration'; + var tilesetSubtreeUrl = './Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json'; + var batchedExpirationUrl = './Data/Cesium3DTiles/Batched/BatchedExpiration'; + var batchedColorsB3dmUrl = './Data/Cesium3DTiles/Batched/BatchedColors/batchedColors.b3dm'; + + var styleUrl = './Data/Cesium3DTiles/Style/style.json'; + + var pointCloudUrl = './Data/Cesium3DTiles/PointCloud/PointCloudRGB'; + var pointCloudBatchedUrl = './Data/Cesium3DTiles/PointCloud/PointCloudBatched'; + + beforeAll(function() { + scene = createScene(); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + beforeEach(function() { + RequestScheduler.clearForSpecs(); + scene.morphTo3D(0.0); + + var camera = scene.camera; + camera.frustum = new PerspectiveFrustum(); + camera.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; + camera.frustum.fov = CesiumMath.toRadians(60.0); + + viewAllTiles(); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + function setZoom(distance) { + // Bird's eye view + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, distance)); + } + + function viewAllTiles() { + setZoom(15.0); + } + + function viewRootOnly() { + setZoom(100.0); + } + + function viewNothing() { + setZoom(200.0); + } + + function viewSky() { + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude, 100); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, 1.57, 10.0)); + } + + function viewBottomLeft() { + viewAllTiles(); + scene.camera.moveLeft(200.0); + scene.camera.moveDown(200.0); + } + + function viewInstances() { + setZoom(30.0); + } + + function viewPointCloud() { + setZoom(5.0); + } + + it('throws with undefined url', function() { + expect(function() { + return new Cesium3DTileset(); + }).toThrowDeveloperError(); + }); + + it('rejects readyPromise with invalid tileset.json', function() { + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + deferred.reject(); + }); + + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : 'invalid.json' + })); + return tileset.readyPromise.then(function() { + fail('should not resolve'); + }).otherwise(function(error) { + expect(tileset.ready).toEqual(false); + }); + }); + + it('rejects readyPromise with invalid tileset version', function() { + var tilesetJson = { + asset : { + version : 2.0 + } + }; + + var uri = 'data:text/plain;base64,' + btoa(JSON.stringify(tilesetJson)); + + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : uri + })); + return tileset.readyPromise.then(function() { + fail('should not resolve'); + }).otherwise(function(error) { + expect(tileset.ready).toEqual(false); + }); + }); + + it('url and tilesetUrl set up correctly given tileset.json path', function() { + var path = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json'; + var tileset = new Cesium3DTileset({ + url : path + }); + expect(tileset.url).toEqual(path); + expect(tileset._tilesetUrl).toEqual(path); + }); + + it('url and tilesetUrl set up correctly given directory without trailing slash', function() { + var path = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets'; + var tileset = new Cesium3DTileset({ + url : path + }); + expect(tileset.url).toEqual(path); + expect(tileset._tilesetUrl).toEqual(path + '/tileset.json'); + }); + + it('url and tilesetUrl set up correctly given directory with trailing slash', function() { + var path = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/'; + var tileset = new Cesium3DTileset({ + url : path + }); + expect(tileset.url).toEqual(path); + expect(tileset._tilesetUrl).toEqual(path + 'tileset.json'); + }); + + it('url and tilesetUrl set up correctly given path with query string', function() { + var path = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets'; + var param = '?param1=1¶m2=2'; + var tileset = new Cesium3DTileset({ + url : path + param + }); + expect(tileset.url).toEqual(path + param); + expect(tileset._tilesetUrl).toEqual(path + '/tileset.json' + param); + }); + + it('resolves readyPromise', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + return tileset.readyPromise.then(function(tileset) { + expect(tileset.ready).toEqual(true); + }); + }); + }); + + it('loads tileset.json', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var asset = tileset.asset; + expect(asset).toBeDefined(); + expect(asset.version).toEqual('0.0'); + expect(asset.tilesetVersion).toEqual('1.2.3'); + + var properties = tileset.properties; + expect(properties).toBeDefined(); + expect(properties.id).toBeDefined(); + expect(properties.id.minimum).toEqual(0); + expect(properties.id.maximum).toEqual(9); + + expect(tileset._geometricError).toEqual(240.0); + expect(tileset._root).toBeDefined(); + expect(tileset.url).toEqual(tilesetUrl); + }); + }); + + it('passes version in query string to tiles', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + expect(tileset._root.content._url).toEqual(tilesetUrl + 'parent.b3dm?v=1.2.3'); + }); + }); + + it('passes version in query string to all external resources', function() { + // Spy on loadWithXhr so we can verify requested urls + spyOn(loadWithXhr, 'load').and.callThrough(); + + var queryParams = '?a=1&b=boy'; + var queryParamsWithVersion = '?a=1&b=boy&v=1.2.3'; + return Cesium3DTilesTester.loadTileset(scene, tilesetWithExternalResourcesUrl + queryParams).then(function(tileset) { + var calls = loadWithXhr.load.calls.all(); + var callsLength = calls.length; + for (var i = 0; i < callsLength; ++i) { + var url = calls[0].args[0]; + if (url.indexOf(tilesetWithExternalResourcesUrl) >= 0) { + var query = url.slice(url.indexOf('?')); + if (url.indexOf('tileset.json') >= 0) { + // The initial tileset.json does not have a tileset version parameter + expect(query).toBe(queryParams); + } else { + expect(query).toBe(queryParamsWithVersion); + } + } + } + }); + }); + + it('throws when getting asset and tileset is not ready', function() { + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + expect(function() { + return tileset.asset; + }).toThrowDeveloperError(); + }); + + it('throws when getting properties and tileset is not ready', function() { + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + expect(function() { + return tileset.properties; + }).toThrowDeveloperError(); + }); + + it('requests tile with invalid magic', function() { + var invalidMagicBuffer = Cesium3DTilesTester.generateBatchedTileBuffer({ + magic : [120, 120, 120, 120] + }); + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetUrl + })); + return tileset.readyPromise.then(function(tileset) { + // Start spying after the tileset json has been loaded + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + deferred.resolve(invalidMagicBuffer); + }); + scene.renderForSpecs(); // Request root + var root = tileset._root; + return root.contentReadyPromise.then(function() { + fail('should not resolve'); + }).otherwise(function(error) { + expect(error.message).toBe('Invalid tile content.'); + expect(root._contentState).toEqual(Cesium3DTileContentState.FAILED); + }); + }); + }); + + it('handles failed tile requests', function() { + viewRootOnly(); + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetUrl + })); + return tileset.readyPromise.then(function(tileset) { + // Start spying after the tileset json has been loaded + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + deferred.reject(); + }); + scene.renderForSpecs(); // Request root + var root = tileset._root; + return root.contentReadyPromise.then(function() { + fail('should not resolve'); + }).otherwise(function(error) { + expect(root._contentState).toEqual(Cesium3DTileContentState.FAILED); + }); + }); + }); + + it('renders tileset', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + }); + }); + + it('renders tileset in CV', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + scene.morphToColumbusView(0.0); + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + }); + }); + + it('renders tileset in 2D', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + scene.morphTo2D(0.0); + tileset.maximumScreenSpaceError = 3; + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(10); + }); + }); + + it('does not render during morph', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var commandList = scene.frameState.commandList; + scene.renderForSpecs(); + expect(commandList.length).toBeGreaterThan(0); + scene.morphToColumbusView(1.0); + scene.renderForSpecs(); + expect(commandList.length).toBe(0); + }); + + }); + + it('renders tileset with empty root tile', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetEmptyRootUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(4); // Empty tile doesn't issue a command + }); + }); + + it('verify statistics', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetUrl + })); + + // Verify initial values + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(0); + expect(statistics.numberOfCommands).toEqual(0); + expect(statistics.numberOfPendingRequests).toEqual(0); + expect(statistics.numberOfTilesProcessing).toEqual(0); + + return Cesium3DTilesTester.waitForReady(scene, tileset).then(function() { + // Check that root and children are requested + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(0); + expect(statistics.numberOfPendingRequests).toEqual(5); + expect(statistics.numberOfTilesProcessing).toEqual(0); + + // Wait for all tiles to load and check that they are all visited and rendered + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfPendingRequests).toEqual(0); + expect(statistics.numberOfTilesProcessing).toEqual(0); + }); + }); + }); + + function checkPointAndFeatureCounts(tileset, features, points, triangles) { + var statistics = tileset._statistics; + + expect(statistics.numberOfFeaturesSelected).toEqual(0); + expect(statistics.numberOfFeaturesLoaded).toEqual(0); + expect(statistics.numberOfPointsSelected).toEqual(0); + expect(statistics.numberOfPointsLoaded).toEqual(0); + expect(statistics.numberOfTrianglesSelected).toEqual(0); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfFeaturesSelected).toEqual(features); + expect(statistics.numberOfFeaturesLoaded).toEqual(features); + expect(statistics.numberOfPointsSelected).toEqual(points); + expect(statistics.numberOfPointsLoaded).toEqual(points); + expect(statistics.numberOfTrianglesSelected).toEqual(triangles); + + viewNothing(); + scene.renderForSpecs(); + + expect(statistics.numberOfFeaturesSelected).toEqual(0); + expect(statistics.numberOfFeaturesLoaded).toEqual(features); + expect(statistics.numberOfPointsSelected).toEqual(0); + expect(statistics.numberOfPointsLoaded).toEqual(points); + expect(statistics.numberOfTrianglesSelected).toEqual(0); + + tileset.trimLoadedTiles(); + scene.renderForSpecs(); + + expect(statistics.numberOfFeaturesSelected).toEqual(0); + expect(statistics.numberOfFeaturesLoaded).toEqual(0); + expect(statistics.numberOfPointsSelected).toEqual(0); + expect(statistics.numberOfPointsLoaded).toEqual(0); + expect(statistics.numberOfTrianglesSelected).toEqual(0); + }); + } + + it('verify batched features statistics', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : withBatchTableUrl + })); + + return checkPointAndFeatureCounts(tileset, 10, 0, 120); + }); + + it('verify no batch table features statistics', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : noBatchIdsUrl + })); + + return checkPointAndFeatureCounts(tileset, 0, 0, 120); + }); + + it('verify instanced features statistics', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : instancedRedMaterialUrl + })); + + return checkPointAndFeatureCounts(tileset, 25, 0, 12); + }); + + it('verify composite features statistics', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : compositeUrl + })); + + return checkPointAndFeatureCounts(tileset, 35, 0, 132); + }); + + it('verify tileset of tilesets features statistics', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetOfTilesetsUrl + })); + + return checkPointAndFeatureCounts(tileset, 50, 0, 600); + }); + + it('verify points statistics', function() { + viewPointCloud(); + + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : pointCloudUrl + })); + + return checkPointAndFeatureCounts(tileset, 0, 1000, 0); + }); + + it('verify triangle statistics', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetEmptyRootUrl + })); + + return checkPointAndFeatureCounts(tileset, 40, 0, 480); + }); + + it('verify batched points statistics', function() { + viewPointCloud(); + + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : pointCloudBatchedUrl + })); + + return checkPointAndFeatureCounts(tileset, 8, 1000, 0); + }); + + it('verify memory usage statistics', function() { + // Calculations in Batched3DModel3DTilesContentSpec + var singleTileGeometryMemory = 8880; + var singleTileTextureMemory = 0; + var singleTileBatchTextureMemory = 40; + var singleTilePickTextureMemory = 40; + var tilesLength = 5; + + viewNothing(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var statistics = tileset._statistics; + + // No tiles loaded + expect(statistics.geometryByteLength).toEqual(0); + expect(statistics.texturesByteLength).toEqual(0); + expect(statistics.batchTableByteLength).toEqual(0); + + viewRootOnly(); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + // Root tile loaded + expect(statistics.geometryByteLength).toEqual(singleTileGeometryMemory); + expect(statistics.texturesByteLength).toEqual(singleTileTextureMemory); + expect(statistics.batchTableByteLength).toEqual(0); + + viewAllTiles(); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + // All tiles loaded + expect(statistics.geometryByteLength).toEqual(singleTileGeometryMemory * tilesLength); + expect(statistics.texturesByteLength).toEqual(singleTileTextureMemory * tilesLength); + expect(statistics.batchTableByteLength).toEqual(0); + + // One feature colored, the batch table memory is now higher + tileset._root.content.getFeature(0).color = Color.RED; + scene.renderForSpecs(); + expect(statistics.geometryByteLength).toEqual(singleTileGeometryMemory * tilesLength); + expect(statistics.texturesByteLength).toEqual(singleTileTextureMemory * tilesLength); + expect(statistics.batchTableByteLength).toEqual(singleTileBatchTextureMemory); + + // All tiles picked, the texture memory is now higher + scene.pickForSpecs(); + expect(statistics.geometryByteLength).toEqual(singleTileGeometryMemory * tilesLength); + expect(statistics.texturesByteLength).toEqual(singleTileTextureMemory * tilesLength); + expect(statistics.batchTableByteLength).toEqual(singleTileBatchTextureMemory + singleTilePickTextureMemory * tilesLength); + + // Tiles are still in memory when zoomed out + viewNothing(); + scene.renderForSpecs(); + expect(statistics.geometryByteLength).toEqual(singleTileGeometryMemory * tilesLength); + expect(statistics.texturesByteLength).toEqual(singleTileTextureMemory * tilesLength); + expect(statistics.batchTableByteLength).toEqual(singleTileBatchTextureMemory + singleTilePickTextureMemory * tilesLength); + + // Trim loaded tiles, expect the memory statistics to be 0 + tileset.trimLoadedTiles(); + scene.renderForSpecs(); + expect(statistics.geometryByteLength).toEqual(0); + expect(statistics.texturesByteLength).toEqual(0); + expect(statistics.batchTableByteLength).toEqual(0); + }); + }); + }); + }); + + it('verify memory usage statistics for shared resources', function() { + // Six tiles total: + // * Two b3dm tiles - no shared resources + // * Two i3dm tiles with embedded glTF - no shared resources + // * Two i3dm tiles with external glTF - shared resources + // Expect to see some saving with memory usage since two of the tiles share resources + // All tiles reference the same external texture but texture caching is not supported yet + // TODO : tweak test when #5051 is in + + var b3dmGeometryMemory = 840; // Only one box in the tile, unlike most other test tiles + var i3dmGeometryMemory = 840; + + // Texture is 211x211 RGBA bytes, but upsampled to 256x256 because the wrap mode is REPEAT + var texturesByteLength = 262144; + + var expectedGeometryMemory = b3dmGeometryMemory * 2 + i3dmGeometryMemory * 3; + var expectedTextureMemory = texturesByteLength * 5; + + return Cesium3DTilesTester.loadTileset(scene, tilesetWithExternalResourcesUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.geometryByteLength).toBe(expectedGeometryMemory); + expect(statistics.texturesByteLength).toBe(expectedTextureMemory); + }); + }); + + it('does not process tileset when screen space error is not met', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + + // Set zoom far enough away to not meet sse + viewNothing(); + scene.renderForSpecs(); + expect(statistics.visited).toEqual(0); + expect(statistics.numberOfCommands).toEqual(0); + }); + }); + + it('does not select tiles when outside of view frustum', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + + viewSky(); + + scene.renderForSpecs(); + expect(statistics.visited).toEqual(0); + expect(statistics.numberOfCommands).toEqual(0); + expect(tileset._root.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).toEqual(CullingVolume.MASK_OUTSIDE); + }); + }); + + it('culls with content box', function() { + // Root tile has a content box that is half the extents of its box + // Expect to cull root tile and three child tiles + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + + viewBottomLeft(); + scene.renderForSpecs(); + expect(statistics.visited).toEqual(2); // Visits root, but does not render it + expect(statistics.numberOfCommands).toEqual(1); + expect(tileset._selectedTiles[0]).not.toBe(tileset._root); + + // Set contents box to undefined, and now root won't be culled + tileset._root._contentBoundingVolume = undefined; + scene.renderForSpecs(); + expect(statistics.visited).toEqual(2); + expect(statistics.numberOfCommands).toEqual(2); + }); + }); + + function findTileByUrl(tiles, url) { + var length = tiles.length; + for (var i = 0; i < length; ++i) { + if (tiles[i].content._url.indexOf(url) >= 0) { + return tiles[i]; + } + } + return undefined; + } + + it('selects children in front to back order', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + // After moving the camera left by 1.0 and down by 0.5, the distance from the camera should be in the order: + // 1. lower left + // 2. upper left + // 3. lower right + // 4. upper right + + scene.camera.moveLeft(1.0); + scene.camera.moveDown(0.5); + scene.renderForSpecs(); + + var root = tileset._root; + var llTile = findTileByUrl(root.children, 'll.b3dm'); + var lrTile = findTileByUrl(root.children, 'lr.b3dm'); + var urTile = findTileByUrl(root.children, 'ur.b3dm'); + var ulTile = findTileByUrl(root.children, 'ul.b3dm'); + + var selectedTiles = tileset._selectedTiles; + expect(selectedTiles[0]).toBe(root); + expect(selectedTiles[1]).toBe(llTile); + expect(selectedTiles[2]).toBe(ulTile); + expect(selectedTiles[3]).toBe(lrTile); + expect(selectedTiles[4]).toBe(urTile); + }); + }); + + function testDynamicScreenSpaceError(url, distance) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + var statistics = tileset._statistics; + + // Horizon view, only root is visible + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, distance)); + + // Set dynamic SSE to false (default) + tileset.dynamicScreenSpaceError = false; + scene.renderForSpecs(); + expect(statistics.visited).toEqual(1); + expect(statistics.numberOfCommands).toEqual(1); + + // Set dynamic SSE to true, now the root is not rendered + tileset.dynamicScreenSpaceError = true; + tileset.dynamicScreenSpaceErrorDensity = 1.0; + tileset.dynamicScreenSpaceErrorFactor = 10.0; + scene.renderForSpecs(); + expect(statistics.visited).toEqual(0); + expect(statistics.numberOfCommands).toEqual(0); + }); + } + + function numberOfChildrenWithoutContent(tile) { + var children = tile.children; + var length = children.length; + var count = 0; + for (var i = 0; i < length; ++i) { + var child = children[i]; + if (!child.contentReady) { + ++count; + } + } + return count; + } + + // Adjust distances for each test because the dynamic SSE takes the + // bounding volume height into account, which differs for each bounding volume. + it('uses dynamic screen space error for tileset with region', function() { + return testDynamicScreenSpaceError(withTransformRegionUrl, 103.0); + }); + + it('uses dynamic screen space error for tileset with bounding sphere', function() { + return testDynamicScreenSpaceError(withBoundingSphereUrl, 137.0); + }); + + it('uses dynamic screen space error for local tileset with box', function() { + return testDynamicScreenSpaceError(withTransformBoxUrl, 103.0); + }); + + it('uses dynamic screen space error for local tileset with sphere', function() { + return testDynamicScreenSpaceError(withTransformSphereUrl, 144.0); + }); + + it('additive refinement - selects root when sse is met', function() { + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + // Meets screen space error, only root tile is rendered + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(1); + expect(statistics.numberOfCommands).toEqual(1); + }); + }); + + it('additive refinement - selects all tiles when sse is not met', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + // Does not meet screen space error, all tiles are visible + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + }); + }); + + it('additive refinement - use parent\'s geometric error on child\'s box for early refinement', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); + expect(statistics.numberOfCommands).toEqual(5); + + // Both right tiles don't meet the SSE anymore + scene.camera.moveLeft(50.0); + scene.renderForSpecs(); + expect(statistics.visited).toEqual(3); + expect(statistics.numberOfCommands).toEqual(3); + }); + }); + + it('additive refinement - selects tile when inside viewer request volume', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl).then(function(tileset) { + var statistics = tileset._statistics; + // Force root tile to always not meet SSE since this is just checking the request volume + tileset.maximumScreenSpaceError = 0.0; + + // Renders all 5 tiles + setZoom(20.0); + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(5); + + // No longer renders the tile with a request volume + setZoom(1500.0); + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(4); + }); + }); + + it('replacement refinement - selects root when sse is met', function() { + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset._root.refine = Cesium3DTileRefine.REPLACE; + + // Meets screen space error, only root tile is rendered + scene.renderForSpecs(); + + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(1); + expect(statistics.numberOfCommands).toEqual(1); + }); + }); + + it('replacement refinement - selects children when sse is not met', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset._root.refine = Cesium3DTileRefine.REPLACE; + + // Does not meet screen space error, child tiles replace root tile + scene.renderForSpecs(); + + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(5); // Visits root, but does not render it + expect(statistics.numberOfCommands).toEqual(4); + }); + }); + + it('replacement refinement - selects root when sse is not met and children are not ready', function() { + // Set view so that only root tile is loaded initially + viewRootOnly(); + + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var root = tileset._root; + root.refine = Cesium3DTileRefine.REPLACE; + + // Set zoom to start loading child tiles + viewAllTiles(); + scene.renderForSpecs(); + + var statistics = tileset._statistics; + // LOD skipping visits all visible + expect(statistics.visited).toEqual(5); + // no stencil clear command because only the root tile + expect(statistics.numberOfCommands).toEqual(1); + expect(statistics.numberOfPendingRequests).toEqual(4); + expect(numberOfChildrenWithoutContent(root)).toEqual(4); + }); + }); + + it('replacement refinement - selects tile when inside viewer request volume', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl).then(function(tileset) { + var statistics = tileset._statistics; + + var root = tileset._root; + root.refine = Cesium3DTileRefine.REPLACE; + // Force root tile to always not meet SSE since this is just checking the request volume + tileset.maximumScreenSpaceError = 0.0; + + // Renders all 5 tiles + setZoom(20.0); + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(5); + expect(root.selected).toBe(false); + + // No longer renders the tile with a request volume + setZoom(1500.0); + root.hasRenderableContent = true; // mock content + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(4); + expect(root.selected).toBe(true); // one child is no longer selected. root is chosen instead + }); + }); + + it('replacement refinement - selects root when sse is not met and subtree is not refinable (1)', function() { + // No children have content, but all grandchildren have content + // + // C + // E E + // C C C C + // + + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement1Url).then(function(tileset) { + viewAllTiles(); + scene.renderForSpecs(); + + var statistics = tileset._statistics; + var root = tileset._root; + + return when.join(root.children[0].contentReadyPromise, root.children[1].contentReadyPromise).then(function() { + // Even though root's children are loaded, the grandchildren need to be loaded before it becomes refinable + expect(numberOfChildrenWithoutContent(root)).toEqual(0); // Children are loaded + expect(statistics.numberOfCommands).toEqual(1); // No stencil or backface commands; no mixed content + expect(statistics.numberOfPendingRequests).toEqual(4); // Loading grandchildren + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(4); // Render children + }); + }); + }); + }); + + it('replacement refinement - selects root when sse is not met and subtree is not refinable (2)', function() { + // Check that the root is refinable once its child is loaded + // + // C + // E + // C E + // C (smaller geometric error) + // + + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement2Url).then(function(tileset) { + var statistics = tileset._statistics; + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(1); + + setZoom(5.0); // Zoom into the last tile, when it is ready the root is refinable + scene.renderForSpecs(); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(2); // Renders two content tiles + }); + }); + }); + }); + + it('replacement refinement - selects root when sse is not met and subtree is not refinable (3)', function() { + // Check that the root is refinable once its child is loaded + // + // C + // T (external tileset ref) + // E (root of external tileset) + // C C C C + // + + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + var statistics = tileset._statistics; + var root = tileset._root; + expect(statistics.numberOfCommands).toEqual(1); + + viewAllTiles(); + scene.renderForSpecs(); + return root.children[0].contentReadyPromise.then(function() { + // The external tileset json is loaded, but the external tileset isn't. + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(1); // root + expect(statistics.numberOfPendingRequests).toEqual(4); // Loading child content tiles + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(root.selected).toEqual(false); + expect(statistics.numberOfCommands).toEqual(4); // Render child content tiles + }); + }); + }); + }); + + it('replacement and additive refinement', function() { + // A + // A R (not rendered) + // R A R A + // + return Cesium3DTilesTester.loadTileset(scene, tilesetRefinementMix).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(7); + expect(statistics.numberOfCommands).toEqual(6); + }); + }); + + describe('children bound union optimization', function() { + it('does not select visible tiles with invisible children', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude, 22.0); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, 1.57, 1.0)); + + var root = tileset._root; + var childRoot = root.children[0]; + + scene.renderForSpecs(); + + expect(childRoot.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(childRoot.children[0].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[1].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).toEqual(CullingVolume.MASK_OUTSIDE); + + expect(tileset._selectedTiles.length).toEqual(0); + expect(childRoot.selected).toBe(false); + }); + }); + + it('does not select visible tiles not meeting SSE with visible children', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + var root = tileset._root; + var childRoot = root.children[0]; + childRoot.geometricError = 240; + + scene.renderForSpecs(); + + expect(childRoot.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(childRoot.children[0].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[1].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(childRoot.selected).toBe(false); + }); + }); + + it('does select visible tiles meeting SSE with visible children', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + var root = tileset._root; + var childRoot = root.children[0]; + + childRoot.geometricError = 0; // child root should meet SSE and children should not be drawn + scene.renderForSpecs(); + // wait for load because geometric error has changed + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + expect(childRoot.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(childRoot.children[0].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[1].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(childRoot.selected).toBe(true); + }); + }); + }); + + it('does select visibile tiles with visible children failing request volumes', function() { + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + var root = tileset._root; + var childRoot = root.children[0]; + + expect(childRoot.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(childRoot.children[0].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[1].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(tileset._selectedTiles.length).toEqual(1); + expect(childRoot.selected).toBe(true); + }); + }); + + it('does select visibile tiles with visible children passing request volumes', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + var root = tileset._root; + var childRoot = root.children[0]; + childRoot.geometricError = 0; + + // wait for load because geometric error has changed + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + expect(childRoot.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(childRoot.children[0].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[1].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + + expect(tileset._selectedTiles.length).toEqual(1); + expect(childRoot.selected).toBe(true); + + childRoot.geometricError = 200; + scene.renderForSpecs(); + expect(tileset._selectedTiles.length).toEqual(4); + expect(childRoot.selected).toBe(false); + }); + }); + }); + }); + + it('loads tileset with external tileset.json', function() { + // Set view so that no tiles are loaded initially + viewNothing(); + + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { + // Root points to an external tileset.json and has no children until it is requested + var root = tileset._root; + expect(root.children.length).toEqual(0); + + // Set view so that root's content is requested + viewRootOnly(); + scene.renderForSpecs(); + return root.contentReadyPromise.then(function() { + expect(root.hasTilesetContent).toEqual(true); + + // Root has one child now, the root of the external tileset + expect(root.children.length).toEqual(1); + + // Check that headers are equal + var subtreeRoot = root.children[0]; + expect(root.geometricError).toEqual(subtreeRoot.geometricError); + expect(root.refine).toEqual(subtreeRoot.refine); + expect(root.contentBoundingVolume.boundingVolume).toEqual(subtreeRoot.contentBoundingVolume.boundingVolume); + + // Check that subtree root has 4 children + expect(subtreeRoot.hasTilesetContent).toEqual(false); + expect(subtreeRoot.children.length).toEqual(4); + }); + }); + }); + + it('preserves query string with external tileset.json', function() { + // Set view so that no tiles are loaded initially + viewNothing(); + + //Spy on loadWithXhr so we can verify requested urls + spyOn(loadWithXhr, 'load').and.callThrough(); + + var queryParams = '?a=1&b=boy'; + var expectedUrl = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json' + queryParams; + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl + queryParams).then(function(tileset) { + //Make sure tileset.json was requested with query parameters + expect(loadWithXhr.load.calls.argsFor(0)[0]).toEqual(expectedUrl); + + loadWithXhr.load.calls.reset(); + + // Set view so that root's content is requested + viewRootOnly(); + scene.renderForSpecs(); + + return tileset._root.contentReadyPromise; + }).then(function() { + //Make sure tileset2.json was requested with query parameters and version + var queryParamsWithVersion = queryParams + '&v=0.0'; + expectedUrl = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json' + queryParamsWithVersion; + expect(loadWithXhr.load.calls.argsFor(0)[0]).toEqual(expectedUrl); + }); + }); + + it('renders tileset with external tileset.json', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(7); // Visits two tiles with tileset content, five tiles with b3dm content + expect(statistics.numberOfCommands).toEqual(5); // Render the five tiles with b3dm content + }); + }); + + it('set tile color', function() { + return Cesium3DTilesTester.loadTileset(scene, noBatchIdsUrl).then(function(tileset) { + // Get initial color + var color; + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + color = rgba; + }); + + // Check for color + tileset._root.color = Color.RED; + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba).not.toEqual(color); + }); + }); + }); + + it('debugFreezeFrame', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + viewRootOnly(); + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(1); + expect(statistics.numberOfCommands).toEqual(1); + + tileset.debugFreezeFrame = true; + viewAllTiles(); + scene.renderForSpecs(); + expect(statistics.visited).toEqual(0); // selectTiles returns early, so no tiles are visited + expect(statistics.numberOfCommands).toEqual(1); // root tile is still in selectedTiles list + }); + }); + + function checkDebugColorizeTiles(url) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + // Get initial color + var color; + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + color = rgba; + }); + + // Check for debug color + tileset.debugColorizeTiles = true; + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba).not.toEqual(color); + }); + + // Check for original color + tileset.debugColorizeTiles = false; + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba).toEqual(color); + }); + }); + } + + it('debugColorizeTiles for b3dm with batch table', function() { + return checkDebugColorizeTiles(withBatchTableUrl); + }); + + it('debugColorizeTiles for b3dm without batch table', function() { + return checkDebugColorizeTiles(noBatchIdsUrl); + }); + + it('debugColorizeTiles for i3dm', function() { + viewInstances(); + return checkDebugColorizeTiles(instancedUrl); + }); + + it('debugColorizeTiles for cmpt', function() { + return checkDebugColorizeTiles(compositeUrl); + }); + + it('debugColorizeTiles for pnts with batch table', function() { + viewPointCloud(); + return checkDebugColorizeTiles(pointCloudBatchedUrl); + }); + + it('debugColorizeTiles for pnts without batch table', function() { + viewPointCloud(); + return checkDebugColorizeTiles(pointCloudUrl); + }); + + it('debugWireframe', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + viewRootOnly(); + tileset.debugWireframe = true; + scene.renderForSpecs(); + var commands = scene.frameState.commandList; + var length = commands.length; + var i; + for (i = 0; i < length; ++i) { + expect(commands[i].primitiveType).toEqual(PrimitiveType.LINES); + } + + tileset.debugWireframe = false; + scene.renderForSpecs(); + commands = scene.frameState.commandList; + for (i = 0; i < length; ++i) { + expect(commands[i].primitiveType).toEqual(PrimitiveType.TRIANGLES); + } + }); + }); + + it('debugShowBoundingVolume', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + viewRootOnly(); + tileset.debugShowBoundingVolume = true; + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(1); + expect(statistics.numberOfCommands).toEqual(2); // Tile command + bounding volume command + + tileset.debugShowBoundingVolume = false; + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(1); + }); + }); + + it('debugShowContentBoundingVolume', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + viewRootOnly(); + tileset.debugShowContentBoundingVolume = true; + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(1); + expect(statistics.numberOfCommands).toEqual(2); // Tile command + bounding volume command + + tileset.debugShowContentBoundingVolume = false; + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(1); + }); + }); + + it('debugShowViewerRequestVolume', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl).then(function(tileset) { + tileset.debugShowViewerRequestVolume = true; + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.visited).toEqual(6); // 1 empty root tile + 4 b3dm tiles + 1 pnts tile + expect(statistics.numberOfCommands).toEqual(6); // 5 tile commands + viewer request volume command + + tileset.debugShowViewerRequestVolume = false; + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(5); + }); + }); + + it('show tile debug labels with regions', function() { + // tilesetUrl has bounding regions + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.debugShowGeometricError = true; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).toBeDefined(); + expect(tileset._tileDebugLabels.length).toEqual(5); + + var root = tileset._root; + expect(tileset._tileDebugLabels._labels[0].text).toEqual('Geometric error: ' + root.geometricError); + expect(tileset._tileDebugLabels._labels[1].text).toEqual('Geometric error: ' + root.children[0].geometricError); + expect(tileset._tileDebugLabels._labels[2].text).toEqual('Geometric error: ' + root.children[1].geometricError); + expect(tileset._tileDebugLabels._labels[3].text).toEqual('Geometric error: ' + root.children[2].geometricError); + expect(tileset._tileDebugLabels._labels[4].text).toEqual('Geometric error: ' + root.children[3].geometricError); + + tileset.debugShowGeometricError = false; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).not.toBeDefined(); + }); + }); + + it('show tile debug labels with boxes', function() { + // tilesetWithTransformsUrl has bounding boxes + return Cesium3DTilesTester.loadTileset(scene, tilesetWithTransformsUrl).then(function(tileset) { + tileset.debugShowGeometricError = true; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).toBeDefined(); + expect(tileset._tileDebugLabels.length).toEqual(2); + + var root = tileset._root; + expect(tileset._tileDebugLabels._labels[0].text).toEqual('Geometric error: ' + root.geometricError); + expect(tileset._tileDebugLabels._labels[1].text).toEqual('Geometric error: ' + root.children[0].geometricError); + + tileset.debugShowGeometricError = false; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).not.toBeDefined(); + }); + }); + + it('show tile debug labels with bounding spheres', function() { + // tilesetWithViewerRequestVolumeUrl has bounding sphere + return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl).then(function(tileset) { + tileset.debugShowGeometricError = true; + scene.renderForSpecs(); + + var length = tileset._selectedTiles.length; + expect(tileset._tileDebugLabels).toBeDefined(); + expect(tileset._tileDebugLabels.length).toEqual(length); + + for (var i = 0; i < length; ++i) { + expect(tileset._tileDebugLabels._labels[i].text).toEqual('Geometric error: ' + tileset._selectedTiles[i].geometricError); + } + + tileset.debugShowGeometricError = false; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).not.toBeDefined(); + }); + }); + + it('show tile debug labels with rendering statistics', function() { + // tilesetUrl has bounding regions + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.debugShowRenderingStatistics = true; + viewRootOnly(); + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).toBeDefined(); + expect(tileset._tileDebugLabels.length).toEqual(1); + + var content = tileset._root.content; + var expected = 'Commands: ' + tileset._root.commandsLength + '\n' + + 'Triangles: ' + content.trianglesLength + '\n' + + 'Features: ' + content.featuresLength; + + expect(tileset._tileDebugLabels._labels[0].text).toEqual(expected); + + tileset.debugShowRenderingStatistics = false; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).not.toBeDefined(); + }); + }); + + it('show tile debug labels with memory usage', function() { + // tilesetUrl has bounding regions + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.debugShowMemoryUsage = true; + viewRootOnly(); + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).toBeDefined(); + expect(tileset._tileDebugLabels.length).toEqual(1); + + var expected = 'Texture Memory: 0\n' + + 'Geometry Memory: 0.008'; + + expect(tileset._tileDebugLabels._labels[0].text).toEqual(expected); + + tileset.debugShowMemoryUsage = false; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).not.toBeDefined(); + }); + }); + + it('show tile debug labels with all statistics', function() { + // tilesetUrl has bounding regions + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.debugShowGeometricError = true; + tileset.debugShowRenderingStatistics = true; + tileset.debugShowMemoryUsage = true; + viewRootOnly(); + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).toBeDefined(); + + var expected = 'Geometric error: 70\n' + + 'Commands: 1\n' + + 'Triangles: 120\n' + + 'Features: 10\n' + + 'Texture Memory: 0\n' + + 'Geometry Memory: 0.008'; + expect(tileset._tileDebugLabels._labels[0].text).toEqual(expected); + + tileset.debugShowGeometricError = false; + tileset.debugShowRenderingStatistics = false; + tileset.debugShowMemoryUsage = false; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).not.toBeDefined(); + }); + }); + + it('show only picked tile debug label with all stats', function() { + // tilesetUrl has bounding regions + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.debugShowGeometricError = true; + tileset.debugShowRenderingStatistics = true; + tileset.debugShowMemoryUsage = true; + tileset.debugPickedTileLabelOnly = true; + + var scratchPosition = new Cartesian3(1.0, 1.0, 1.0); + tileset.debugPickedTile = tileset._root; + tileset.debugPickPosition = scratchPosition; + + scene.renderForSpecs(); + expect(tileset._tileDebugLabels).toBeDefined(); + + var expected = 'Geometric error: 70\n' + + 'Commands: 1\n' + + 'Triangles: 120\n' + + 'Features: 10\n' + + 'Texture Memory: 0\n' + + 'Geometry Memory: 0.008'; + expect(tileset._tileDebugLabels.get(0).text).toEqual(expected); + expect(tileset._tileDebugLabels.get(0).position).toEqual(scratchPosition); + + tileset.debugPickedTile = undefined; + scene.renderForSpecs(); + expect(tileset._tileDebugLabels.length).toEqual(0); + }); + }); + + it('does not request tiles when picking', function() { + viewNothing(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + viewRootOnly(); + scene.pickForSpecs(); + expect(tileset._statistics.numberOfPendingRequests).toEqual(0); + scene.renderForSpecs(); + expect(tileset._statistics.numberOfPendingRequests).toEqual(1); + }); + }); + + it('does not process tiles when picking', function() { + var spy = spyOn(Cesium3DTile.prototype, 'process').and.callThrough(); + + viewNothing(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + viewRootOnly(); + scene.renderForSpecs(); // Request root + expect(tileset._statistics.numberOfPendingRequests).toEqual(1); + return tileset._root.contentReadyToProcessPromise.then(function() { + scene.pickForSpecs(); + expect(spy).not.toHaveBeenCalled(); + scene.renderForSpecs(); + expect(spy).toHaveBeenCalled(); + }); + }); + }); + + it('does not request tiles when the request scheduler is full', function() { + viewRootOnly(); // Root tiles are loaded initially + var options = { + skipLevelOfDetail : false + }; + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl, options).then(function(tileset) { + // Try to load 4 children. Only 3 requests will go through, 1 will be attempted. + var oldMaximumRequestsPerServer = RequestScheduler.maximumRequestsPerServer; + RequestScheduler.maximumRequestsPerServer = 3; + + viewAllTiles(); + scene.renderForSpecs(); + + expect(tileset._statistics.numberOfPendingRequests).toEqual(3); + expect(tileset._statistics.numberOfAttemptedRequests).toEqual(1); + + RequestScheduler.maximumRequestsPerServer = oldMaximumRequestsPerServer; + }); + }); + + it('load progress events are raised', function() { + // [numberOfPendingRequests, numberOfTilesProcessing] + var results = [ + [1, 0], + [0, 1], + [0, 0] + ]; + var spyUpdate = jasmine.createSpy('listener'); + + viewNothing(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.loadProgress.addEventListener(spyUpdate); + viewRootOnly(); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(spyUpdate.calls.count()).toEqual(3); + expect(spyUpdate.calls.allArgs()).toEqual(results); + }); + }); + }); + + it('tilesLoaded', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetUrl + })); + expect(tileset.tilesLoaded).toBe(false); + tileset.readyPromise.then(function() { + expect(tileset.tilesLoaded).toBe(false); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(tileset.tilesLoaded).toBe(true); + }); + }); + }); + + it('all tiles loaded event is raised', function() { + // Called first when only the root is visible and it becomes loaded, and then again when + // the rest of the tileset is visible and all tiles are loaded. + var spyUpdate = jasmine.createSpy('listener'); + viewRootOnly(); + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetUrl + })); + tileset.allTilesLoaded.addEventListener(spyUpdate); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + viewAllTiles(); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(spyUpdate.calls.count()).toEqual(2); + }); + }); + }); + + it('tile visible event is raised', function() { + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var spyUpdate = jasmine.createSpy('listener'); + tileset.tileVisible.addEventListener(spyUpdate); + scene.renderForSpecs(); + expect(tileset._root.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(spyUpdate.calls.count()).toEqual(1); + expect(spyUpdate.calls.argsFor(0)[0]).toBe(tileset._root); + }); + }); + + it('destroys', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var root = tileset._root; + expect(tileset.isDestroyed()).toEqual(false); + scene.primitives.remove(tileset); + expect(tileset.isDestroyed()).toEqual(true); + + // Check that all tiles are destroyed + expect(root.isDestroyed()).toEqual(true); + expect(root.children[0].isDestroyed()).toEqual(true); + expect(root.children[1].isDestroyed()).toEqual(true); + expect(root.children[2].isDestroyed()).toEqual(true); + expect(root.children[3].isDestroyed()).toEqual(true); + }); + }); + + it('destroys before external tileset.json finishes loading', function() { + viewNothing(); + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { + var root = tileset._root; + + viewRootOnly(); + scene.renderForSpecs(); // Request external tileset.json + + var statistics = tileset._statistics; + expect(statistics.numberOfPendingRequests).toEqual(1); + scene.primitives.remove(tileset); + + return root.contentReadyPromise.then(function(root) { + fail('should not resolve'); + }).otherwise(function(error) { + // Expect the root to not have added any children from the external tileset.json + expect(root.children.length).toEqual(0); + }); + }); + }); + + it('destroys before tile finishes loading', function() { + viewRootOnly(); + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetUrl + })); + return tileset.readyPromise.then(function(tileset) { + var root = tileset._root; + scene.renderForSpecs(); // Request root + scene.primitives.remove(tileset); + + return root.contentReadyPromise.then(function(content) { + fail('should not resolve'); + }).otherwise(function(error) { + expect(root._contentState).toBe(Cesium3DTileContentState.FAILED); + }); + }); + }); + + /////////////////////////////////////////////////////////////////////////// + // Styling tests + + it('applies show style to a tileset', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + var hideStyle = new Cesium3DTileStyle({show : 'false'}); + tileset.style = hideStyle; + expect(tileset.style).toBe(hideStyle); + expect(scene).toRender([0, 0, 0, 255]); + + tileset.style = new Cesium3DTileStyle({show : 'true'}); + expect(scene).notToRender([0, 0, 0, 255]); + }); + }); + + it('applies style with complex show expression to a tileset', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + // Each feature in the b3dm file has an id property from 0 to 9 + // ${id} >= 10 will always evaluate to false + tileset.style = new Cesium3DTileStyle({show : '${id} >= 50 * 2'}); + expect(scene).toRender([0, 0, 0, 255]); + + // ${id} < 10 will always evaluate to true + tileset.style = new Cesium3DTileStyle({show : '${id} < 200 / 2'}); + expect(scene).notToRender([0, 0, 0, 255]); + }); + }); + + it('applies show style to a tileset with a composite tile', function() { + return Cesium3DTilesTester.loadTileset(scene, compositeUrl).then(function(tileset) { + tileset.style = new Cesium3DTileStyle({show : 'false'}); + expect(scene).toRender([0, 0, 0, 255]); + + tileset.style = new Cesium3DTileStyle({show : 'true'}); + expect(scene).notToRender([0, 0, 0, 255]); + }); + }); + + function expectColorStyle(tileset) { + var color; + expect(scene).toRenderAndCall(function(rgba) { + color = rgba; + }); + + tileset.style = new Cesium3DTileStyle({color : 'color("blue")'}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toEqual(0); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toBeGreaterThan(0); + expect(rgba[3]).toEqual(255); + }); + + // set color to transparent + tileset.style = new Cesium3DTileStyle({color : 'color("blue", 0.0)'}); + expect(scene).toRender([0, 0, 0, 255]); + + tileset.style = new Cesium3DTileStyle({color : 'color("cyan")'}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toEqual(0); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[2]).toBeGreaterThan(0); + expect(rgba[3]).toEqual(255); + }); + + // Remove style + tileset.style = undefined; + expect(scene).toRender(color); + } + + it('applies color style to a tileset', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + expectColorStyle(tileset); + }); + }); + + it('applies color style to a tileset with translucent tiles', function() { + return Cesium3DTilesTester.loadTileset(scene, translucentUrl).then(function(tileset) { + expectColorStyle(tileset); + }); + }); + + it('applies color style to a tileset with translucent and opaque tiles', function() { + return Cesium3DTilesTester.loadTileset(scene, translucentOpaqueMixUrl).then(function(tileset) { + expectColorStyle(tileset); + }); + }); + + it('applies style when feature properties change', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + // Initially, all feature ids are less than 10 + tileset.style = new Cesium3DTileStyle({show : '${id} < 10'}); + expect(scene).notToRender([0, 0, 0, 255]); + + // Change feature ids so the show expression will evaluate to false + var content = tileset._root.content; + var length = content.featuresLength; + var i; + var feature; + for (i = 0; i < length; ++i) { + feature = content.getFeature(i); + feature.setProperty('id', feature.getProperty('id') + 10); + } + expect(scene).toRender([0, 0, 0, 255]); + + // Change ids back + for (i = 0; i < length; ++i) { + feature = content.getFeature(i); + feature.setProperty('id', feature.getProperty('id') - 10); + } + expect(scene).notToRender([0, 0, 0, 255]); + }); + }); + + it('applies style with complex color expression to a tileset', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + // Each feature in the b3dm file has an id property from 0 to 9 + // ${id} >= 10 will always evaluate to false + tileset.style = new Cesium3DTileStyle({color : '(${id} >= 50 * 2) ? color("red") : color("blue")'}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toEqual(0); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toBeGreaterThan(0); + expect(rgba[3]).toEqual(255); + }); + + // ${id} < 10 will always evaluate to true + tileset.style = new Cesium3DTileStyle({color : '(${id} < 50 * 2) ? color("red") : color("blue")'}); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + }); + }); + + it('applies conditional color style to a tileset', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + // ${id} < 10 will always evaluate to true + tileset.style = new Cesium3DTileStyle({ + color : { + conditions : [ + ['${id} < 10', 'color("red")'], + ['true', 'color("blue")'] + ] + } + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // ${id}>= 10 will always evaluate to false + tileset.style = new Cesium3DTileStyle({ + color : { + conditions : [ + ['${id} >= 10', 'color("red")'], + ['true', 'color("blue")'] + ] + } + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toEqual(0); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toBeGreaterThan(0); + expect(rgba[3]).toEqual(255); + }); + }); + }); + + it('loads style from uri', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + // ${id} < 10 will always evaluate to true + tileset.style = new Cesium3DTileStyle(styleUrl); + return tileset.style.readyPromise.then(function(style) { + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + }).otherwise(function(error) { + expect(error).not.toBeDefined(); + }); + }); + }); + + it('applies custom style to a tileset', function() { + var style = new Cesium3DTileStyle(); + style.show = { + evaluate : function(frameState, feature) { + return this._value; + }, + _value : false + }; + style.color = { + evaluateColor : function(frameState, feature, result) { + return Color.clone(Color.WHITE, result); + } + }; + + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + tileset.style = style; + expect(tileset.style).toBe(style); + expect(scene).toRender([0, 0, 0, 255]); + + style.show._value = true; + tileset.makeStyleDirty(); + expect(scene).notToRender([0, 0, 0, 255]); + }); + }); + + function testColorBlendMode(url) { + return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + // Check that the feature is red + var sourceRed; + expect(scene).toRenderAndCall(function(rgba) { + sourceRed = rgba[0]; + }); + + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Use HIGHLIGHT blending + tileset.colorBlendMode = Cesium3DTileColorBlendMode.HIGHLIGHT; + + // Style with dark yellow. Expect the red channel to be darker than before. + tileset.style = new Cesium3DTileStyle({ + color : 'rgb(128, 128, 0)' + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[0]).toBeLessThan(sourceRed); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Style with yellow + alpha. Expect the red channel to be darker than before. + tileset.style = new Cesium3DTileStyle({ + color : 'rgba(255, 255, 0, 0.5)' + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[0]).toBeLessThan(sourceRed); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Use REPLACE blending + tileset.colorBlendMode = Cesium3DTileColorBlendMode.REPLACE; + + // Style with dark yellow. Expect the red and green channels to be roughly dark yellow. + tileset.style = new Cesium3DTileStyle({ + color : 'rgb(128, 128, 0)' + }); + var replaceRed; + var replaceGreen; + expect(scene).toRenderAndCall(function(rgba) { + replaceRed = rgba[0]; + replaceGreen = rgba[1]; + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[0]).toBeLessThan(255); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[1]).toBeLessThan(255); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Style with yellow + alpha. Expect the red and green channels to be a shade of yellow. + tileset.style = new Cesium3DTileStyle({ + color : 'rgba(255, 255, 0, 0.5)' + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[0]).toBeLessThan(255); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[1]).toBeLessThan(255); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Use MIX blending + tileset.colorBlendMode = Cesium3DTileColorBlendMode.MIX; + tileset.colorBlendAmount = 0.5; + + // Style with dark yellow. Expect color to be a mix of the source and style colors. + tileset.style = new Cesium3DTileStyle({ + color : 'rgb(128, 128, 0)' + }); + var mixRed; + var mixGreen; + expect(scene).toRenderAndCall(function(rgba) { + mixRed = rgba[0]; + mixGreen = rgba[1]; + expect(rgba[0]).toBeGreaterThan(replaceRed); + expect(rgba[0]).toBeLessThan(sourceRed); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[1]).toBeLessThan(replaceGreen); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Set colorBlendAmount to 0.25. Expect color to be closer to the source color. + tileset.colorBlendAmount = 0.25; + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(mixRed); + expect(rgba[0]).toBeLessThan(sourceRed); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[1]).toBeLessThan(mixGreen); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Set colorBlendAmount to 0.0. Expect color to equal the source color + tileset.colorBlendAmount = 0.0; + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toEqual(sourceRed); + expect(rgba[1]).toEqual(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Set colorBlendAmount to 1.0. Expect color to equal the style color + tileset.colorBlendAmount = 1.0; + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toEqual(replaceRed); + expect(rgba[1]).toEqual(replaceGreen); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Style with yellow + alpha. Expect color to be a mix of the source and style colors. + tileset.colorBlendAmount = 0.5; + tileset.style = new Cesium3DTileStyle({ + color : 'rgba(255, 255, 0, 0.5)' + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + }); + } + + it('sets colorBlendMode', function() { + return testColorBlendMode(colorsUrl); + }); + + it('sets colorBlendMode when vertex texture fetch is not supported', function() { + // Disable VTF + var maximumVertexTextureImageUnits = ContextLimits.maximumVertexTextureImageUnits; + ContextLimits._maximumVertexTextureImageUnits = 0; + return testColorBlendMode(colorsUrl).then(function() { + // Re-enable VTF + ContextLimits._maximumVertexTextureImageUnits = maximumVertexTextureImageUnits; + }); + }); + + it('sets colorBlendMode for textured tileset', function() { + return testColorBlendMode(texturedUrl); + }); + + it('sets colorBlendMode for instanced tileset', function() { + viewInstances(); + return testColorBlendMode(instancedRedMaterialUrl); + }); + + /////////////////////////////////////////////////////////////////////////// + // Cache replacement tests + + it('Unload all cached tiles not required to meet SSE using maximumMemoryUsage', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumMemoryUsage = 0; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var statistics = tileset._statistics; + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles + expect(tileset.totalMemoryUsageInBytes).toEqual(44400); // Specific to this tileset + + // Zoom out so only root tile is needed to meet SSE. This unloads + // the four children since the maximum memory usage is zero. + viewRootOnly(); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(1); + expect(statistics.numberOfTilesWithContentReady).toEqual(1); + expect(tileset.totalMemoryUsageInBytes).toEqual(8880); // Specific to this tileset + + // Zoom back in so all four children are re-requested. + viewAllTiles(); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles + expect(tileset.totalMemoryUsageInBytes).toEqual(44400); // Specific to this tileset + }); + }); + }); + + it('Unload some cached tiles not required to meet SSE using maximumMemoryUsage', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumMemoryUsage = 0.03; // Just enough memory to allow 3 tiles to remain + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var statistics = tileset._statistics; + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles + + // Zoom out so only root tile is needed to meet SSE. This unloads + // two of the four children so three tiles are still loaded (the + // root and two children) since the maximum memory usage is sufficient. + viewRootOnly(); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(1); + expect(statistics.numberOfTilesWithContentReady).toEqual(3); + + // Zoom back in so the two children are re-requested. + viewAllTiles(); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles + }); + }); + }); + + it('Unloads cached tiles outside of the view frustum using maximumMemoryUsage', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumMemoryUsage = 0; + + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + + viewSky(); + + // All tiles are unloaded + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(0); + expect(statistics.numberOfTilesWithContentReady).toEqual(0); + + // Reset camera so all tiles are reloaded + viewAllTiles(); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + }); + }); + }); + + it('Unloads cached tiles in a tileset with external tileset.json using maximumMemoryUsage', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { + var statistics = tileset._statistics; + var replacementList = tileset._replacementList; + + tileset.maximumMemoryUsage = 0.025; + + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + expect(replacementList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel. + + // Zoom out so only root tile is needed to meet SSE. This unloads + // all tiles except the root and one of the b3dm children + viewRootOnly(); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(1); + expect(statistics.numberOfTilesWithContentReady).toEqual(2); + expect(replacementList.length - 1).toEqual(2); + + // Reset camera so all tiles are reloaded + viewAllTiles(); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + + expect(replacementList.length - 1).toEqual(5); + }); + }); + }); + + it('Unloads cached tiles in a tileset with empty tiles using maximumMemoryUsage', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetEmptyRootUrl).then(function(tileset) { + var statistics = tileset._statistics; + + tileset.maximumMemoryUsage = 0.025; + + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(4); + expect(statistics.numberOfTilesWithContentReady).toEqual(4); // 4 children with b3dm content (does not include empty root) + + viewSky(); + + // Unload tiles to meet cache size + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(0); + expect(statistics.numberOfTilesWithContentReady).toEqual(2); // 2 children with b3dm content (does not include empty root) + + // Reset camera so all tiles are reloaded + viewAllTiles(); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(4); + expect(statistics.numberOfTilesWithContentReady).toEqual(4); + }); + }); + }); + + it('Unload cached tiles when a tileset uses replacement refinement using maximumMemoryUsage', function() { + // No children have content, but all grandchildren have content + // + // C + // E E + // C C C C + // + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement1Url).then(function(tileset) { + tileset.maximumMemoryUsage = 0; // Only root needs to be visible + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var statistics = tileset._statistics; + expect(statistics.numberOfCommands).toEqual(4); // 4 grandchildren. Root is replaced. + expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Root + four grandchildren (does not include empty children) + + // Zoom out so only root tile is needed to meet SSE. This unloads + // all grandchildren since the max number of loaded tiles is one. + viewRootOnly(); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(1); + expect(statistics.numberOfTilesWithContentReady).toEqual(1); + + // Zoom back in so the four children are re-requested. + viewAllTiles(); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + expect(statistics.numberOfCommands).toEqual(4); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + }); + }); + }); + + it('Explicitly unloads cached tiles with trimLoadedTiles', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumMemoryUsage = 0.05; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var statistics = tileset._statistics; + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles + + // Zoom out so only root tile is needed to meet SSE. The children + // are not unloaded since max number of loaded tiles is five. + viewRootOnly(); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(1); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + + tileset.trimLoadedTiles(); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(1); + expect(statistics.numberOfTilesWithContentReady).toEqual(1); + }); + }); + + it('tileUnload event is raised', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumMemoryUsage = 0; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var statistics = tileset._statistics; + expect(statistics.numberOfCommands).toEqual(5); + expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles + + // Zoom out so only root tile is needed to meet SSE. All the + // children are unloaded since max number of loaded tiles is one. + viewRootOnly(); + var spyUpdate = jasmine.createSpy('listener'); + tileset.tileUnload.addEventListener(spyUpdate); + scene.renderForSpecs(); + + expect(tileset._root.visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(spyUpdate.calls.count()).toEqual(4); + expect(spyUpdate.calls.argsFor(0)[0]).toBe(tileset._root.children[0]); + expect(spyUpdate.calls.argsFor(1)[0]).toBe(tileset._root.children[1]); + expect(spyUpdate.calls.argsFor(2)[0]).toBe(tileset._root.children[2]); + expect(spyUpdate.calls.argsFor(3)[0]).toBe(tileset._root.children[3]); + }); + }); + + it('maximumMemoryUsage throws when negative', function() { + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + expect(function() { + tileset.maximumMemoryUsage = -1; + }).toThrowDeveloperError(); + }); + + it('maximumScreenSpaceError throws when negative', function() { + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + expect(function() { + tileset.maximumScreenSpaceError = -1; + }).toThrowDeveloperError(); + }); + + it('propagates tile transform down the tree', function() { + var b3dmCommands = 1; + var i3dmCommands = scene.context.instancedArrays ? 1 : 25; // When instancing is not supported there is one command per instance + var totalCommands = b3dmCommands + i3dmCommands; + return Cesium3DTilesTester.loadTileset(scene, tilesetWithTransformsUrl).then(function(tileset) { + var statistics = tileset._statistics; + var root = tileset._root; + var rootTransform = Matrix4.unpack(root._header.transform); + + var child = root.children[0]; + var childTransform = Matrix4.unpack(child._header.transform); + var computedTransform = Matrix4.multiply(rootTransform, childTransform, new Matrix4()); + + expect(statistics.numberOfCommands).toBe(totalCommands); + expect(root.computedTransform).toEqual(rootTransform); + expect(child.computedTransform).toEqual(computedTransform); + + // Set the tileset's modelMatrix + var tilesetTransform = Matrix4.fromTranslation(new Cartesian3(0.0, 1.0, 0.0)); + tileset.modelMatrix = tilesetTransform; + computedTransform = Matrix4.multiply(tilesetTransform, computedTransform, computedTransform); + scene.renderForSpecs(); + expect(child.computedTransform).toEqual(computedTransform); + + // Set the modelMatrix somewhere off screen + tileset.modelMatrix = Matrix4.fromTranslation(new Cartesian3(0.0, 100000.0, 0.0)); + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toBe(0); + + // Now bring it back + tileset.modelMatrix = Matrix4.IDENTITY; + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toBe(totalCommands); + + // Do the same steps for a tile transform + child.transform = Matrix4.fromTranslation(new Cartesian3(0.0, 100000.0, 0.0)); + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toBe(1); + child.transform = Matrix4.IDENTITY; + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toBe(totalCommands); + }); + }); + + it('does not mark tileset as refining when tiles have selection depth 0', function() { + viewRootOnly(); + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + viewAllTiles(); + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(statistics.numberOfTilesWithContentReady).toEqual(1); + expect(tileset._selectedTiles[0]._selectionDepth).toEqual(0); + expect(tileset._hasMixedContent).toBe(false); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + expect(tileset._hasMixedContent).toBe(false); + }); + }); + }); + + it('marks tileset as mixed when tiles have nonzero selection depth', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + var statistics = tileset._statistics; + + tileset._root.children[0].children[0].children[0].unloadContent(); + tileset._root.children[0].children[0].children[1].unloadContent(); + tileset._root.children[0].children[0].children[2].unloadContent(); + statistics.numberOfTilesWithContentReady -= 3; + + scene.renderForSpecs(); + + expect(tileset._hasMixedContent).toBe(true); + expect(statistics.numberOfTilesWithContentReady).toEqual(2); + expect(tileset._root.children[0].children[0].children[3]._selectionDepth).toEqual(1); + expect(tileset._root._selectionDepth).toEqual(0); + + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + expect(statistics.numberOfTilesWithContentReady).toEqual(5); + expect(tileset._hasMixedContent).toBe(false); + }); + }); + }); + + it('adds stencil clear command first when unresolved', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + + tileset._root.children[0].children[0].children[0].unloadContent(); + tileset._root.children[0].children[0].children[1].unloadContent(); + tileset._root.children[0].children[0].children[2].unloadContent(); + + scene.renderForSpecs(); + var commandList = scene.frameState.commandList; + expect(commandList[0] instanceof ClearCommand).toBe(true); + expect(commandList[0].stencil).toBe(0); + }); + }); + + it('creates duplicate backface commands', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + + var statistics = tileset._statistics; + var root = tileset._root; + + tileset._root.children[0].children[0].children[0].unloadContent(); + tileset._root.children[0].children[0].children[1].unloadContent(); + tileset._root.children[0].children[0].children[2].unloadContent(); + + scene.renderForSpecs(); + + // 2 for root tile, 2 for child, 1 for stencil clear + expect(statistics.numberOfCommands).toEqual(5); + expect(root.selected).toBe(true); + expect(root._finalResolution).toBe(false); + expect(root.children[0].children[0].children[3].selected).toBe(true); + expect(root.children[0].children[0].children[3]._finalResolution).toBe(true); + expect(tileset._hasMixedContent).toBe(true); + + var commandList = scene.frameState.commandList; + var rs = commandList[1].renderState; + expect(rs.cull.enabled).toBe(true); + expect(rs.cull.face).toBe(CullFace.FRONT); + }); + }); + + it('does not create duplicate backface commands if no selected descendants', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + var statistics = tileset._statistics; + var root = tileset._root; + + tileset._root.children[0].children[0].children[0].unloadContent(); + tileset._root.children[0].children[0].children[1].unloadContent(); + tileset._root.children[0].children[0].children[2].unloadContent(); + tileset._root.children[0].children[0].children[3].unloadContent(); + + scene.renderForSpecs(); + + // 2 for root tile, 1 for child, 1 for stencil clear + expect(statistics.numberOfCommands).toEqual(1); + expect(root.selected).toBe(true); + expect(root._finalResolution).toBe(true); + expect(root.children[0].children[0].children[0].selected).toBe(false); + expect(root.children[0].children[0].children[1].selected).toBe(false); + expect(root.children[0].children[0].children[2].selected).toBe(false); + expect(root.children[0].children[0].children[3].selected).toBe(false); + expect(tileset._hasMixedContent).toBe(false); + }); + }); + + it('does not add commands or stencil clear command with no selected tiles', function() { + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetUrl + })); + scene.renderForSpecs(); + var statistics = tileset._statistics; + expect(tileset._selectedTiles.length).toEqual(0); + expect(statistics.numberOfCommands).toEqual(0); + }); + + it('does not add stencil clear command or backface commands when fully resolved', function() { + viewAllTiles(); + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.numberOfCommands).toEqual(tileset._selectedTiles.length); + + var commandList = scene.frameState.commandList; + var length = commandList.length; + for (var i = 0; i < length; ++i) { + var command = commandList[i]; + expect(command instanceof ClearCommand).toBe(false); + expect(command.renderState.cull.face).not.toBe(CullFace.FRONT); + } + }); + }); + + it('loadSiblings', function() { + viewBottomLeft(); + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url, { + loadSiblings : false, + baseScreenSpaceError: 1000000000 + }).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.numberOfTilesWithContentReady).toBe(2); + tileset.loadSiblings = true; + scene.renderForSpecs(); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + expect(statistics.numberOfTilesWithContentReady).toBe(5); + }); + }); + }); + + it('immediatelyLoadDesiredLevelOfDetail', function() { + viewBottomLeft(); + var tileset = scene.primitives.add(new Cesium3DTileset({ + url : tilesetOfTilesetsUrl, + immediatelyLoadDesiredLevelOfDetail : true + })); + return Cesium3DTilesTester.waitForReady(scene, tileset).then(function(tileset) { + scene.renderForSpecs(); + return tileset._root.contentReadyPromise.then(function() { + tileset._root.refine = Cesium3DTileRefine.REPLACE; + tileset._root.children[0].refine = Cesium3DTileRefine.REPLACE; + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + var statistics = tileset._statistics; + expect(statistics.numberOfTilesWithContentReady).toBe(1); + }); + }); + }); + }); + + it('selects children if no ancestors available', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { + var statistics = tileset._statistics; + var parent = tileset._root.children[0]; + var child = parent.children[3].children[0]; + parent.refine = Cesium3DTileRefine.REPLACE; + parent.unloadContent(); + + viewBottomLeft(); + scene.renderForSpecs(); + + expect(child.contentReady).toBe(true); + expect(parent.contentReady).toBe(false); + expect(child.selected).toBe(true); + expect(parent.selected).toBe(false); + expect(statistics.numberOfCommands).toEqual(1); + }); + }); + + it('tile expires', function() { + return Cesium3DTilesTester.loadTileset(scene, batchedExpirationUrl).then(function(tileset) { + // Intercept the request and load content that produces more draw commands, to simulate fetching new content after the original expires + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + loadWithXhr.defaultLoad(batchedColorsB3dmUrl, responseType, method, data, headers, deferred, overrideMimeType); + }); + var tile = tileset._root; + var statistics = tileset._statistics; + var expiredContent; + + // Check that expireDuration and expireDate are correctly set + var expireDate = JulianDate.addSeconds(JulianDate.now(), 5.0, new JulianDate()); + expect(JulianDate.secondsDifference(tile.expireDate, expireDate)).toEqualEpsilon(0.0, CesiumMath.EPSILON1); + expect(tile.expireDuration).toBe(5.0); + expect(tile.contentExpired).toBe(false); + expect(tile.contentReady).toBe(true); + expect(tile.contentAvailable).toBe(true); + expect(tile._expiredContent).toBeUndefined(); + + // Check statistics + expect(statistics.numberOfCommands).toBe(1); + expect(statistics.numberOfTilesTotal).toBe(1); + + // Trigger expiration to happen next frame + tile.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); + + // Stays in the expired state until the request goes through + var originalMaxmimumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; // Artificially limit Request Scheduler so the request won't go through + scene.renderForSpecs(); + RequestScheduler.maximumRequests = originalMaxmimumRequests; + expiredContent = tile._expiredContent; + expect(tile.contentExpired).toBe(true); + expect(tile.contentAvailable).toBe(true); // Expired content now exists + expect(expiredContent).toBeDefined(); + + // Expired content renders while new content loads in + expect(statistics.numberOfCommands).toBe(1); + expect(statistics.numberOfTilesTotal).toBe(1); + + // Request goes through, now in the LOADING state + scene.renderForSpecs(); + expect(tile.contentExpired).toBe(false); + expect(tile.contentReady).toBe(false); + expect(tile.contentAvailable).toBe(true); + expect(tile._contentState).toBe(Cesium3DTileContentState.LOADING); + expect(tile._expiredContent).toBeDefined(); // Still holds onto expired content until the content state is READY + + // Check that url contains a query param with the timestamp + var url = loadWithXhr.load.calls.first().args[0]; + expect(url.indexOf('expired=') >= 0).toBe(true); + + // statistics are still the same + expect(statistics.numberOfCommands).toBe(1); + expect(statistics.numberOfTilesTotal).toBe(1); + + return pollToPromise(function() { + expect(statistics.numberOfCommands).toBe(1); // Still renders expired content + scene.renderForSpecs(); + return tile.contentReady; + }).then(function() { + scene.renderForSpecs(); + + // Expired content is destroyed + expect(tile._expiredContent).toBeUndefined(); + expect(expiredContent.isDestroyed()).toBe(true); + + // statistics for new content + expect(statistics.numberOfCommands).toBe(10); + expect(statistics.numberOfTilesTotal).toBe(1); + }); + }); + }); + + function modifySubtreeBuffer(arrayBuffer) { + var uint8Array = new Uint8Array(arrayBuffer); + var jsonString = getStringFromTypedArray(uint8Array); + var json = JSON.parse(jsonString); + json.root.children.splice(0, 1); + + jsonString = JSON.stringify(json); + var length = jsonString.length; + uint8Array = new Uint8Array(length); + for (var i = 0; i < length; i++) { + uint8Array[i] = jsonString.charCodeAt(i); + } + return uint8Array.buffer; + } + + it('tile with tileset content expires', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetSubtreeExpirationUrl).then(function(tileset) { + // Intercept the request and load a subtree with one less child. Still want to make an actual request to simulate + // real use cases instead of immediately returning a pre-created array buffer. + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + var newDeferred = when.defer(); + loadWithXhr.defaultLoad(tilesetSubtreeUrl, responseType, method, data, headers, newDeferred, overrideMimeType); + newDeferred.promise.then(function(arrayBuffer) { + deferred.resolve(modifySubtreeBuffer(arrayBuffer)); + }); + }); + + var subtreeRoot = tileset._root.children[0]; + var subtreeChildren = subtreeRoot.children[0].children; + var childrenLength = subtreeChildren.length; + var statistics = tileset._statistics; + + // Check statistics + expect(statistics.numberOfCommands).toBe(5); + expect(statistics.numberOfTilesTotal).toBe(7); + expect(statistics.numberOfTilesWithContentReady).toBe(5); + + // Trigger expiration to happen next frame + subtreeRoot.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); + + // Listen to tile unload events + var spyUpdate = jasmine.createSpy('listener'); + tileset.tileUnload.addEventListener(spyUpdate); + + // Tiles in the subtree are removed from the cache and destroyed. + scene.renderForSpecs(); // Becomes expired + scene.renderForSpecs(); // Makes request + expect(subtreeRoot.children).toEqual([]); + for (var i = 0; i < childrenLength; ++i) { + expect(subtreeChildren[0].isDestroyed()).toBe(true); + } + expect(spyUpdate.calls.count()).toEqual(4); + + // Remove the spy so new tiles load in normally + loadWithXhr.load = loadWithXhr.defaultLoad; + + // Wait for the new tileset content to come in with one less leaf + return pollToPromise(function() { + scene.renderForSpecs(); + return subtreeRoot.contentReady && tileset.tilesLoaded; + }).then(function() { + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toBe(4); + expect(statistics.numberOfTilesTotal).toBe(6); + expect(statistics.numberOfTilesWithContentReady).toBe(4); + }); + }); + }); + + it('tile expires and request fails', function() { + return Cesium3DTilesTester.loadTileset(scene, batchedExpirationUrl).then(function(tileset) { + spyOn(loadWithXhr, 'load').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) { + deferred.reject(); + }); + var tile = tileset._root; + var statistics = tileset._statistics; + + // Trigger expiration to happen next frame + tile.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); + + // After update the tile is expired + scene.renderForSpecs(); + + // Make request (it will fail) + scene.renderForSpecs(); + + // Render scene + scene.renderForSpecs(); + expect(tile._contentState).toBe(Cesium3DTileContentState.FAILED); + expect(statistics.numberOfCommands).toBe(0); + expect(statistics.numberOfTilesTotal).toBe(1); + }); + }); + + it('tile expiration date', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var tile = tileset._root; + + // Trigger expiration to happen next frame + tile.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); + + // Stays in the expired state until the request goes through + scene.renderForSpecs(); + expect(tile.contentExpired).toBe(true); + + return pollToPromise(function() { + scene.renderForSpecs(); + return tile.contentReady; + }).then(function() { + scene.renderForSpecs(); + expect(tile._expiredContent).toBeUndefined(); + expect(tile.expireDate).toBeUndefined(); + }); + }); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/Composite3DTileContentSpec.js b/Specs/Scene/Composite3DTileContentSpec.js new file mode 100644 index 000000000000..6c0111372c8a --- /dev/null +++ b/Specs/Scene/Composite3DTileContentSpec.js @@ -0,0 +1,129 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Composite3DTileContent', + 'Core/Cartesian3', + 'Core/Color', + 'Core/HeadingPitchRange', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene' + ], function( + Composite3DTileContent, + Cartesian3, + Color, + HeadingPitchRange, + Cesium3DTilesTester, + createScene) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + var compositeUrl = './Data/Cesium3DTiles/Composite/Composite/'; + var compositeOfComposite = './Data/Cesium3DTiles/Composite/CompositeOfComposite/'; + + beforeAll(function() { + scene = createScene(); + // One item in each data set is always located in the center, so point the camera there + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 30.0)); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + function expectRenderComposite(tileset) { + expect(scene).toPickAndCall(function(result) { + // Pick a building + var pickedBuilding = result; + expect(pickedBuilding).toBeDefined(); + + // Change the color of the picked building to yellow + pickedBuilding.color = Color.clone(Color.YELLOW, pickedBuilding.color); + + // Expect the pixel color to be some shade of yellow + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Both a building and instance are located at the center, hide the building and pick the instance + pickedBuilding.show = false; + + var pickedInstance; + expect(scene).toPickAndCall(function(result) { + pickedInstance = result; + expect(pickedInstance).toBeDefined(); + expect(pickedInstance).not.toEqual(pickedBuilding); + }); + + // Change the color of the picked instance to green + pickedInstance.color = Color.clone(Color.GREEN, pickedInstance.color); + + // Expect the pixel color to be some shade of green + Cesium3DTilesTester.expectRender(scene, tileset, function(rgba) { + expect(rgba[0]).toEqual(0); + expect(rgba[1]).toBeGreaterThan(0); + expect(rgba[2]).toEqual(0); + expect(rgba[3]).toEqual(255); + }); + + // Hide the instance, and expect the render to be blank + pickedInstance.show = false; + Cesium3DTilesTester.expectRenderBlank(scene, tileset); + }); + } + + it('throws with invalid version', function() { + var arrayBuffer = Cesium3DTilesTester.generateCompositeTileBuffer({ + version : 2 + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'cmpt'); + }); + + it('throws with invalid inner tile content type', function() { + var arrayBuffer = Cesium3DTilesTester.generateCompositeTileBuffer({ + tiles : [Cesium3DTilesTester.generateInstancedTileBuffer({ + magic : [120, 120, 120, 120] + })] + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'cmpt'); + }); + + it('resolves readyPromise', function() { + return Cesium3DTilesTester.resolvesReadyPromise(scene, compositeUrl); + }); + + it('rejects readyPromise on error', function() { + // Try loading a composite tile with an instanced tile that has an invalid url. + // Expect promise to be rejected in Model, ModelInstanceCollection, + // Instanced3DModel3DTileContent, and Composite3DTileContent. + var arrayBuffer = Cesium3DTilesTester.generateCompositeTileBuffer({ + tiles : [Cesium3DTilesTester.generateInstancedTileBuffer({ + gltfFormat : 0, + gltfUri : 'invalid' + })] + }); + return Cesium3DTilesTester.rejectsReadyPromiseOnError(scene, arrayBuffer, 'cmpt'); + }); + + it('renders composite', function() { + return Cesium3DTilesTester.loadTileset(scene, compositeUrl).then(expectRenderComposite); + }); + + it('renders composite of composite', function() { + return Cesium3DTilesTester.loadTileset(scene, compositeOfComposite).then(expectRenderComposite); + }); + + it('destroys', function() { + return Cesium3DTilesTester.tileDestroys(scene, compositeUrl); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/ConditionsExpressionSpec.js b/Specs/Scene/ConditionsExpressionSpec.js new file mode 100644 index 000000000000..e1d7650e5464 --- /dev/null +++ b/Specs/Scene/ConditionsExpressionSpec.js @@ -0,0 +1,123 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/ConditionsExpression', + 'Core/Cartesian4', + 'Core/Color' + ], function( + ConditionsExpression, + Cartesian4, + Color) { + 'use strict'; + + var frameState = {}; + + function MockFeature(value) { + this._value = value; + } + + MockFeature.prototype.getProperty = function() { + return this._value; + }; + + var jsonExp = { + conditions : [ + ['${Height} > 100', 'color("blue")'], + ['${Height} > 50', 'color("red")'], + ['true', 'color("lime")'] + ] + }; + + var additionalExpressions = { + halfHeight: '${Height}/2', + quarterHeight: '${Height}/4' + }; + + var jsonExpWithAdditionalExpressions = { + conditions : [ + ['${halfHeight} > 50 && ${halfHeight} < 100', 'color("blue")'], + ['${quarterHeight} > 50 && ${quarterHeight} < 52', 'color("red")'], + ['true', 'color("lime")'] + ] + }; + + it('constructs', function() { + var expression = new ConditionsExpression(jsonExp); + expect(expression.conditionsExpression).toEqual(jsonExp); + }); + + it('evaluates conditional', function() { + var expression = new ConditionsExpression(jsonExp); + expect(expression.evaluateColor(frameState, new MockFeature(101))).toEqual(Color.BLUE); + expect(expression.evaluateColor(frameState, new MockFeature(52))).toEqual(Color.RED); + expect(expression.evaluateColor(frameState, new MockFeature(3))).toEqual(Color.LIME); + }); + + it('evaluates conditional with additional expressions', function() { + var expression = new ConditionsExpression(jsonExpWithAdditionalExpressions, additionalExpressions); + expect(expression.evaluateColor(frameState, new MockFeature(101))).toEqual(Color.BLUE); + expect(expression.evaluateColor(frameState, new MockFeature(52))).toEqual(Color.LIME); + expect(expression.evaluateColor(frameState, new MockFeature(3))).toEqual(Color.LIME); + }); + + it('evaluate takes result argument', function() { + var result = new Cartesian4(); + var expression = new ConditionsExpression(jsonExpWithAdditionalExpressions, additionalExpressions, result); + var value = expression.evaluate(frameState, new MockFeature(101), result); + expect(value).toEqual(new Cartesian4(0.0, 0.0, 1.0, 1.0)); + expect(value).toBe(result); + }); + + it('evaluate takes a color result argument', function() { + var result = new Color(); + var expression = new ConditionsExpression(jsonExpWithAdditionalExpressions, additionalExpressions, result); + var value = expression.evaluate(frameState, new MockFeature(101), result); + expect(value).toEqual(Color.BLUE); + expect(value).toBe(result); + }); + + it('constructs and evaluates empty conditional', function() { + var expression = new ConditionsExpression({ + "conditions" : [] + }); + expect(expression._conditions).toEqual([]); + expect(expression.evaluate(frameState, new MockFeature(101))).toEqual(undefined); + expect(expression.evaluate(frameState, new MockFeature(52))).toEqual(undefined); + expect(expression.evaluate(frameState, new MockFeature(3))).toEqual(undefined); + }); + + it('constructs and evaluates empty', function() { + var expression = new ConditionsExpression([]); + expect(expression._conditions).toEqual(undefined); + expect(expression.evaluate(frameState, new MockFeature(101))).toEqual(undefined); + expect(expression.evaluate(frameState, new MockFeature(52))).toEqual(undefined); + expect(expression.evaluate(frameState, new MockFeature(3))).toEqual(undefined); + }); + + it('gets shader function', function() { + var expression = new ConditionsExpression(jsonExp); + var shaderFunction = expression.getShaderFunction('getColor', '', {}, 'vec4'); + var expected = 'vec4 getColor() \n' + + '{ \n' + + ' if ((Height > 100.0)) \n' + + ' { \n' + + ' return vec4(vec3(0.0, 0.0, 1.0), 1.0); \n' + + ' } \n' + + ' else if ((Height > 50.0)) \n' + + ' { \n' + + ' return vec4(vec3(1.0, 0.0, 0.0), 1.0); \n' + + ' } \n' + + ' else if (true) \n' + + ' { \n' + + ' return vec4(vec3(0.0, 1.0, 0.0), 1.0); \n' + + ' } \n' + + ' return vec4(1.0); \n' + + '} \n'; + expect(shaderFunction).toEqual(expected); + }); + + it('return undefined shader function when there are no conditions', function() { + var expression = new ConditionsExpression([]); + var shaderFunction = expression.getShaderFunction('getColor', '', {}, 'vec4'); + expect(shaderFunction).toBeUndefined(); + }); +}); diff --git a/Specs/Scene/Empty3DTileContentSpec.js b/Specs/Scene/Empty3DTileContentSpec.js new file mode 100644 index 000000000000..86dbf32b9438 --- /dev/null +++ b/Specs/Scene/Empty3DTileContentSpec.js @@ -0,0 +1,32 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Empty3DTileContent' + ], function( + Empty3DTileContent) { + 'use strict'; + + it('destroys', function() { + var content = new Empty3DTileContent(); + expect(content.isDestroyed()).toEqual(false); + content.destroy(); + expect(content.isDestroyed()).toEqual(true); + }); + + it('gets properties', function() { + var mockTileset = {}; + var mockTile = {}; + var content = new Empty3DTileContent(mockTileset, mockTile); + expect(content.featuresLength).toBe(0); + expect(content.pointsLength).toBe(0); + expect(content.trianglesLength).toBe(0); + expect(content.geometryByteLength).toBe(0); + expect(content.texturesByteLength).toBe(0); + expect(content.batchTableByteLength).toBe(0); + expect(content.innerContents).toBeUndefined(); + expect(content.readyPromise).toBeUndefined(); + expect(content.tileset).toBe(mockTileset); + expect(content.tile).toBe(mockTile); + expect(content.url).toBeUndefined(); + expect(content.batchTable).toBeUndefined(); + }); +}); diff --git a/Specs/Scene/ExpressionSpec.js b/Specs/Scene/ExpressionSpec.js new file mode 100644 index 000000000000..540befded12f --- /dev/null +++ b/Specs/Scene/ExpressionSpec.js @@ -0,0 +1,3626 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Expression', + 'Core/Cartesian2', + 'Core/Cartesian3', + 'Core/Cartesian4', + 'Core/Color', + 'Core/Math', + 'Scene/ExpressionNodeType' + ], function( + Expression, + Cartesian2, + Cartesian3, + Cartesian4, + Color, + CesiumMath, + ExpressionNodeType) { + 'use strict'; + + var frameState = {}; + + function MockFeature() { + this._properties = {}; + this._className = undefined; + this._inheritedClassName = undefined; + this.content = { + tileset : { + timeSinceLoad : 0.0 + } + }; + } + + MockFeature.prototype.addProperty = function(name, value) { + this._properties[name] = value; + }; + + MockFeature.prototype.getProperty = function(name) { + return this._properties[name]; + }; + + MockFeature.prototype.setClass = function(className) { + this._className = className; + }; + + MockFeature.prototype.setInheritedClass = function(className) { + this._inheritedClassName = className; + }; + + MockFeature.prototype.isExactClass = function(className) { + return this._className === className; + }; + + MockFeature.prototype.isClass = function(className) { + return (this._className === className) || (this._inheritedClassName === className); + }; + + MockFeature.prototype.getExactClassName = function() { + return this._className; + }; + + it('parses backslashes', function() { + var expression = new Expression('"\\he\\\\\\ll\\\\o"'); + expect(expression.evaluate(frameState, undefined)).toEqual('\\he\\\\\\ll\\\\o'); + }); + + it('evaluates variable', function() { + var feature = new MockFeature(); + feature.addProperty('height', 10); + feature.addProperty('width', 5); + feature.addProperty('string', 'hello'); + feature.addProperty('boolean', true); + feature.addProperty('vector', Cartesian3.UNIT_X); + feature.addProperty('null', null); + feature.addProperty('undefined', undefined); + + var expression = new Expression('${height}'); + expect(expression.evaluate(frameState, feature)).toEqual(10); + + expression = new Expression('\'${height}\''); + expect(expression.evaluate(frameState, feature)).toEqual('10'); + + expression = new Expression('${height}/${width}'); + expect(expression.evaluate(frameState, feature)).toEqual(2); + + expression = new Expression('${string}'); + expect(expression.evaluate(frameState, feature)).toEqual('hello'); + + expression = new Expression('\'replace ${string}\''); + expect(expression.evaluate(frameState, feature)).toEqual('replace hello'); + + expression = new Expression('\'replace ${string} multiple ${height}\''); + expect(expression.evaluate(frameState, feature)).toEqual('replace hello multiple 10'); + + expression = new Expression('"replace ${string}"'); + expect(expression.evaluate(frameState, feature)).toEqual('replace hello'); + + expression = new Expression('\'replace ${string\''); + expect(expression.evaluate(frameState, feature)).toEqual('replace ${string'); + + expression = new Expression('${boolean}'); + expect(expression.evaluate(frameState, feature)).toEqual(true); + + expression = new Expression('\'${boolean}\''); + expect(expression.evaluate(frameState, feature)).toEqual('true'); + + expression = new Expression('${vector}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian3.UNIT_X); + + expression = new Expression('\'${vector}\''); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian3.UNIT_X.toString()); + + expression = new Expression('${null}'); + expect(expression.evaluate(frameState, feature)).toEqual(null); + + expression = new Expression('\'${null}\''); + expect(expression.evaluate(frameState, feature)).toEqual(''); + + expression = new Expression('${undefined}'); + expect(expression.evaluate(frameState, feature)).toEqual(undefined); + + expression = new Expression('\'${undefined}\''); + expect(expression.evaluate(frameState, feature)).toEqual(''); + + expect(function() { + return new Expression('${height'); + }).toThrowRuntimeError(); + }); + + it('evaluates with additional expressions', function() { + var additionalExpressions = { + halfHeight: '${Height}/2' + }; + var feature = new MockFeature(); + feature.addProperty('Height', 10); + + var expression = new Expression('${halfHeight}', additionalExpressions); + expect(expression.evaluate(frameState, feature)).toEqual(5); + }); + + it('evaluate takes result argument', function() { + var expression = new Expression('vec3(1.0)'); + var result = new Cartesian3(); + var value = expression.evaluate(frameState, undefined, result); + expect(value).toEqual(new Cartesian3(1.0, 1.0, 1.0)); + expect(value).toBe(result); + }); + + it('evaluate takes a color result argument', function() { + var expression = new Expression('color("red")'); + var result = new Color(); + var value = expression.evaluate(frameState, undefined, result); + expect(value).toEqual(Color.RED); + expect(value).toBe(result); + }); + + it('gets expressions', function() { + var expressionString = "(regExp('^Chest').test(${County})) && (${YearBuilt} >= 1970)"; + var expression = new Expression(expressionString); + expect(expression.expression).toEqual(expressionString); + }); + + it('throws on invalid expressions', function() { + expect(function() { + return new Expression(false); + }).toThrowDeveloperError(); + + expect(function() { + return new Expression(''); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('this'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('2; 3;'); + }).toThrowRuntimeError(); + }); + + it('throws on unknown characters', function() { + expect(function() { + return new Expression('#'); + }).toThrowRuntimeError(); + }); + + it('throws on unmatched parenthesis', function() { + expect(function() { + return new Expression('((true)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('(true))'); + }).toThrowRuntimeError(); + }); + + it('throws on unknown identifiers', function() { + expect(function() { + return new Expression('flse'); + }).toThrowRuntimeError(); + }); + + it('throws on unknown function calls', function() { + expect(function() { + return new Expression('unknown()'); + }).toThrowRuntimeError(); + }); + + it('throws on unknown member function calls', function() { + expect(function() { + return new Expression('regExp().unknown()'); + }).toThrowRuntimeError(); + }); + + it('throws with unsupported operators', function() { + expect(function() { + return new Expression('~1'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('2 | 3'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('2 & 3'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('2 << 3'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('2 >> 3'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('2 >>> 3'); + }).toThrowRuntimeError(); + }); + + it('evaluates literal null', function() { + var expression = new Expression('null'); + expect(expression.evaluate(frameState, undefined)).toEqual(null); + }); + + it('evaluates literal undefined', function() { + var expression = new Expression('undefined'); + expect(expression.evaluate(frameState, undefined)).toEqual(undefined); + }); + + it('evaluates literal boolean', function() { + var expression = new Expression('true'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('false'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + }); + + it('converts to literal boolean', function() { + var expression = new Expression('Boolean()'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('Boolean(1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('Boolean("true")'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + }); + + it('evaluates literal number', function() { + var expression = new Expression('1'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('0'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('NaN'); + expect(expression.evaluate(frameState, undefined)).toEqual(NaN); + + expression = new Expression('Infinity'); + expect(expression.evaluate(frameState, undefined)).toEqual(Infinity); + }); + + it('evaluates math constants', function() { + var expression = new Expression('Math.PI'); + expect(expression.evaluate(frameState, undefined)).toEqual(Math.PI); + + expression = new Expression('Math.E'); + expect(expression.evaluate(frameState, undefined)).toEqual(Math.E); + }); + + it('converts to literal number', function() { + var expression = new Expression('Number()'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('Number("1")'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('Number(true)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + }); + + it('evaluates literal string', function() { + var expression = new Expression('\'hello\''); + expect(expression.evaluate(frameState, undefined)).toEqual('hello'); + + expression = new Expression('\'Cesium\''); + expect(expression.evaluate(frameState, undefined)).toEqual('Cesium'); + + expression = new Expression('"Cesium"'); + expect(expression.evaluate(frameState, undefined)).toEqual('Cesium'); + }); + + it('converts to literal string', function() { + var expression = new Expression('String()'); + expect(expression.evaluate(frameState, undefined)).toEqual(''); + + expression = new Expression('String(1)'); + expect(expression.evaluate(frameState, undefined)).toEqual('1'); + + expression = new Expression('String(true)'); + expect(expression.evaluate(frameState, undefined)).toEqual('true'); + }); + + it('evaluates literal color', function() { + var expression = new Expression('color(\'#ffffff\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('color(\'#00FFFF\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.CYAN)); + + expression = new Expression('color(\'#fff\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('color(\'#0FF\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.CYAN)); + + expression = new Expression('color(\'white\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('color(\'cyan\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.CYAN)); + + expression = new Expression('color(\'white\', 0.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.fromAlpha(Color.WHITE, 0.5))); + + expression = new Expression('rgb(255, 255, 255)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('rgb(100, 255, 190)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.fromBytes(100, 255, 190))); + + expression = new Expression('hsl(0, 0, 1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('hsl(1.0, 0.6, 0.7)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.fromHsl(1.0, 0.6, 0.7))); + + expression = new Expression('rgba(255, 255, 255, 0.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.fromAlpha(Color.WHITE, 0.5))); + + expression = new Expression('rgba(100, 255, 190, 0.25)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.fromBytes(100, 255, 190, 0.25 * 255))); + + expression = new Expression('hsla(0, 0, 1, 0.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(new Color(1.0, 1.0, 1.0, 0.5))); + + expression = new Expression('hsla(1.0, 0.6, 0.7, 0.75)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.fromHsl(1.0, 0.6, 0.7, 0.75))); + + expression = new Expression('color()'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.WHITE)); + }); + + it('evaluates literal color with result parameter', function() { + var color = new Color(); + + var expression = new Expression('color(\'#0000ff\')'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(Color.BLUE); + expect(color).toEqual(Color.BLUE); + + expression = new Expression('color(\'#f00\')'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(Color.RED); + expect(color).toEqual(Color.RED); + + expression = new Expression('color(\'cyan\')'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(Color.CYAN); + expect(color).toEqual(Color.CYAN); + + expression = new Expression('color(\'white\', 0.5)'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(new Color(1.0, 1.0, 1.0, 0.5)); + expect(color).toEqual(new Color(1.0, 1.0, 1.0, 0.5)); + + expression = new Expression('rgb(0, 0, 0)'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(Color.BLACK); + expect(color).toEqual(Color.BLACK); + + expression = new Expression('hsl(0, 0, 1)'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(Color.WHITE); + expect(color).toEqual(Color.WHITE); + + expression = new Expression('rgba(255, 0, 255, 0.5)'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(new Color(1.0, 0, 1.0, 0.5)); + expect(color).toEqual(new Color(1.0, 0, 1.0, 0.5)); + + expression = new Expression('hsla(0, 0, 1, 0.5)'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(new Color(1.0, 1.0, 1.0, 0.5)); + expect(color).toEqual(new Color(1.0, 1.0, 1.0, 0.5)); + + expression = new Expression('color()'); + expect(expression.evaluate(frameState, undefined, color)).toEqual(Color.WHITE); + expect(color).toEqual(Color.WHITE); + }); + + it('evaluates color with expressions as arguments', function() { + var feature = new MockFeature(); + feature.addProperty('hex6', '#ffffff'); + feature.addProperty('hex3', '#fff'); + feature.addProperty('keyword', 'white'); + feature.addProperty('alpha', 0.2); + + var expression = new Expression('color(${hex6})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('color(${hex3})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('color(${keyword})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('color(${keyword}, ${alpha} + 0.6)'); + expect(expression.evaluate(frameState, feature).x).toEqual(1.0); + expect(expression.evaluate(frameState, feature).y).toEqual(1.0); + expect(expression.evaluate(frameState, feature).z).toEqual(1.0); + expect(expression.evaluate(frameState, feature).w).toEqual(0.8); + }); + + it('evaluates rgb with expressions as arguments', function() { + var feature = new MockFeature(); + feature.addProperty('red', 100); + feature.addProperty('green', 200); + feature.addProperty('blue', 255); + + var expression = new Expression('rgb(${red}, ${green}, ${blue})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromBytes(100, 200, 255))); + + expression = new Expression('rgb(${red}/2, ${green}/2, ${blue})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromBytes(50, 100, 255))); + }); + + it('evaluates hsl with expressions as arguments', function() { + var feature = new MockFeature(); + feature.addProperty('h', 0.0); + feature.addProperty('s', 0.0); + feature.addProperty('l', 1.0); + + var expression = new Expression('hsl(${h}, ${s}, ${l})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('hsl(${h} + 0.2, ${s} + 1.0, ${l} - 0.5)'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromHsl(0.2, 1.0, 0.5))); + }); + + it('evaluates rgba with expressions as arguments', function() { + var feature = new MockFeature(); + feature.addProperty('red', 100); + feature.addProperty('green', 200); + feature.addProperty('blue', 255); + feature.addProperty('a', 0.3); + + var expression = new Expression('rgba(${red}, ${green}, ${blue}, ${a})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromBytes(100, 200, 255, 0.3*255))); + + expression = new Expression('rgba(${red}/2, ${green}/2, ${blue}, ${a} * 2)'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromBytes(50, 100, 255, 0.6*255))); + }); + + it('evaluates hsla with expressions as arguments', function() { + var feature = new MockFeature(); + feature.addProperty('h', 0.0); + feature.addProperty('s', 0.0); + feature.addProperty('l', 1.0); + feature.addProperty('a', 1.0); + + var expression = new Expression('hsla(${h}, ${s}, ${l}, ${a})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('hsla(${h} + 0.2, ${s} + 1.0, ${l} - 0.5, ${a} / 4)'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromHsl(0.2, 1.0, 0.5, 0.25))); + }); + + it('evaluates rgba with expressions as arguments', function() { + var feature = new MockFeature(); + feature.addProperty('red', 100); + feature.addProperty('green', 200); + feature.addProperty('blue', 255); + feature.addProperty('alpha', 0.5); + + var expression = new Expression('rgba(${red}, ${green}, ${blue}, ${alpha})'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromBytes(100, 200, 255, 0.5 * 255))); + + expression = new Expression('rgba(${red}/2, ${green}/2, ${blue}, ${alpha} + 0.1)'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.fromColor(Color.fromBytes(50, 100, 255, 0.6 * 255))); + }); + + it('color constructors throw with wrong number of arguments', function() { + expect(function() { + return new Expression('rgb(255, 255)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('hsl(1, 1)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('rgba(255, 255, 255)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('hsla(1, 1, 1)'); + }).toThrowRuntimeError(); + }); + + it('evaluates color properties (r, g, b, a)', function() { + var expression = new Expression('color(\'#ffffff\').r'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgb(255, 255, 0).g'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('color("cyan").b'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgba(255, 255, 0, 0.5).a'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.5); + }); + + it('evaluates color properties (x, y, z, w)', function() { + var expression = new Expression('color(\'#ffffff\').x'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgb(255, 255, 0).y'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('color("cyan").z'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgba(255, 255, 0, 0.5).w'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.5); + }); + + it('evaluates color properties ([0], [1], [2]. [3])', function() { + var expression = new Expression('color(\'#ffffff\')[0]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgb(255, 255, 0)[1]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('color("cyan")[2]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgba(255, 255, 0, 0.5)[3]'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.5); + }); + + it('evaluates color properties (["r"], ["g"], ["b"], ["a"])', function() { + var expression = new Expression('color(\'#ffffff\')["r"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgb(255, 255, 0)["g"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('color("cyan")["b"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgba(255, 255, 0, 0.5)["a"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.5); + }); + + it('evaluates color properties (["x"], ["y"], ["z"], ["w"])', function() { + var expression = new Expression('color(\'#ffffff\')["x"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgb(255, 255, 0)["y"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('color("cyan")["z"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('rgba(255, 255, 0, 0.5)["w"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.5); + }); + + it('evaluates vec2', function() { + var expression = new Expression('vec2(2.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(2.0, 2.0)); + + expression = new Expression('vec2(3.0, 4.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(3.0, 4.0)); + + expression = new Expression('vec2(vec2(3.0, 4.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(3.0, 4.0)); + + expression = new Expression('vec2(vec3(3.0, 4.0, 5.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(3.0, 4.0)); + + expression = new Expression('vec2(vec4(3.0, 4.0, 5.0, 6.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(3.0, 4.0)); + }); + + it('throws if vec2 has invalid number of arguments', function() { + var expression = new Expression('vec2()'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec2(3.0, 4.0, 5.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec2(vec2(3.0, 4.0), 5.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('throws if vec2 has invalid argument', function() { + var expression = new Expression('vec2("1")'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates vec3', function() { + var expression = new Expression('vec3(2.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(2.0, 2.0, 2.0)); + + expression = new Expression('vec3(3.0, 4.0, 5.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3.0, 4.0, 5.0)); + + expression = new Expression('vec3(vec2(3.0, 4.0), 5.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3.0, 4.0, 5.0)); + + expression = new Expression('vec3(3.0, vec2(4.0, 5.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3.0, 4.0, 5.0)); + + expression = new Expression('vec3(vec3(3.0, 4.0, 5.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3.0, 4.0, 5.0)); + + expression = new Expression('vec3(vec4(3.0, 4.0, 5.0, 6.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3.0, 4.0, 5.0)); + }); + + it ('throws if vec3 has invalid number of arguments', function() { + var expression = new Expression('vec3()'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec3(3.0, 4.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec3(3.0, 4.0, 5.0, 6.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec3(vec2(3.0, 4.0), vec2(5.0, 6.0))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec3(vec4(3.0, 4.0, 5.0, 6.0), 1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('throws if vec3 has invalid argument', function() { + var expression = new Expression('vec3(1.0, "1.0", 2.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates vec4', function() { + var expression = new Expression('vec4(2.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(2.0, 2.0, 2.0, 2.0)); + + expression = new Expression('vec4(3.0, 4.0, 5.0, 6.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3.0, 4.0, 5.0, 6.0)); + + expression = new Expression('vec4(vec2(3.0, 4.0), 5.0, 6.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3.0, 4.0, 5.0, 6.0)); + + expression = new Expression('vec4(3.0, vec2(4.0, 5.0), 6.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3.0, 4.0, 5.0, 6.0)); + + expression = new Expression('vec4(3.0, 4.0, vec2(5.0, 6.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3.0, 4.0, 5.0, 6.0)); + + expression = new Expression('vec4(vec3(3.0, 4.0, 5.0), 6.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3.0, 4.0, 5.0, 6.0)); + + expression = new Expression('vec4(3.0, vec3(4.0, 5.0, 6.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3.0, 4.0, 5.0, 6.0)); + + expression = new Expression('vec4(vec4(3.0, 4.0, 5.0, 6.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3.0, 4.0, 5.0, 6.0)); + }); + + it ('throws if vec4 has invalid number of arguments', function() { + var expression = new Expression('vec4()'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec4(3.0, 4.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec4(3.0, 4.0, 5.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec4(3.0, 4.0, 5.0, 6.0, 7.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec4(vec3(3.0, 4.0, 5.0))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('throws if vec4 has invalid argument', function() { + var expression = new Expression('vec4(1.0, "2.0", 3.0, 4.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates vector with expressions as arguments', function() { + var feature = new MockFeature(); + feature.addProperty('height', 2); + feature.addProperty('width', 4); + feature.addProperty('depth', 3); + feature.addProperty('scale', 1); + + var expression = new Expression('vec4(${height}, ${width}, ${depth}, ${scale})'); + expect(expression.evaluate(frameState, feature)).toEqual(new Cartesian4(2.0, 4.0, 3.0, 1.0)); + }); + + it('evaluates expression with multiple nested vectors', function() { + var expression = new Expression('vec4(vec2(1, 2)[vec3(6, 1, 5).y], 2, vec4(1.0).w, 5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(2.0, 2.0, 1.0, 5.0)); + }); + + it('evaluates vector properties (x, y, z, w)', function() { + var expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).x'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).y'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).z'); + expect(expression.evaluate(frameState, undefined)).toEqual(3.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).w'); + expect(expression.evaluate(frameState, undefined)).toEqual(4.0); + }); + + it('evaluates vector properties (r, g, b, a)', function() { + var expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).r'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).g'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).b'); + expect(expression.evaluate(frameState, undefined)).toEqual(3.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0).a'); + expect(expression.evaluate(frameState, undefined)).toEqual(4.0); + }); + + it('evaluates vector properties ([0], [1], [2], [3])', function() { + var expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)[0]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)[1]'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)[2]'); + expect(expression.evaluate(frameState, undefined)).toEqual(3.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)[3]'); + expect(expression.evaluate(frameState, undefined)).toEqual(4.0); + }); + + it('evaluates vector properties (["x"], ["y"], ["z"]. ["w"])', function() { + var expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["x"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["y"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["z"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(3.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["w"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(4.0); + }); + + it('evaluates vector properties (["r"], ["g"], ["b"]. ["a"])', function() { + var expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["r"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["g"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["b"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(3.0); + + expression = new Expression('vec4(1.0, 2.0, 3.0, 4.0)["a"]'); + expect(expression.evaluate(frameState, undefined)).toEqual(4.0); + }); + + it('evaluates unary not', function() { + var expression = new Expression('!true'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('!!true'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + }); + + it('throws if unary not takes invalid argument', function() { + var expression = new Expression('!"true"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates unary negative', function() { + var expression = new Expression('-5'); + expect(expression.evaluate(frameState, undefined)).toEqual(-5); + + expression = new Expression('-(-5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(5); + }); + + it('throws if unary negative takes invalid argument', function() { + var expression = new Expression('-"56"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates unary positive', function() { + var expression = new Expression('+5'); + expect(expression.evaluate(frameState, undefined)).toEqual(5); + }); + + it('throws if unary positive takes invalid argument', function() { + var expression = new Expression('+"56"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary addition', function() { + var expression = new Expression('1 + 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(3); + + expression = new Expression('1 + 2 + 3 + 4'); + expect(expression.evaluate(frameState, undefined)).toEqual(10); + }); + + it('evaluates binary addition with strings', function() { + var expression = new Expression('1 + "10"'); + expect(expression.evaluate(frameState, undefined)).toEqual('110'); + + expression = new Expression('"10" + 1'); + expect(expression.evaluate(frameState, undefined)).toEqual('101'); + + expression = new Expression('"name_" + "building"'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_building'); + + expression = new Expression('"name_" + true'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_true'); + + expression = new Expression('"name_" + null'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_null'); + + expression = new Expression('"name_" + undefined'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_undefined'); + + expression = new Expression('"name_" + vec2(1.1)'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_(1.1, 1.1)'); + + expression = new Expression('"name_" + vec3(1.1)'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_(1.1, 1.1, 1.1)'); + + expression = new Expression('"name_" + vec4(1.1)'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_(1.1, 1.1, 1.1, 1.1)'); + + expression = new Expression('"name_" + regExp("a")'); + expect(expression.evaluate(frameState, undefined)).toEqual('name_/a/'); + }); + + it('throws if binary addition takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) + vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1.0 + vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary subtraction', function() { + var expression = new Expression('2 - 1'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('4 - 3 - 2 - 1'); + expect(expression.evaluate(frameState, undefined)).toEqual(-2); + }); + + it('throws if binary subtraction takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) - vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1.0 - vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('"name1" - "name2"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary multiplication', function() { + var expression = new Expression('1 * 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(2); + + expression = new Expression('1 * 2 * 3 * 4'); + expect(expression.evaluate(frameState, undefined)).toEqual(24); + }); + + it('throws if binary multiplication takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) * vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec2(1.0) * "name"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary division', function() { + var expression = new Expression('2 / 1'); + expect(expression.evaluate(frameState, undefined)).toEqual(2); + + expression = new Expression('1/2'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.5); + + expression = new Expression('24 / -4 / 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(-3); + }); + + it('throws if binary division takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) / vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec2(1.0) / "2.0"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1.0 / vec4(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary modulus', function() { + var expression = new Expression('2 % 1'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('6 % 4 % 3'); + expect(expression.evaluate(frameState, undefined)).toEqual(2); + }); + + it('throws if binary modulus takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) % vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('vec2(1.0) % "2.0"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1.0 % vec4(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary equals strict', function() { + var expression = new Expression('\'hello\' === \'hello\''); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('1 === 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('false === true === false'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('1 === "1"'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + }); + + it('evaluates binary not equals strict', function() { + var expression = new Expression('\'hello\' !== \'hello\''); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('1 !== 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('false !== true !== false'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('1 !== "1"'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + }); + + it('evaluates binary less than', function() { + var expression = new Expression('2 < 3'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('2 < 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('3 < 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + }); + + it('throws if binary less than takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) < vec2(2.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 < vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('true < false'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('color(\'blue\') < 10'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary less than or equals', function() { + var expression = new Expression('2 <= 3'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('2 <= 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('3 <= 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + }); + + it('throws if binary less than or equals takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) <= vec2(2.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 <= vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1.0 <= "5"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('true <= false'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('color(\'blue\') <= 10'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary greater than', function() { + var expression = new Expression('2 > 3'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('2 > 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('3 > 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + }); + + it('throws if binary greater than takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) > vec2(2.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 > vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1.0 > "5"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('true > false'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('color(\'blue\') > 10'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates binary greater than or equals', function() { + var expression = new Expression('2 >= 3'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('2 >= 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('3 >= 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + }); + + it('throws if binary greater than or equals takes invalid arguments', function() { + var expression = new Expression('vec2(1.0) >= vec2(2.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 >= vec3(1.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1.0 >= "5"'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('true >= false'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('color(\'blue\') >= 10'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates logical and', function() { + var expression = new Expression('false && false'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('false && true'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('true && true'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('2 && color(\'red\')'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('throws with invalid and operands', function() { + var expression = new Expression('2 && true'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('true && color(\'red\')'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates logical or', function() { + var expression = new Expression('false || false'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('false || true'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('true || true'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + }); + + it('throws with invalid or operands', function() { + var expression = new Expression('2 || false'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('false || color(\'red\')'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates color operations', function() { + var expression = new Expression('+rgba(255, 0, 0, 1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.RED)); + + expression = new Expression('rgba(255, 0, 0, 0.5) + rgba(0, 0, 255, 0.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.MAGENTA)); + + expression = new Expression('rgba(0, 255, 255, 1.0) - rgba(0, 255, 0, 0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.BLUE)); + + expression = new Expression('rgba(255, 255, 255, 1.0) * rgba(255, 0, 0, 1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.RED)); + + expression = new Expression('rgba(255, 255, 0, 1.0) * 1.0'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.YELLOW)); + + expression = new Expression('1 * rgba(255, 255, 0, 1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.YELLOW)); + + expression = new Expression('rgba(255, 255, 255, 1.0) / rgba(255, 255, 255, 1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(Color.WHITE)); + + expression = new Expression('rgba(255, 255, 255, 1.0) / 2'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(new Color(0.5, 0.5, 0.5, 0.5))); + + expression = new Expression('rgba(255, 255, 255, 1.0) % rgba(255, 255, 255, 1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Cartesian4.fromColor(new Color(0, 0, 0, 0))); + + expression = new Expression('color(\'green\') === color(\'green\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('color(\'green\') !== color(\'green\')'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + }); + + it('evaluates vector operations', function() { + var expression = new Expression('+vec2(1, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(1, 2)); + + expression = new Expression('+vec3(1, 2, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(1, 2, 3)); + + expression = new Expression('+vec4(1, 2, 3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(1, 2, 3, 4)); + + expression = new Expression('-vec2(1, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(-1, -2)); + + expression = new Expression('-vec3(1, 2, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(-1, -2, -3)); + + expression = new Expression('-vec4(1, 2, 3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(-1, -2, -3, -4)); + + expression = new Expression('vec2(1, 2) + vec2(3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(4, 6)); + + expression = new Expression('vec3(1, 2, 3) + vec3(3, 4, 5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(4, 6, 8)); + + expression = new Expression('vec4(1, 2, 3, 4) + vec4(3, 4, 5, 6)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(4, 6, 8, 10)); + + expression = new Expression('vec2(1, 2) - vec2(3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(-2, -2)); + + expression = new Expression('vec3(1, 2, 3) - vec3(3, 4, 5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(-2, -2, -2)); + + expression = new Expression('vec4(1, 2, 3, 4) - vec4(3, 4, 5, 6)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(-2, -2, -2, -2)); + + expression = new Expression('vec2(1, 2) * vec2(3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(3, 8)); + + expression = new Expression('vec2(1, 2) * 3.0'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(3, 6)); + + expression = new Expression('3.0 * vec2(1, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(3, 6)); + + expression = new Expression('vec3(1, 2, 3) * vec3(3, 4, 5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3, 8, 15)); + + expression = new Expression('vec3(1, 2, 3) * 3.0'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3, 6, 9)); + + expression = new Expression('3.0 * vec3(1, 2, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3, 6, 9)); + + expression = new Expression('vec4(1, 2, 3, 4) * vec4(3, 4, 5, 6)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3, 8, 15, 24)); + + expression = new Expression('vec4(1, 2, 3, 4) * 3.0'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3, 6, 9, 12)); + + expression = new Expression('3.0 * vec4(1, 2, 3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(3, 6, 9, 12)); + + expression = new Expression('vec2(1, 2) / vec2(2, 5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0.5, 0.4)); + + expression = new Expression('vec2(1, 2) / 2.0'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0.5, 1.0)); + + expression = new Expression('vec3(1, 2, 3) / vec3(2, 5, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(0.5, 0.4, 1.0)); + + expression = new Expression('vec3(1, 2, 3) / 2.0'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(0.5, 1.0, 1.5)); + + expression = new Expression('vec4(1, 2, 3, 4) / vec4(2, 5, 3, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(0.5, 0.4, 1.0, 2.0)); + + expression = new Expression('vec4(1, 2, 3, 4) / 2.0'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(0.5, 1.0, 1.5, 2.0)); + + expression = new Expression('vec2(2, 3) % vec2(3, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(2, 0)); + + expression = new Expression('vec3(2, 3, 4) % vec3(3, 3, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(2, 0, 1)); + + expression = new Expression('vec4(2, 3, 4, 5) % vec4(3, 3, 3, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(2, 0, 1, 1)); + + expression = new Expression('vec2(1, 2) === vec2(1, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('vec3(1, 2, 3) === vec3(1, 2, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('vec4(1, 2, 3, 4) === vec4(1, 2, 3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('vec2(1, 2) !== vec2(1, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('vec3(1, 2, 3) !== vec3(1, 2, 3)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('vec4(1, 2, 3, 4) !== vec4(1, 2, 3, 4)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + }); + + it('evaluates color toString function', function() { + var expression = new Expression('color("red").toString()'); + expect(expression.evaluate(frameState, undefined)).toEqual('(1, 0, 0, 1)'); + + expression = new Expression('rgba(0, 0, 255, 0.5).toString()'); + expect(expression.evaluate(frameState, undefined)).toEqual('(0, 0, 1, 0.5)'); + }); + + it('evaluates vector toString function', function() { + var feature = new MockFeature(); + feature.addProperty('property', new Cartesian4(1, 2, 3, 4)); + + var expression = new Expression('vec2(1, 2).toString()'); + expect(expression.evaluate(frameState, undefined)).toEqual('(1, 2)'); + + expression = new Expression('vec3(1, 2, 3).toString()'); + expect(expression.evaluate(frameState, undefined)).toEqual('(1, 2, 3)'); + + expression = new Expression('vec4(1, 2, 3, 4).toString()'); + expect(expression.evaluate(frameState, undefined)).toEqual('(1, 2, 3, 4)'); + + expression = new Expression('${property}.toString()'); + expect(expression.evaluate(frameState, feature)).toEqual('(1, 2, 3, 4)'); + }); + + it('evaluates isNaN function', function() { + var expression = new Expression('isNaN()'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('isNaN(NaN)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('isNaN(1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isNaN(Infinity)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isNaN(null)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isNaN(true)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isNaN("hello")'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('isNaN(color("white"))'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + }); + + it('evaluates isFinite function', function() { + var expression = new Expression('isFinite()'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isFinite(NaN)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isFinite(1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('isFinite(Infinity)'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isFinite(null)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('isFinite(true)'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('isFinite("hello")'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('isFinite(color("white"))'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + }); + + it('evaluates isExactClass function', function() { + var feature = new MockFeature(); + feature.setClass('door'); + + var expression = new Expression('isExactClass("door")'); + expect(expression.evaluate(frameState, feature)).toEqual(true); + + expression = new Expression('isExactClass("roof")'); + expect(expression.evaluate(frameState, feature)).toEqual(false); + }); + + it('throws if isExactClass takes an invalid number of arguments', function() { + expect(function() { + return new Expression('isExactClass()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('isExactClass("door", "roof")'); + }).toThrowRuntimeError(); + }); + + it('evaluates isClass function', function() { + var feature = new MockFeature(); + + feature.setClass('door'); + feature.setInheritedClass('building'); + + var expression = new Expression('isClass("door") && isClass("building")'); + expect(expression.evaluate(frameState, feature)).toEqual(true); + }); + + it('throws if isClass takes an invalid number of arguments', function() { + expect(function() { + return new Expression('isClass()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('isClass("door", "building")'); + }).toThrowRuntimeError(); + }); + + it('evaluates getExactClassName function', function() { + var feature = new MockFeature(); + feature.setClass('door'); + var expression = new Expression('getExactClassName()'); + expect(expression.evaluate(frameState, feature)).toEqual('door'); + }); + + it('throws if getExactClassName takes an invalid number of arguments', function() { + expect(function() { + return new Expression('getExactClassName("door")'); + }).toThrowRuntimeError(); + }); + + it('throws if built-in unary function is given an invalid argument', function() { + // Argument must be a number or vector + var expression = new Expression('abs("-1")'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates abs function', function() { + var expression = new Expression('abs(-1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('abs(1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('abs(vec2(-1.0, 1.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(1.0, 1.0)); + + expression = new Expression('abs(vec3(-1.0, 1.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(1.0, 1.0, 0.0)); + + expression = new Expression('abs(vec4(-1.0, 1.0, 0.0, -1.2))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(1.0, 1.0, 0.0, 1.2)); + }); + + it('throws if abs function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('abs()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('abs(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates cos function', function() { + var expression = new Expression('cos(0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('cos(vec2(0, Math.PI))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(1.0, -1.0), CesiumMath.EPSILON7); + + expression = new Expression('cos(vec3(0, Math.PI, -Math.PI))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(1.0, -1.0, -1.0), CesiumMath.EPSILON7); + + expression = new Expression('cos(vec4(0, Math.PI, -Math.PI, 0))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(1.0, -1.0, -1.0, 1.0), CesiumMath.EPSILON7); + }); + + it('throws if cos function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('cos()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('cos(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates sin function', function() { + var expression = new Expression('sin(0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('sin(vec2(0, Math.PI/2))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(0.0, 1.0), CesiumMath.EPSILON7); + + expression = new Expression('sin(vec3(0, Math.PI/2, -Math.PI/2))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(0.0, 1.0, -1.0), CesiumMath.EPSILON7); + + expression = new Expression('sin(vec4(0, Math.PI/2, -Math.PI/2, 0))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(0.0, 1.0, -1.0, 0.0), CesiumMath.EPSILON7); + }); + + it('throws if sin function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('sin()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('sin(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates tan function', function() { + var expression = new Expression('tan(0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('tan(vec2(0, Math.PI/4))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(0.0, 1.0), CesiumMath.EPSILON7); + + expression = new Expression('tan(vec3(0, Math.PI/4, Math.PI))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(0.0, 1.0, 0.0), CesiumMath.EPSILON7); + + expression = new Expression('tan(vec4(0, Math.PI/4, Math.PI, -Math.PI/4))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(0.0, 1.0, 0.0, -1.0), CesiumMath.EPSILON7); + }); + + it('throws if tan function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('tan()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('tan(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates acos function', function() { + var expression = new Expression('acos(1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('acos(vec2(1, 0))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(0.0, CesiumMath.PI_OVER_TWO), CesiumMath.EPSILON7); + + expression = new Expression('acos(vec3(1, 0, 1))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(0.0, CesiumMath.PI_OVER_TWO, 0.0, CesiumMath.PI_OVER_TWO), CesiumMath.EPSILON7); + + expression = new Expression('acos(vec4(1, 0, 1, 0))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(0.0, CesiumMath.PI_OVER_TWO, 0.0, CesiumMath.PI_OVER_TWO, 0.0), CesiumMath.EPSILON7); + }); + + it('throws if acos function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('acos()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('acos(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates asin function', function() { + var expression = new Expression('asin(0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('asin(vec2(0, 1))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(0.0, CesiumMath.PI_OVER_TWO), CesiumMath.EPSILON7); + + expression = new Expression('asin(vec3(0, 1, 0))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(0.0, CesiumMath.PI_OVER_TWO, 0.0, CesiumMath.PI_OVER_TWO), CesiumMath.EPSILON7); + + expression = new Expression('asin(vec4(0, 1, 0, 1))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(0.0, CesiumMath.PI_OVER_TWO, 0.0, CesiumMath.PI_OVER_TWO, 0.0), CesiumMath.EPSILON7); + }); + + it('throws if asin function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('asin()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('asin(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates atan function', function() { + var expression = new Expression('atan(0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('atan(vec2(0, 1))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(0.0, CesiumMath.PI_OVER_FOUR), CesiumMath.EPSILON7); + + expression = new Expression('atan(vec3(0, 1, 0))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(0.0, CesiumMath.PI_OVER_FOUR, 0.0, CesiumMath.PI_OVER_FOUR), CesiumMath.EPSILON7); + + expression = new Expression('atan(vec4(0, 1, 0, 1))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(0.0, CesiumMath.PI_OVER_FOUR, 0.0, CesiumMath.PI_OVER_FOUR, 0.0), CesiumMath.EPSILON7); + }); + + it('throws if atan function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('atan()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('atan(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates radians function', function() { + var expression = new Expression('radians(180)'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(Math.PI, CesiumMath.EPSILON10); + + expression = new Expression('radians(vec2(180, 90))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(Math.PI, CesiumMath.PI_OVER_TWO), CesiumMath.EPSILON7); + + expression = new Expression('radians(vec3(180, 90, 180))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(Math.PI, CesiumMath.PI_OVER_TWO, Math.PI), CesiumMath.EPSILON7); + + expression = new Expression('radians(vec4(180, 90, 180, 90))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(Math.PI, CesiumMath.PI_OVER_TWO, Math.PI, CesiumMath.PI_OVER_TWO), CesiumMath.EPSILON7); + }); + + it('throws if radians function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('radians()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('radians(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates degrees function', function() { + var expression = new Expression('degrees(2 * Math.PI)'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(360, CesiumMath.EPSILON10); + + expression = new Expression('degrees(vec2(2 * Math.PI, Math.PI))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian2(360, 180), CesiumMath.EPSILON7); + + expression = new Expression('degrees(vec3(2 * Math.PI, Math.PI, 2 * Math.PI))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(360, 180, 360), CesiumMath.EPSILON7); + + expression = new Expression('degrees(vec4(2 * Math.PI, Math.PI, 2 * Math.PI, Math.PI))'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian4(360, 180, 360, 180), CesiumMath.EPSILON7); + }); + + it('throws if degrees function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('degrees()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('degrees(1, 2)'); + }); + }); + + it('evaluates sqrt function', function() { + var expression = new Expression('sqrt(1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('sqrt(4.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('sqrt(-1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(NaN); + + expression = new Expression('sqrt(vec2(1.0, 4.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(1.0, 2.0)); + + expression = new Expression('sqrt(vec3(1.0, 4.0, 9.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(1.0, 2.0, 3.0)); + + expression = new Expression('sqrt(vec4(1.0, 4.0, 9.0, 16.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(1.0, 2.0, 3.0, 4.0)); + }); + + it('throws if sqrt function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('sqrt()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('sqrt(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates sign function', function() { + var expression = new Expression('sign(5.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('sign(0.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('sign(-5.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(-1.0); + + expression = new Expression('sign(vec2(5.0, -5.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(1.0, -1.0)); + + expression = new Expression('sign(vec3(5.0, -5.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(1.0, -1.0, 0.0)); + + expression = new Expression('sign(vec4(5.0, -5.0, 0.0, 1.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(1.0, -1.0, 0.0, 1.0)); + }); + + it('throws if sign function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('sign()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('sign(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates floor function', function() { + var expression = new Expression('floor(5.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(5.0); + + expression = new Expression('floor(0.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('floor(-1.2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(-2.0); + + expression = new Expression('floor(vec2(5.5, -1.2))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(5.0, -2.0)); + + expression = new Expression('floor(vec3(5.5, -1.2, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(5.0, -2.0, 0.0)); + + expression = new Expression('floor(vec4(5.5, -1.2, 0.0, -2.9))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(5.0, -2.0, 0.0, -3.0)); + }); + + it('throws if floor function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('floor()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('floor(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates ceil function', function() { + var expression = new Expression('ceil(5.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(6.0); + + expression = new Expression('ceil(0.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('ceil(-1.2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(-1.0); + + expression = new Expression('ceil(vec2(5.5, -1.2))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(6.0, -1.0)); + + expression = new Expression('ceil(vec3(5.5, -1.2, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(6.0, -1.0, 0.0)); + + expression = new Expression('ceil(vec4(5.5, -1.2, 0.0, -2.9))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(6.0, -1.0, 0.0, -2.0)); + }); + + it('throws if ceil function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('ceil()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('ceil(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates round function', function() { + var expression = new Expression('round(5.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(6); + + expression = new Expression('round(0.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0); + + expression = new Expression('round(1.2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1); + + expression = new Expression('round(vec2(5.5, -1.2))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(6.0, -1.0)); + + expression = new Expression('round(vec3(5.5, -1.2, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(6.0, -1.0, 0.0)); + + expression = new Expression('round(vec4(5.5, -1.2, 0.0, -2.9))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(6.0, -1.0, 0.0, -3.0)); + }); + + it('throws if round function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('round()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('round(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates exp function', function() { + var expression = new Expression('exp(1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(Math.E); + + expression = new Expression('exp(0.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('exp(vec2(1.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(Math.E, 1.0)); + + expression = new Expression('exp(vec3(1.0, 0.0, 1.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(Math.E, 1.0, Math.E)); + + expression = new Expression('exp(vec4(1.0, 0.0, 1.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(Math.E, 1.0, Math.E, 1.0)); + }); + + it('throws if exp function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('exp()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('exp(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates exp2 function', function() { + var expression = new Expression('exp2(1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('exp2(0.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('exp2(2.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(4.0); + + expression = new Expression('exp2(vec2(1.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(2.0, 1.0)); + + expression = new Expression('exp2(vec3(1.0, 0.0, 2.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(2.0, 1.0, 4.0)); + + expression = new Expression('exp2(vec4(1.0, 0.0, 2.0, 3.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(2.0, 1.0, 4.0, 8.0)); + }); + + it('throws if exp2 function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('exp2()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('exp2(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates log function', function() { + var expression = new Expression('log(1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('log(10.0)'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(2.302585092994046, CesiumMath.EPSILON7); + + expression = new Expression('log(vec2(1.0, Math.E))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0.0, 1.0)); + + expression = new Expression('log(vec3(1.0, Math.E, 1.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(0.0, 1.0, 0.0)); + + expression = new Expression('log(vec4(1.0, Math.E, 1.0, Math.E))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(0.0, 1.0, 0.0, 1.0)); + }); + + it('throws if log function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('log()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('log(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates log2 function', function() { + var expression = new Expression('log2(1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('log2(2.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('log2(4.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('log2(vec2(1.0, 2.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0.0, 1.0)); + + expression = new Expression('log2(vec3(1.0, 2.0, 4.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(0.0, 1.0, 2.0)); + + expression = new Expression('log2(vec4(1.0, 2.0, 4.0, 8.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(0.0, 1.0, 2.0, 3.0)); + }); + + it('throws if log2 function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('log2()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('log2(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates fract function', function() { + var expression = new Expression('fract(1.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('fract(2.25)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.25); + + expression = new Expression('fract(-2.25)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.75); + + expression = new Expression('fract(vec2(1.0, 2.25))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0.0, 0.25)); + + expression = new Expression('fract(vec3(1.0, 2.25, -2.25))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(0.0, 0.25, 0.75)); + + expression = new Expression('fract(vec4(1.0, 2.25, -2.25, 1.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(0.0, 0.25, 0.75, 0.0)); + }); + + it('throws if fract function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('fract()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('fract(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates length function', function() { + var expression = new Expression('length(-3.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(3.0); + + expression = new Expression('length(vec2(-3.0, 4.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(5.0); + + expression = new Expression('length(vec3(2.0, 3.0, 6.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(7.0); + + expression = new Expression('length(vec4(2.0, 4.0, 7.0, 10.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(13.0); + }); + + it('throws if length function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('length()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('length(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates normalize function', function() { + var expression = new Expression('normalize(5.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('normalize(vec2(3.0, 4.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0.6, 0.8)); + + expression = new Expression('normalize(vec3(2.0, 3.0, -4.0))'); + var length = Math.sqrt(2 * 2 + 3 * 3 + 4 * 4); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(new Cartesian3(2.0 / length, 3.0 / length, -4.0 / length), CesiumMath.EPSILON10); + + expression = new Expression('normalize(vec4(-2.0, 3.0, -4.0, 5.0))'); + length = Math.sqrt(2 * 2 + 3 * 3 + 4 * 4 + 5 * 5); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(-2.0 / length, 3.0 / length, -4.0 / length, 5.0/length), CesiumMath.EPSILON10); + }); + + it('throws if normalize function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('fract()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('fract(1, 2)'); + }).toThrowRuntimeError(); + }); + + it('evaluates clamp function', function() { + var expression = new Expression('clamp(50.0, 0.0, 100.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(50.0); + + expression = new Expression('clamp(50.0, 0.0, 25.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(25.0); + + expression = new Expression('clamp(50.0, 75.0, 100.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(75.0); + + expression = new Expression('clamp(vec2(50.0,50.0), vec2(0.0,75.0), 100.0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(50.0, 75.0)); + + expression = new Expression('clamp(vec2(50.0,50.0), vec2(0.0,75.0), vec2(25.0,100.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(25.0, 75.0)); + + expression = new Expression('clamp(vec3(50.0, 50.0, 50.0), vec3(0.0, 0.0, 75.0), vec3(100.0, 25.0, 100.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(50.0, 25.0, 75.0)); + + expression = new Expression('clamp(vec4(50.0, 50.0, 50.0, 100.0), vec4(0.0, 0.0, 75.0, 75.0), vec4(100.0, 25.0, 100.0, 85.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(50.0, 25.0, 75.0, 85.0)); + }); + + it('throws if clamp function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('clamp()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('clamp(1)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('clamp(1, 2)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('clamp(1, 2, 3, 4)'); + }).toThrowRuntimeError(); + }); + + it('throws if clamp function takes mismatching types', function() { + var expression = new Expression('clamp(0.0,vec2(0,1),0.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('clamp(vec2(0,1),vec3(0,1,2),0.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('clamp(vec2(0,1),vec2(0,1), vec3(1,2,3))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates mix function', function() { + var expression = new Expression('mix(0.0, 2.0, 0.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('mix(vec2(0.0,1.0), vec2(2.0,3.0), 0.5)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(1.0, 2.0)); + + expression = new Expression('mix(vec2(0.0,1.0), vec2(2.0,3.0), vec2(0.5,4.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(1.0, 9.0)); + + expression = new Expression('mix(vec3(0.0,1.0,2.0), vec3(2.0,3.0,4.0), vec3(0.5,4.0,5.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(1.0, 9.0, 12.0)); + + expression = new Expression('mix(vec4(0.0,1.0,2.0,1.5), vec4(2.0,3.0,4.0,2.5), vec4(0.5,4.0,5.0,3.5))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(1.0, 9.0, 12.0, 5.0)); + }); + + it('throws if mix function takes mismatching types', function() { + var expression = new Expression('mix(0.0,vec2(0,1),0.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('mix(vec2(0,1),vec3(0,1,2),0.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('mix(vec2(0,1),vec2(0,1), vec3(1,2,3))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('throws if mix function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('mix()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('mix(1)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('mix(1, 2)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('mix(1, 2, 3, 4)'); + }).toThrowRuntimeError(); + }); + + it('evaluates atan2 function', function() { + var expression = new Expression('atan2(0,1)'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(0.0, CesiumMath.EPSILON10); + + expression = new Expression('atan2(1,0)'); + expect(expression.evaluate(frameState, undefined)).toEqualEpsilon(0.5 * Math.PI, CesiumMath.EPSILON10); + + expression = new Expression('atan2(vec2(0,1),vec2(1,0))'); + expect(expression.evaluate(frameState, undefined)) + .toEqualEpsilon(new Cartesian2(0.0, 0.5 * Math.PI), CesiumMath.EPSILON10); + + expression = new Expression('atan2(vec3(0,1,0.5),vec3(1,0,0.5))'); + expect(expression.evaluate(frameState, undefined)) + .toEqualEpsilon(new Cartesian3(0.0, 0.5 * Math.PI, 0.25 * Math.PI), CesiumMath.EPSILON10); + + expression = new Expression('atan2(vec4(0,1,0.5,1),vec4(1,0,0.5,0))'); + expect(expression.evaluate(frameState, undefined)) + .toEqualEpsilon(new Cartesian4(0.0, 0.5 * Math.PI, 0.25 * Math.PI, 0.5 * Math.PI), CesiumMath.EPSILON10); + }); + + it('throws if atan2 function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('atan2(0.0)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('atan2(1, 2, 0)'); + }).toThrowRuntimeError(); + }); + + it('throws if atan2 function takes mismatching types', function() { + var expression = new Expression('atan2(0.0,vec2(0,1))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('atan2(vec2(0,1),0.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('atan2(vec2(0,1),vec3(0,1,2))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates pow function', function() { + var expression = new Expression('pow(5,0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('pow(4,2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(16.0); + + expression = new Expression('pow(vec2(5,4),vec2(0,2))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(1.0, 16.0)); + + expression = new Expression('pow(vec3(5,4,3),vec3(0,2,3))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(1.0, 16.0, 27.0)); + + expression = new Expression('pow(vec4(5,4,3,2),vec4(0,2,3,5))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(1.0, 16.0, 27.0, 32.0)); + }); + + it('throws if pow function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('pow(0.0)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('pow(1, 2, 0)'); + }).toThrowRuntimeError(); + }); + + it('throws if pow function takes mismatching types', function() { + var expression = new Expression('pow(0.0, vec2(0,1))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('pow(vec2(0,1),0.0)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('pow(vec2(0,1),vec3(0,1,2))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates min function', function() { + var expression = new Expression('min(0,1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('min(-1,0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(-1.0); + + expression = new Expression('min(vec2(-1,1),0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(-1.0, 0)); + + expression = new Expression('min(vec2(-1,2),vec2(0,1))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(-1.0, 1.0)); + + expression = new Expression('min(vec3(-1,2,1),vec3(0,1,2))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(-1.0, 1.0, 1.0)); + + expression = new Expression('min(vec4(-1,2,1,4),vec4(0,1,2,3))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(-1.0, 1.0, 1.0, 3.0)); + }); + + it('throws if min function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('min(0.0)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('min(1, 2, 0)'); + }).toThrowRuntimeError(); + }); + + it('throws if min function takes mismatching types', function() { + var expression = new Expression('min(0.0, vec2(0,1))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('min(vec2(0,1),vec3(0,1,2))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates max function', function() { + var expression = new Expression('max(0,1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('max(-1,0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(0.0); + + expression = new Expression('max(vec2(-1,1),0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0, 1.0)); + + expression = new Expression('max(vec2(-1,2),vec2(0,1))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian2(0, 2.0)); + + expression = new Expression('max(vec3(-1,2,1),vec3(0,1,2))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(0, 2.0, 2.0)); + + expression = new Expression('max(vec4(-1,2,1,4),vec4(0,1,2,3))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian4(0, 2.0, 2.0, 4.0)); + }); + + it('throws if max function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('max(0.0)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('max(1, 2, 0)'); + }).toThrowRuntimeError(); + }); + + it('throws if max function takes mismatching types', function() { + var expression = new Expression('max(0.0, vec2(0,1))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('max(vec2(0,1),vec3(0,1,2))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates the distance function', function() { + var expression = new Expression('distance(0, 1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('distance(vec2(1.0, 0.0), vec2(0.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(1.0); + + expression = new Expression('distance(vec3(3.0, 2.0, 1.0), vec3(1.0, 0.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(3.0); + + expression = new Expression('distance(vec4(5.0, 5.0, 5.0, 5.0), vec4(0.0, 0.0, 0.0, 0.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(10.0); + }); + + it('throws if distance function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('distance(0.0)'); + }) .toThrowRuntimeError(); + + expect(function() { + return new Expression('distance(1, 3, 0)'); + }).toThrowRuntimeError(); + }); + + it('throws if distance function takes mismatching types of arguments', function() { + expect(function() { + return new Expression('distance(1, vec2(3.0, 2.0)').evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('distance(vec4(5.0, 2.0, 3.0, 1.0), vec3(4.0, 4.0, 4.0))').evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates the dot function', function() { + var expression = new Expression('dot(1, 2)'); + expect(expression.evaluate(frameState, undefined)).toEqual(2.0); + + expression = new Expression('dot(vec2(1.0, 1.0), vec2(2.0, 2.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(4.0); + + expression = new Expression('dot(vec3(1.0, 2.0, 3.0), vec3(2.0, 2.0, 1.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(9.0); + + expression = new Expression('dot(vec4(5.0, 5.0, 2.0, 3.0), vec4(1.0, 2.0, 1.0, 1.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(20.0); + }); + + it('throws if dot function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('dot(0.0)'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('dot(1, 3, 0)'); + }).toThrowRuntimeError(); + }); + + it('throws if dot function takes mismatching types of arguments', function() { + expect(function() { + return new Expression('dot(1, vec2(3.0, 2.0)').evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('dot(vec4(5.0, 2.0, 3.0, 1.0), vec3(4.0, 4.0, 4.0))').evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates the cross function', function() { + var expression = new Expression('cross(vec3(1.0, 1.0, 1.0), vec3(2.0, 2.0, 2.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(0.0, 0.0, 0.0)); + + expression = new Expression('cross(vec3(-1.0, -1.0, -1.0), vec3(0.0, -2.0, -5.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(3.0, -5.0, 2.0)); + + expression = new Expression('cross(vec3(5.0, -2.0, 1.0), vec3(-2.0, -6.0, -8.0))'); + expect(expression.evaluate(frameState, undefined)).toEqual(new Cartesian3(22.0, 38.0, -34.0)); + }); + + it('throws if cross function takes an invalid number of arguments', function() { + expect(function() { + return new Expression('cross(vec3(0.0, 0.0, 0.0))'); + }) .toThrowRuntimeError(); + + expect(function() { + return new Expression('cross(vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0), vec3(2.0, 2.0, 2.0))'); + }).toThrowRuntimeError(); + }); + + it('throws if cross function does not take vec3 arguments', function() { + expect(function() { + return new Expression('cross(vec2(1.0, 2.0), vec2(3.0, 2.0)').evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('cross(vec4(5.0, 2.0, 3.0, 1.0), vec3(4.0, 4.0, 4.0))').evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates ternary conditional', function() { + var expression = new Expression('true ? "first" : "second"'); + expect(expression.evaluate(frameState, undefined)).toEqual('first'); + + expression = new Expression('false ? "first" : "second"'); + expect(expression.evaluate(frameState, undefined)).toEqual('second'); + + expression = new Expression('(!(1 + 2 > 3)) ? (2 > 1 ? 1 + 1 : 0) : (2 > 1 ? -1 + -1 : 0)'); + expect(expression.evaluate(frameState, undefined)).toEqual(2); + }); + + it('evaluates member expression with dot', function() { + var feature = new MockFeature(); + feature.addProperty('height', 10); + feature.addProperty('width', 5); + feature.addProperty('string', 'hello'); + feature.addProperty('boolean', true); + feature.addProperty('vector', Cartesian4.UNIT_X); + feature.addProperty('vector.x', 'something else'); + feature.addProperty('feature.vector', Cartesian4.UNIT_Y); + feature.addProperty('feature', { + vector : Cartesian4.UNIT_Z + }); + feature.addProperty('null', null); + feature.addProperty('undefined', undefined); + feature.addProperty('address', { + "street" : "Example Street", + "city" : "Example City" + }); + + var expression = new Expression('${vector.x}'); + expect(expression.evaluate(frameState, feature)).toEqual(1.0); + + expression = new Expression('${vector.z}'); + expect(expression.evaluate(frameState, feature)).toEqual(0.0); + + expression = new Expression('${height.z}'); + expect(expression.evaluate(frameState, feature)).toEqual(undefined); + + expression = new Expression('${undefined.z}'); + expect(expression.evaluate(frameState, feature)).toEqual(undefined); + + expression = new Expression('${feature}'); + expect(expression.evaluate(frameState, feature)).toEqual({ + vector : Cartesian4.UNIT_Z + }); + + expression = new Expression('${feature.vector}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_X); + + expression = new Expression('${feature.feature.vector}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_Z); + + expression = new Expression('${feature.vector.x}'); + expect(expression.evaluate(frameState, feature)).toEqual(1.0); + + expression = new Expression('${address.street}'); + expect(expression.evaluate(frameState, feature)).toEqual("Example Street"); + + expression = new Expression('${address.city}'); + expect(expression.evaluate(frameState, feature)).toEqual("Example City"); + }); + + it('evaluates member expression with brackets', function() { + var feature = new MockFeature(); + feature.addProperty('height', 10); + feature.addProperty('width', 5); + feature.addProperty('string', 'hello'); + feature.addProperty('boolean', true); + feature.addProperty('vector', Cartesian4.UNIT_X); + feature.addProperty('vector.x', 'something else'); + feature.addProperty('feature.vector', Cartesian4.UNIT_Y); + feature.addProperty('feature', { + vector : Cartesian4.UNIT_Z + }); + feature.addProperty('null', null); + feature.addProperty('undefined', undefined); + feature.addProperty('address.street', "Other Street"); + feature.addProperty('address', { + "street" : "Example Street", + "city" : "Example City" + }); + + var expression = new Expression('${vector["x"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(1.0); + + expression = new Expression('${vector["z"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(0.0); + + expression = new Expression('${height["z"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(undefined); + + expression = new Expression('${undefined["z"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(undefined); + + expression = new Expression('${feature["vector"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_X); + + expression = new Expression('${feature.vector["x"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(1.0); + + expression = new Expression('${feature["vector"].x}'); + expect(expression.evaluate(frameState, feature)).toEqual(1.0); + + expression = new Expression('${feature["vector.x"]}'); + expect(expression.evaluate(frameState, feature)).toEqual('something else'); + + expression = new Expression('${feature.feature["vector"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_Z); + + expression = new Expression('${feature["feature.vector"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_Y); + + expression = new Expression('${address.street}'); + expect(expression.evaluate(frameState, feature)).toEqual("Example Street"); + + expression = new Expression('${feature.address.street}'); + expect(expression.evaluate(frameState, feature)).toEqual("Example Street"); + + expression = new Expression('${feature["address"].street}'); + expect(expression.evaluate(frameState, feature)).toEqual("Example Street"); + + expression = new Expression('${feature["address.street"]}'); + expect(expression.evaluate(frameState, feature)).toEqual("Other Street"); + + expression = new Expression('${address["street"]}'); + expect(expression.evaluate(frameState, feature)).toEqual("Example Street"); + + expression = new Expression('${address["city"]}'); + expect(expression.evaluate(frameState, feature)).toEqual("Example City"); + }); + + it('member expressions throw without variable notation', function() { + expect(function() { + return new Expression('color.r'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('color["r"]'); + }).toThrowRuntimeError(); + }); + + it('member expression throws with variable property', function() { + var feature = new MockFeature(); + feature.addProperty('vector', Cartesian4.UNIT_X); + feature.addProperty('vectorName', 'UNIT_X'); + + expect(function() { + return new Expression('${vector[${vectorName}]}'); + }).toThrowRuntimeError(); + }); + + it('evaluates feature property', function() { + var feature = new MockFeature(); + feature.addProperty('feature', { + vector : Cartesian4.UNIT_X + }); + + var expression = new Expression('${feature}'); + expect(expression.evaluate(frameState, feature)).toEqual({ + vector : Cartesian4.UNIT_X + }); + + expression = new Expression('${feature} === ${feature.feature}'); + expect(expression.evaluate(frameState, feature)).toEqual(true); + }); + + it('constructs regex', function() { + var feature = new MockFeature(); + feature.addProperty('pattern', "[abc]"); + + var expression = new Expression('regExp("a")'); + expect(expression.evaluate(frameState, undefined)).toEqual(/a/); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.LITERAL_REGEX); + + expression = new Expression('regExp("\\w")'); + expect(expression.evaluate(frameState, undefined)).toEqual(/\w/); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.LITERAL_REGEX); + + expression = new Expression('regExp(1 + 1)'); + expect(expression.evaluate(frameState, undefined)).toEqual(/2/); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.REGEX); + + expression = new Expression('regExp(true)'); + expect(expression.evaluate(frameState, undefined)).toEqual(/true/); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.LITERAL_REGEX); + + expression = new Expression('regExp()'); + expect(expression.evaluate(frameState, undefined)).toEqual(/(?:)/); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.LITERAL_REGEX); + + expression = new Expression('regExp(${pattern})'); + expect(expression.evaluate(frameState, feature)).toEqual(/[abc]/); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.REGEX); + }); + + it ('constructs regex with flags', function() { + var expression = new Expression('regExp("a", "i")'); + expect(expression.evaluate(frameState, undefined)).toEqual(/a/i); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.LITERAL_REGEX); + + expression = new Expression('regExp("a", "m" + "g")'); + expect(expression.evaluate(frameState, undefined)).toEqual(/a/mg); + expect(expression._runtimeAst._type).toEqual(ExpressionNodeType.REGEX); + }); + + it('throws if regex constructor has invalid pattern', function() { + var expression = new Expression('regExp("(?<=\\s)" + ".")'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('regExp("(?<=\\s)")'); + }).toThrowRuntimeError(); + }); + + it('throws if regex constructor has invalid flags', function() { + var expression = new Expression('regExp("a" + "b", "q")'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('regExp("a", "q")'); + }).toThrowRuntimeError(); + }); + + it('evaluates regex test function', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'abc'); + + var expression = new Expression('regExp("a").test("abc")'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('regExp("a").test("bcd")'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('regExp("quick\\s(brown).+?(jumps)", "ig").test("The Quick Brown Fox Jumps Over The Lazy Dog")'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('regExp("a").test()'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('regExp(${property}).test(${property})'); + expect(expression.evaluate(frameState, feature)).toEqual(true); + }); + + it('throws if regex test function has invalid arguments', function() { + var expression = new Expression('regExp("1").test(1)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('regExp("a").test(regExp("b"))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates regex exec function', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'abc'); + feature.addProperty('Name', 'Building 1'); + + var expression = new Expression('regExp("a(.)", "i").exec("Abc")'); + expect(expression.evaluate(frameState, undefined)).toEqual('b'); + + expression = new Expression('regExp("a(.)").exec("qbc")'); + expect(expression.evaluate(frameState, undefined)).toEqual(null); + + expression = new Expression('regExp("a(.)").exec()'); + expect(expression.evaluate(frameState, undefined)).toEqual(null); + + expression = new Expression('regExp("quick\\s(b.*n).+?(jumps)", "ig").exec("The Quick Brown Fox Jumps Over The Lazy Dog")'); + expect(expression.evaluate(frameState, undefined)).toEqual('Brown'); + + expression = new Expression('regExp("(" + ${property} + ")").exec(${property})'); + expect(expression.evaluate(frameState, feature)).toEqual('abc'); + + expression = new Expression('regExp("Building\\s(\\d)").exec(${Name})'); + expect(expression.evaluate(frameState, feature)).toEqual('1'); + }); + + it('throws if regex exec function has invalid arguments', function() { + var expression = new Expression('regExp("1").exec(1)'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('regExp("a").exec(regExp("b"))'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates regex match operator', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'abc'); + + var expression = new Expression('regExp("a") =~ "abc"'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('"abc" =~ regExp("a")'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('regExp("a") =~ "bcd"'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('"bcd" =~ regExp("a")'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('regExp("quick\\s(brown).+?(jumps)", "ig") =~ "The Quick Brown Fox Jumps Over The Lazy Dog"'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('regExp(${property}) =~ ${property}'); + expect(expression.evaluate(frameState, feature)).toEqual(true); + }); + + it('throws if regex match operator has invalid arguments', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'abc'); + + var expression = new Expression('regExp("a") =~ 1'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 =~ regExp("a")'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 =~ 1'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('evaluates regex not match operator', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'abc'); + + var expression = new Expression('regExp("a") !~ "abc"'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('"abc" !~ regExp("a")'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('regExp("a") !~ "bcd"'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('"bcd" !~ regExp("a")'); + expect(expression.evaluate(frameState, undefined)).toEqual(true); + + expression = new Expression('regExp("quick\\s(brown).+?(jumps)", "ig") !~ "The Quick Brown Fox Jumps Over The Lazy Dog"'); + expect(expression.evaluate(frameState, undefined)).toEqual(false); + + expression = new Expression('regExp(${property}) !~ ${property}'); + expect(expression.evaluate(frameState, feature)).toEqual(false); + }); + + it('throws if regex not match operator has invalid arguments', function() { + var expression = new Expression('regExp("a") !~ 1'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 !~ regExp("a")'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + + expression = new Expression('1 !~ 1'); + expect(function() { + expression.evaluate(frameState, undefined); + }).toThrowRuntimeError(); + }); + + it('throws if test is not called with a RegExp', function() { + expect(function() { + return new Expression('color("blue").test()'); + }).toThrowRuntimeError(); + + expect(function() { + return new Expression('"blue".test()'); + }).toThrowRuntimeError(); + }); + + it('evaluates regExp toString function', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'abc'); + + var expression = new Expression('regExp().toString()'); + expect(expression.evaluate(frameState, undefined)).toEqual('/(?:)/'); + + expression = new Expression('regExp("\\d\\s\\d", "ig").toString()'); + expect(expression.evaluate(frameState, undefined)).toEqual('/\\d\\s\\d/gi'); + + expression = new Expression('regExp(${property}).toString()'); + expect(expression.evaluate(frameState, feature)).toEqual('/abc/'); + }); + + it('throws when using toString on other type', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'abc'); + + var expression = new Expression('${property}.toString()'); + expect(function() { + return expression.evaluate(frameState, feature); + }).toThrowRuntimeError(); + }); + + it('evaluates array expression', function() { + var feature = new MockFeature(); + feature.addProperty('property', 'value'); + feature.addProperty('array', [Cartesian4.UNIT_X, Cartesian4.UNIT_Y, Cartesian4.UNIT_Z]); + feature.addProperty('complicatedArray', [{ + 'subproperty' : Cartesian4.UNIT_X, + 'anotherproperty' : Cartesian4.UNIT_Y + }, { + 'subproperty' : Cartesian4.UNIT_Z, + 'anotherproperty' : Cartesian4.UNIT_W + }]); + feature.addProperty('temperatures', { + "scale" : "fahrenheit", + "values" : [70, 80, 90] + }); + + var expression = new Expression('[1, 2, 3]'); + expect(expression.evaluate(frameState, undefined)).toEqual([1, 2, 3]); + + expression = new Expression('[1+2, "hello", 2 < 3, color("blue"), ${property}]'); + expect(expression.evaluate(frameState, feature)).toEqual([3, 'hello', true, Cartesian4.fromColor(Color.BLUE), 'value']); + + expression = new Expression('${array[1]}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_Y); + + expression = new Expression('${complicatedArray[1].subproperty}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_Z); + + expression = new Expression('${complicatedArray[0]["anotherproperty"]}'); + expect(expression.evaluate(frameState, feature)).toEqual(Cartesian4.UNIT_Y); + + expression = new Expression('${temperatures["scale"]}'); + expect(expression.evaluate(frameState, feature)).toEqual('fahrenheit'); + + expression = new Expression('${temperatures.values[0]}'); + expect(expression.evaluate(frameState, feature)).toEqual(70); + + expression = new Expression('${temperatures["values"][0]}'); + expect(expression.evaluate(frameState, feature)).toEqual(70); + }); + + it('evaluates tiles3d_tileset_time expression', function() { + var feature = new MockFeature(); + var expression = new Expression('${tiles3d_tileset_time}'); + expect(expression.evaluate(frameState, feature)).toEqual(0.0); + feature.content.tileset.timeSinceLoad = 1.0; + expect(expression.evaluate(frameState, feature)).toEqual(1.0); + }); + + it('gets shader function', function() { + var expression = new Expression('true'); + var shaderFunction = expression.getShaderFunction('getShow', '', {}, 'bool'); + var expected = 'bool getShow() \n' + + '{ \n' + + ' return true; \n' + + '} \n'; + expect(shaderFunction).toEqual(expected); + }); + + it('gets shader expression for variable', function() { + var expression = new Expression('${property}'); + var shaderExpression = expression.getShaderExpression('prefix_', {}); + var expected = 'prefix_property'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for unary not', function() { + var expression = new Expression('!true'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '!true'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for unary negative', function() { + var expression = new Expression('-5.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '-5.0'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for unary positive', function() { + var expression = new Expression('+5.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '+5.0'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for converting to literal boolean', function() { + var expression = new Expression('Boolean(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'bool(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for converting to literal number', function() { + var expression = new Expression('Number(true)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'float(true)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary addition', function() { + var expression = new Expression('1.0 + 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 + 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary subtraction', function() { + var expression = new Expression('1.0 - 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 - 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary multiplication', function() { + var expression = new Expression('1.0 * 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 * 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary division', function() { + var expression = new Expression('1.0 / 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 / 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary modulus', function() { + var expression = new Expression('1.0 % 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'mod(1.0, 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary equals strict', function() { + var expression = new Expression('1.0 === 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 == 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary not equals strict', function() { + var expression = new Expression('1.0 !== 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 != 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary less than', function() { + var expression = new Expression('1.0 < 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 < 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary less than or equals', function() { + var expression = new Expression('1.0 <= 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 <= 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary greater than', function() { + var expression = new Expression('1.0 > 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 > 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for binary greater than or equals', function() { + var expression = new Expression('1.0 >= 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(1.0 >= 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for logical and', function() { + var expression = new Expression('true && false'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(true && false)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for logical or', function() { + var expression = new Expression('true || false'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(true || false)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for ternary conditional', function() { + var expression = new Expression('true ? 1.0 : 2.0'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(true ? 1.0 : 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for array indexing', function() { + var expression = new Expression('${property[0]}'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'property[0]'; + expect(shaderExpression).toEqual(expected); + + expression = new Expression('${property[4 / 2]}'); + shaderExpression = expression.getShaderExpression('', {}); + expected = 'property[int((4.0 / 2.0))]'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for array', function() { + var expression = new Expression('[1.0, 2.0]'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'vec2(1.0, 2.0)'; + expect(shaderExpression).toEqual(expected); + + expression = new Expression('[1.0, 2.0, 3.0]'); + shaderExpression = expression.getShaderExpression('', {}); + expected = 'vec3(1.0, 2.0, 3.0)'; + expect(shaderExpression).toEqual(expected); + + expression = new Expression('[1.0, 2.0, 3.0, 4.0]'); + shaderExpression = expression.getShaderExpression('', {}); + expected = 'vec4(1.0, 2.0, 3.0, 4.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('throws when getting shader expression for array of invalid length', function() { + var expression = new Expression('[]'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + + expression = new Expression('[1.0]'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + + expression = new Expression('[1.0, 2.0, 3.0, 4.0, 5.0]'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('gets shader expression for boolean', function() { + var expression = new Expression('true || false'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(true || false)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for integer', function() { + var expression = new Expression('1'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '1.0'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for float', function() { + var expression = new Expression('1.02'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '1.02'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for color', function() { + var shaderState = {translucent : false}; + var expression = new Expression('color()'); + var shaderExpression = expression.getShaderExpression('', shaderState); + var expected = 'vec4(1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('color("red")'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(vec3(1.0, 0.0, 0.0), 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('color("#FFF")'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(vec3(1.0, 1.0, 1.0), 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('color("#FF0000")'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(vec3(1.0, 0.0, 0.0), 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('color("rgb(255, 0, 0)")'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(vec3(1.0, 0.0, 0.0), 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('color("red", 0.5)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(vec3(1.0, 0.0, 0.0), 0.5)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(true); + + shaderState = {translucent : false}; + expression = new Expression('rgb(255, 0, 0)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(1.0, 0.0, 0.0, 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('rgb(255, ${property}, 0)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(255.0 / 255.0, property / 255.0, 0.0 / 255.0, 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('rgba(255, 0, 0, 0.5)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(1.0, 0.0, 0.0, 0.5)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(true); + + shaderState = {translucent : false}; + expression = new Expression('rgba(255, ${property}, 0, 0.5)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(255.0 / 255.0, property / 255.0, 0.0 / 255.0, 0.5)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(true); + + shaderState = {translucent : false}; + expression = new Expression('hsl(1.0, 0.5, 0.5)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(0.75, 0.25, 0.25, 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('hsla(1.0, 0.5, 0.5, 0.5)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(0.75, 0.25, 0.25, 0.5)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(true); + + shaderState = {translucent : false}; + expression = new Expression('hsl(1.0, ${property}, 0.5)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(czm_HSLToRGB(vec3(1.0, property, 0.5)), 1.0)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(false); + + shaderState = {translucent : false}; + expression = new Expression('hsla(1.0, ${property}, 0.5, 0.5)'); + shaderExpression = expression.getShaderExpression('', shaderState); + expected = 'vec4(czm_HSLToRGB(vec3(1.0, property, 0.5)), 0.5)'; + expect(shaderExpression).toEqual(expected); + expect(shaderState.translucent).toBe(true); + }); + + it('gets shader expression for color components', function() { + // .r, .g, .b, .a + var expression = new Expression('color().r + color().g + color().b + color().a'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(((vec4(1.0)[0] + vec4(1.0)[1]) + vec4(1.0)[2]) + vec4(1.0)[3])'; + expect(shaderExpression).toEqual(expected); + + // .x, .y, .z, .w + expression = new Expression('color().x + color().y + color().z + color().w'); + shaderExpression = expression.getShaderExpression('', {}); + expect(shaderExpression).toEqual(expected); + + // [0], [1], [2], [3] + expression = new Expression('color()[0] + color()[1] + color()[2] + color()[3]'); + shaderExpression = expression.getShaderExpression('', {}); + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for vector', function() { + var expression = new Expression('vec4(1, 2, 3, 4)'); + var shaderExpression = expression.getShaderExpression('', {}); + expect(shaderExpression).toEqual('vec4(1.0, 2.0, 3.0, 4.0)'); + + expression = new Expression('vec4(1) + vec4(2)'); + shaderExpression = expression.getShaderExpression('', {}); + expect(shaderExpression).toEqual('(vec4(1.0) + vec4(2.0))'); + + expression = new Expression('vec4(1, ${property}, vec2(1, 2).x, 0)'); + shaderExpression = expression.getShaderExpression('', {}); + expect(shaderExpression).toEqual('vec4(1.0, property, vec2(1.0, 2.0)[0], 0.0)'); + + expression = new Expression('vec4(vec3(2), 1.0)'); + shaderExpression = expression.getShaderExpression('', {}); + expect(shaderExpression).toEqual('vec4(vec3(2.0), 1.0)'); + }); + + it('gets shader expression for vector components', function() { + // .x, .y, .z, .w + var expression = new Expression('vec4(1).x + vec4(1).y + vec4(1).z + vec4(1).w'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = '(((vec4(1.0)[0] + vec4(1.0)[1]) + vec4(1.0)[2]) + vec4(1.0)[3])'; + expect(shaderExpression).toEqual(expected); + + // [0], [1], [2], [3] + expression = new Expression('vec4(1)[0] + vec4(1)[1] + vec4(1)[2] + vec4(1)[3]'); + shaderExpression = expression.getShaderExpression('', {}); + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for tiles3d_tileset_time', function() { + var expression = new Expression('${tiles3d_tileset_time}'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'u_tilesetTime'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for abs', function() { + var expression = new Expression('abs(-1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'abs(-1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for cos', function() { + var expression = new Expression('cos(0.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'cos(0.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for sin', function() { + var expression = new Expression('sin(0.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'sin(0.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for tan', function() { + var expression = new Expression('tan(0.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'tan(0.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for acos', function() { + var expression = new Expression('acos(0.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'acos(0.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for asin', function() { + var expression = new Expression('asin(0.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'asin(0.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for atan', function() { + var expression = new Expression('atan(0.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'atan(0.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for sqrt', function() { + var expression = new Expression('sqrt(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'sqrt(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for sign', function() { + var expression = new Expression('sign(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'sign(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for floor', function() { + var expression = new Expression('floor(1.5)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'floor(1.5)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for ceil', function() { + var expression = new Expression('ceil(1.2)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'ceil(1.2)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for round', function() { + var expression = new Expression('round(1.2)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'floor(1.2 + 0.5)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for exp', function() { + var expression = new Expression('exp(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'exp(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for exp2', function() { + var expression = new Expression('exp2(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'exp2(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for log', function() { + var expression = new Expression('log(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'log(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for log2', function() { + var expression = new Expression('log2(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'log2(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for fract', function() { + var expression = new Expression('fract(1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'fract(1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for clamp', function() { + var expression = new Expression('clamp(50.0, 0.0, 100.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'clamp(50.0, 0.0, 100.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for mix', function() { + var expression = new Expression('mix(0.0, 2.0, 0.5)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'mix(0.0, 2.0, 0.5)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for atan2', function() { + var expression = new Expression('atan2(0.0,1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'atan(0.0, 1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for pow', function() { + var expression = new Expression('pow(2.0,2.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'pow(2.0, 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for min', function() { + var expression = new Expression('min(3.0,5.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'min(3.0, 5.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for max', function() { + var expression = new Expression('max(3.0,5.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'max(3.0, 5.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for length', function() { + var expression = new Expression('length(3.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'length(3.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for normalize', function() { + var expression = new Expression('normalize(3.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'normalize(3.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for distance', function() { + var expression = new Expression('distance(0.0, 1.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'distance(0.0, 1.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for dot', function() { + var expression = new Expression('dot(1.0, 2.0)'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'dot(1.0, 2.0)'; + expect(shaderExpression).toEqual(expected); + }); + + it('gets shader expression for cross', function() { + var expression = new Expression('cross(vec3(1.0, 1.0, 1.0), vec3(2.0, 2.0, 2.0))'); + var shaderExpression = expression.getShaderExpression('', {}); + var expected = 'cross(vec3(1.0, 1.0, 1.0), vec3(2.0, 2.0, 2.0))'; + expect(shaderExpression).toEqual(expected); + }); + + it('throws when getting shader expression for regex', function() { + var expression = new Expression('regExp("a").test("abc")'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + + expression = new Expression('regExp("a(.)", "i").exec("Abc")'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + + expression = new Expression('regExp("a") =~ "abc"'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + + expression = new Expression('regExp("a") !~ "abc"'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for member expression with dot', function() { + var expression = new Expression('${property.name}'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for string member expression with brackets', function() { + var expression = new Expression('${property["name"]}'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for String', function() { + var expression = new Expression('String(1.0)'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for toString', function() { + var expression = new Expression('color("red").toString()'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for literal string', function() { + var expression = new Expression('"name"'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for variable in string', function() { + var expression = new Expression('"${property}"'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for literal undefined', function() { + var expression = new Expression('undefined'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for literal null', function() { + var expression = new Expression('null'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for isNaN', function() { + var expression = new Expression('isNaN(1.0)'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for isFinite', function() { + var expression = new Expression('isFinite(1.0)'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for isExactClass', function() { + var expression = new Expression('isExactClass("door")'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for isClass', function() { + var expression = new Expression('isClass("door")'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); + + it('throws when getting shader expression for getExactClassName', function() { + var expression = new Expression('getExactClassName()'); + expect(function() { + return expression.getShaderExpression('', {}); + }).toThrowRuntimeError(); + }); +}); diff --git a/Specs/Scene/Instanced3DModel3DTileContentSpec.js b/Specs/Scene/Instanced3DModel3DTileContentSpec.js new file mode 100644 index 000000000000..7ff687f5e473 --- /dev/null +++ b/Specs/Scene/Instanced3DModel3DTileContentSpec.js @@ -0,0 +1,313 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Instanced3DModel3DTileContent', + 'Core/Cartesian3', + 'Core/Color', + 'Core/HeadingPitchRange', + 'Core/HeadingPitchRoll', + 'Core/Transforms', + 'Scene/Cesium3DTileContentState', + 'Scene/TileBoundingSphere', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene' + ], function( + Instanced3DModel3DTileContent, + Cartesian3, + Color, + HeadingPitchRange, + HeadingPitchRoll, + Transforms, + Cesium3DTileContentState, + TileBoundingSphere, + Cesium3DTilesTester, + createScene) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + var gltfExternalUrl = './Data/Cesium3DTiles/Instanced/InstancedGltfExternal/'; + var withBatchTableUrl = './Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/'; + var withBatchTableBinaryUrl = './Data/Cesium3DTiles/Instanced/InstancedWithBatchTableBinary/'; + var withoutBatchTableUrl = './Data/Cesium3DTiles/Instanced/InstancedWithoutBatchTable/'; + var orientationUrl = './Data/Cesium3DTiles/Instanced/InstancedOrientation/'; + var oct16POrientationUrl = './Data/Cesium3DTiles/Instanced/InstancedOct32POrientation/'; + var scaleUrl = './Data/Cesium3DTiles/Instanced/InstancedScale/'; + var scaleNonUniformUrl = './Data/Cesium3DTiles/Instanced/InstancedScaleNonUniform/'; + var rtcUrl = './Data/Cesium3DTiles/Instanced/InstancedRTC'; + var quantizedUrl = './Data/Cesium3DTiles/Instanced/InstancedQuantized/'; + var quantizedOct32POrientationUrl = './Data/Cesium3DTiles/Instanced/InstancedQuantizedOct32POrientation/'; + var withTransformUrl = './Data/Cesium3DTiles/Instanced/InstancedWithTransform/'; + var withBatchIdsUrl = './Data/Cesium3DTiles/Instanced/InstancedWithBatchIds/'; + var texturedUrl = './Data/Cesium3DTiles/Instanced/InstancedTextured/'; + var compressedTexturesUrl = './Data/Cesium3DTiles/Instanced/InstancedCompressedTextures/'; + var gltfZUpUrl = './Data/Cesium3DTiles/Instanced/InstancedGltfZUp'; + + function setCamera(longitude, latitude) { + // One instance is located at the center, point the camera there + var center = Cartesian3.fromRadians(longitude, latitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 27.0)); + } + + beforeAll(function() { + scene = createScene(); + }); + + beforeEach(function() { + scene.morphTo3D(0.0); + setCamera(centerLongitude, centerLatitude); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + it('throws with invalid format', function() { + var arrayBuffer = Cesium3DTilesTester.generateInstancedTileBuffer({ + gltfFormat : 2 + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'i3dm'); + }); + + it('throws with invalid version', function() { + var arrayBuffer = Cesium3DTilesTester.generateInstancedTileBuffer({ + version : 2 + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'i3dm'); + }); + + it('throws with empty gltf', function() { + // Expect to throw DeveloperError in Model due to invalid gltf magic + var arrayBuffer = Cesium3DTilesTester.generateInstancedTileBuffer(); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'i3dm'); + }); + + it('resolves readyPromise', function() { + return Cesium3DTilesTester.resolvesReadyPromise(scene, withoutBatchTableUrl); + }); + + it('rejects readyPromise on error', function() { + // Try loading a tile with an invalid url. + // Expect promise to be rejected in Model, then in ModelInstanceCollection, and + // finally in Instanced3DModel3DTileContent. + var arrayBuffer = Cesium3DTilesTester.generateInstancedTileBuffer({ + gltfFormat : 0, + gltfUri : 'not-a-real-path' + }); + return Cesium3DTilesTester.rejectsReadyPromiseOnError(scene, arrayBuffer, 'i3dm'); + }); + + it('renders with external gltf', function() { + return Cesium3DTilesTester.loadTileset(scene, gltfExternalUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with batch table binary', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableBinaryUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders without batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with feature defined orientation', function() { + return Cesium3DTilesTester.loadTileset(scene, orientationUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with feature defined Oct32P encoded orientation', function() { + return Cesium3DTilesTester.loadTileset(scene, oct16POrientationUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with feature defined scale', function() { + return Cesium3DTilesTester.loadTileset(scene, scaleUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with feature defined non-uniform scale', function() { + return Cesium3DTilesTester.loadTileset(scene, scaleNonUniformUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with RTC_CENTER semantic', function() { + return Cesium3DTilesTester.loadTileset(scene, rtcUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with feature defined quantized position', function() { + return Cesium3DTilesTester.loadTileset(scene, quantizedUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with feature defined quantized position and Oct32P encoded orientation', function() { + return Cesium3DTilesTester.loadTileset(scene, quantizedOct32POrientationUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with batch ids', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchIdsUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with a gltf z-up axis', function() { + return Cesium3DTilesTester.loadTileset(scene, gltfZUpUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with tile transform', function() { + return Cesium3DTilesTester.loadTileset(scene, withTransformUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + + var newLongitude = -1.31962; + var newLatitude = 0.698874; + var newCenter = Cartesian3.fromRadians(newLongitude, newLatitude, 10.0); + var newTransform = Transforms.headingPitchRollToFixedFrame(newCenter, new HeadingPitchRoll()); + + // Update tile transform + tileset._root.transform = newTransform; + + // Move the camera to the new location + setCamera(newLongitude, newLatitude); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with textures', function() { + return Cesium3DTilesTester.loadTileset(scene, texturedUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders with compressed textures', function() { + return Cesium3DTilesTester.loadTileset(scene, compressedTexturesUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders in 2D', function() { + return Cesium3DTilesTester.loadTileset(scene, gltfExternalUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + tileset.maximumScreenSpaceError = 2.0; + scene.morphTo2D(0.0); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders in 2D with tile transform', function() { + return Cesium3DTilesTester.loadTileset(scene, withTransformUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + tileset.maximumScreenSpaceError = 2.0; + scene.morphTo2D(0.0); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders in CV', function() { + return Cesium3DTilesTester.loadTileset(scene, gltfExternalUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + scene.morphToColumbusView(0.0); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders in CV with tile transform', function() { + return Cesium3DTilesTester.loadTileset(scene, withTransformUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + scene.morphToColumbusView(0.0); + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + }); + }); + + it('renders when instancing is disabled', function() { + // Disable extension + var instancedArrays = scene.context._instancedArrays; + scene.context._instancedArrays = undefined; + + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + Cesium3DTilesTester.expectRenderTileset(scene, tileset); + // Re-enable extension + scene.context._instancedArrays = instancedArrays; + }); + }); + + it('throws when calling getFeature with invalid index', function() { + return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { + var content = tileset._root.content; + expect(function(){ + content.getFeature(-1); + }).toThrowDeveloperError(); + expect(function(){ + content.getFeature(10000); + }).toThrowDeveloperError(); + expect(function(){ + content.getFeature(); + }).toThrowDeveloperError(); + }); + }); + + it('gets memory usage', function() { + return Cesium3DTilesTester.loadTileset(scene, texturedUrl).then(function(tileset) { + var content = tileset._root.content; + + // Box model - 32 ushort indices and 24 vertices per building, 8 float components (position, normal, uv) per vertex. + // (24 * 8 * 4) + (36 * 2) = 840 + var geometryByteLength = 840; + + // Texture is 211x211 RGBA bytes, but upsampled to 256x256 because the wrap mode is REPEAT + var texturesByteLength = 262144; + + // One RGBA byte pixel per feature + var batchTexturesByteLength = content.featuresLength * 4; + var pickTexturesByteLength = content.featuresLength * 4; + + // Features have not been picked or colored yet, so the batch table contribution is 0. + expect(content.geometryByteLength).toEqual(geometryByteLength); + expect(content.texturesByteLength).toEqual(texturesByteLength); + expect(content.batchTableByteLength).toEqual(0); + + // Color a feature and expect the texture memory to increase + content.getFeature(0).color = Color.RED; + scene.renderForSpecs(); + expect(content.geometryByteLength).toEqual(geometryByteLength); + expect(content.texturesByteLength).toEqual(texturesByteLength); + expect(content.batchTableByteLength).toEqual(batchTexturesByteLength); + + // Pick the tile and expect the texture memory to increase + scene.pickForSpecs(); + expect(content.geometryByteLength).toEqual(geometryByteLength); + expect(content.texturesByteLength).toEqual(texturesByteLength); + expect(content.batchTableByteLength).toEqual(batchTexturesByteLength + pickTexturesByteLength); + }); + }); + + it('destroys', function() { + return Cesium3DTilesTester.tileDestroys(scene, withoutBatchTableUrl); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/ModelInstanceCollectionSpec.js b/Specs/Scene/ModelInstanceCollectionSpec.js new file mode 100644 index 000000000000..72b7b5e23caf --- /dev/null +++ b/Specs/Scene/ModelInstanceCollectionSpec.js @@ -0,0 +1,698 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/ModelInstanceCollection', + 'Core/BoundingSphere', + 'Core/Cartesian3', + 'Core/defaultValue', + 'Core/defined', + 'Core/HeadingPitchRange', + 'Core/HeadingPitchRoll', + 'Core/JulianDate', + 'Core/Math', + 'Core/Matrix4', + 'Core/PrimitiveType', + 'Core/Transforms', + 'Scene/Model', + 'Scene/ModelAnimationLoop', + 'Scene/SceneMode', + 'Scene/ShadowMode', + 'Specs/createScene', + 'Specs/pollToPromise', + 'ThirdParty/when' + ], function( + ModelInstanceCollection, + BoundingSphere, + Cartesian3, + defaultValue, + defined, + HeadingPitchRange, + HeadingPitchRoll, + JulianDate, + CesiumMath, + Matrix4, + PrimitiveType, + Transforms, + Model, + ModelAnimationLoop, + SceneMode, + ShadowMode, + createScene, + pollToPromise, + when) { + 'use strict'; + + var boxUrl = './Data/Models/Box/CesiumBoxTest.gltf'; + var cesiumAirUrl = './Data/Models/CesiumAir/Cesium_Air.gltf'; + var riggedFigureUrl = './Data/Models/rigged-figure-test/rigged-figure-test.gltf'; + var movingBoxUrl = './Data/Models/moving-box/moving-box.gltf'; + + var boxGltf; + var cesiumAirGltf; + var riggedFigureGltf; + var movingBoxGltf; + + var boxRadius; + + var scene; + + beforeAll(function() { + scene = createScene(); + + var modelPromises = []; + modelPromises.push(loadModel(boxUrl).then(function(model) { + boxGltf = model.gltf; + boxRadius = model.boundingSphere.radius; + scene.primitives.remove(model); + })); + modelPromises.push(loadModel(cesiumAirUrl).then(function(model) { + cesiumAirGltf = model.gltf; + scene.primitives.remove(model); + })); + modelPromises.push(loadModel(riggedFigureUrl).then(function(model) { + riggedFigureGltf = model.gltf; + scene.primitives.remove(model); + })); + modelPromises.push(loadModel(movingBoxUrl).then(function(model) { + movingBoxGltf = model.gltf; + scene.primitives.remove(model); + })); + + return when.all(modelPromises); + }); + + beforeEach(function() { + scene.morphTo3D(0.0); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + function loadModel(url) { + var model = scene.primitives.add(Model.fromGltf({ + url : url + })); + + return pollToPromise(function() { + // Render scene to progressively load the model + scene.renderForSpecs(); + return model.ready; + }).then(function() { + return model; + }); + } + + function loadCollection(options) { + var collection = scene.primitives.add(new ModelInstanceCollection(options)); + + return pollToPromise(function() { + // Render scene to progressively load the model + scene.renderForSpecs(); + return collection.ready; + }).then(function() { + zoomTo(collection, 0); + return collection; + }); + } + + function createInstances(count, heightOffset) { + heightOffset = defaultValue(heightOffset, 0.0); + + var spacing = 20.0; + var centerLongitude = -123.0744619; + var centerLatitude = 44.0503706; + var height = 5000.0 + heightOffset; + + var instances = []; + for (var i = 0; i < count; ++i) { + var instanceHeight = height + spacing * i; + var position = Cartesian3.fromDegrees(centerLongitude, centerLatitude, instanceHeight); + var heading = Math.PI/2.0; + var pitch = 0.0; + var roll = 0.0; + var hpr = new HeadingPitchRoll(heading, pitch, roll); + var modelMatrix = Transforms.headingPitchRollToFixedFrame(position, hpr); + instances.push({ + modelMatrix : modelMatrix + }); + } + + return instances; + } + + function getBoundingSphere(instances, modelRadius) { + var length = instances.length; + var points = new Array(length); + for (var i = 0; i < length; ++i) { + var translation = new Cartesian3(); + Matrix4.getTranslation(instances[i].modelMatrix, translation); + points[i] = translation; + } + var boundingSphere = new BoundingSphere(); + BoundingSphere.fromPoints(points, boundingSphere); + boundingSphere.radius += modelRadius; + return boundingSphere; + } + + var centerScratch = new Cartesian3(); + + function zoomTo(collection, instance) { + var center = Matrix4.getTranslation(collection._instances[instance].modelMatrix, centerScratch); + var camera = scene.camera; + camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, 10.0)); + } + + function expectRender(collection, expectColor, time) { + expectColor = defaultValue(expectColor, true); + + collection.show = false; + expect(scene).toRender([0, 0, 0, 255]); + collection.show = true; + + // Verify each instance + var length = collection.length; + for (var i = 0; i < length; ++i) { + zoomTo(collection, i); + if (expectColor) { + expect({ + scene : scene, + time : time + }).notToRender([0, 0, 0, 255]); + } else { + expect({ + scene : scene, + time : time + }).toRender([0, 0, 0, 255]); + } + } + } + + function verifyPickedInstance(collection, instanceId) { + return function(result) { + expect(result.primitive).toBe(collection); + expect(result.modelMatrix).toBeDefined(); + expect(result.instanceId).toBe(instanceId); + expect(result.model).toBe(collection._model); + }; + } + + function expectPick(collection) { + collection.show = false; + expect(scene).notToPick(); + collection.show = true; + + // Verify each instance + var length = collection.length; + for (var i = 0; i < length; ++i) { + zoomTo(collection, i); + expect(scene).toPickAndCall(verifyPickedInstance(collection, i)); + } + } + + it('throws if neither options.gltf nor options.url are provided', function() { + expect(function() { + return new ModelInstanceCollection(); + }).toThrowDeveloperError(); + }); + + it('throws when both options.gltf and options.url are provided', function() { + expect(function() { + return new ModelInstanceCollection({ + url : boxUrl, + gltf : boxGltf + }); + }).toThrowDeveloperError(); + }); + + it('sets properties', function() { + return loadCollection({ + url : boxUrl, + instances : createInstances(4) + }).then(function(collection) { + expect(collection.ready).toEqual(true); + expect(collection.show).toEqual(true); + expect(collection.allowPicking).toEqual(true); + expect(collection.length).toEqual(4); + expect(collection.debugShowBoundingVolume).toEqual(false); + expect(collection.debugWireframe).toEqual(false); + expect(collection._dynamic).toEqual(false); + expect(collection._cull).toEqual(true); + expect(collection._model).toBeDefined(); + expect(collection._model.ready).toEqual(true); + + if (collection._instancingSupported) { + expect(collection._model.cacheKey).toEqual(boxUrl + '#instanced'); + } + }); + }); + + it('renders from url', function() { + return loadCollection({ + url : boxUrl, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + }); + }); + + it('renders from gltf', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + }); + }); + + it('resolves readyPromise', function() { + var collection = scene.primitives.add(new ModelInstanceCollection({ + gltf : boxGltf, + instances : createInstances(4) + })); + + scene.renderForSpecs(); + scene.renderForSpecs(); + + return collection.readyPromise.then(function(collection) { + expect(collection.ready).toEqual(true); + }); + }); + + it('rejects readyPromise on error', function() { + // Expect promise to be rejected in Model, then in ModelInstanceCollection. + var collection = scene.primitives.add(new ModelInstanceCollection({ + url : 'invalid.gltf', + instances : createInstances(4) + })); + + collection.update(scene.frameState); + + return collection.readyPromise.then(function(collection) { + fail('should not resolve'); + }).otherwise(function(error) { + expect(collection.ready).toEqual(false); + }); + }); + + it('renders one instance', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(1) + }).then(function(collection) { + expectRender(collection); + }); + }); + + it('renders zero instances', function() { + var collection = scene.primitives.add(new ModelInstanceCollection({ + gltf : boxGltf, + instances : createInstances(0) + })); + + // Collection never reaches the ready state due to returning early + for (var i = 0; i < 10; ++i) { + expectRender(collection, false); + expect(collection.ready).toBe(false); + } + }); + + it('renders 100 instances', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(100) + }).then(function(collection) { + expectRender(collection); + }); + }); + + it('renders cesiumAir', function() { + return loadCollection({ + gltf : cesiumAirGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + }); + }); + + it('renders rigged figure', function() { + return loadCollection({ + gltf : riggedFigureGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + }); + }); + + it('renders when instancing is disabled', function() { + // Disable extension + var instancedArrays = scene.context._instancedArrays; + scene.context._instancedArrays = undefined; + + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + // Re-enable extension + scene.context._instancedArrays = instancedArrays; + }); + }); + + it('renders when dynamic is true', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4), + dynamic : true + }).then(function(collection) { + expectRender(collection); + }); + }); + + it('verify bounding volume', function() { + var instances = createInstances(4); + return loadCollection({ + gltf : boxGltf, + instances : instances + }).then(function(collection) { + var boundingSphere = getBoundingSphere(instances, boxRadius); + expect(collection._boundingSphere.center).toEqual(boundingSphere.center); + expect(collection._boundingSphere.radius).toEqual(boundingSphere.radius); + }); + }); + + it('renders bounding volume', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + collection.debugShowBoundingVolume = true; + expectRender(collection); + }); + }); + + it('renders in wireframe', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + collection.debugWireframe = true; + scene.renderForSpecs(); + expect(collection._drawCommands[0].primitiveType).toEqual(PrimitiveType.LINES); + }); + }); + + it('renders with animations', function() { + // Test that all instances are being animated. + // The moving box is in view on frame 1 and out of view by frame 5. + return loadCollection({ + gltf : movingBoxGltf, + instances : createInstances(4) + }).then(function(collection) { + collection.activeAnimations.addAll(); + + // Render when animation is in view + var time = JulianDate.now(); + expectRender(collection, true, time); + + // Render when animation is out of view + time = JulianDate.addSeconds(time, 0.1, new JulianDate()); + expectRender(collection, false, time); + }); + }); + + it('renders with animations when instancing is disabled', function() { + // Instance transforms are updated differently when instancing is disabled + + // Disable extension + var instancedArrays = scene.context._instancedArrays; + scene.context._instancedArrays = undefined; + + return loadCollection({ + gltf : movingBoxGltf, + instances : createInstances(4) + }).then(function(collection) { + collection.activeAnimations.addAll(); + + // Render when animation is in view + var time = JulianDate.now(); + expectRender(collection, true, time); + + // Render when animation is out of view + time = JulianDate.addSeconds(time, 0.1, new JulianDate()); + expectRender(collection, false, time); + + // Re-enable extension + scene.context._instancedArrays = instancedArrays; + }); + }); + + it('renders two model instance collections that use the same cache key', function() { + var collections = []; + var promises = []; + + promises.push(loadCollection({ + url : boxUrl, + instances : createInstances(2) + }).then(function(collection) { + collections.push(collection); + })); + + promises.push(loadCollection({ + url : boxUrl, + instances : createInstances(2, 1000.0) + }).then(function(collection) { + collections.push(collection); + })); + + return when.all(promises).then(function() { + var resourcesFirst = collections[0]._model._rendererResources; + var resourcesSecond = collections[1]._model._rendererResources; + var name; + + expect(collections[0]._model.cacheKey).toEqual(collections[1]._model.cacheKey); + zoomTo(collections[0], 0); + expectRender(collections[0]); + zoomTo(collections[1], 0); + expectRender(collections[1]); + + // Check that buffers are equal + for (name in resourcesFirst.buffers) { + if (resourcesFirst.buffers.hasOwnProperty(name)) { + expect(resourcesFirst.buffers[name]).toEqual(resourcesSecond.buffers[name]); + } + } + + // Check that programs are equal + for (name in resourcesFirst.programs) { + if (resourcesFirst.programs.hasOwnProperty(name)) { + expect(resourcesFirst.programs[name]).toEqual(resourcesSecond.programs[name]); + } + } + + if (collections[0]._instancingSupported) { + // Check that vertex arrays are different, since each collection has a unique vertex buffer for instanced attributes. + for (name in resourcesFirst.vertexArrays) { + if (resourcesFirst.vertexArrays.hasOwnProperty(name)) { + expect(resourcesFirst.vertexArrays[name]).not.toEqual(resourcesSecond.vertexArrays[name]); + } + } + } + }); + }); + + it('culls when out of view and cull is true', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4), + cull : true + }).then(function(collection) { + scene.renderForSpecs(); + expect(scene._frustumCommandsList.length).not.toEqual(0); + scene.camera.lookAt(new Cartesian3(100000.0, 0.0, 0.0), new HeadingPitchRange(0.0, 0.0, 10.0)); + scene.renderForSpecs(); + expect(scene._frustumCommandsList.length).toEqual(0); + }); + }); + + it('does not cull when out of view and cull is false', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4), + cull : false + }).then(function(collection) { + scene.renderForSpecs(); + expect(scene._frustumCommandsList.length).not.toEqual(0); + scene.camera.lookAt(new Cartesian3(100000.0, 0.0, 0.0), new HeadingPitchRange(0.0, 0.0, 10.0)); + scene.renderForSpecs(); + expect(scene._frustumCommandsList.length).not.toEqual(0); + }); + }); + + it('shadows', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + scene.renderForSpecs(); + expect(collection._shadows).toBe(ShadowMode.ENABLED); + var drawCommand = collection._drawCommands[0]; + expect(drawCommand.castShadows).toBe(true); + expect(drawCommand.receiveShadows).toBe(true); + collection.shadows = ShadowMode.DISABLED; + scene.renderForSpecs(); + expect(drawCommand.castShadows).toBe(false); + expect(drawCommand.receiveShadows).toBe(false); + }); + }); + + it('picks', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectPick(collection); + }); + }); + + it('picks when instancing is disabled', function() { + // Disable extension + var instancedArrays = scene.context._instancedArrays; + scene.context._instancedArrays = undefined; + + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectPick(collection); + // Re-enable extension + scene.context._instancedArrays = instancedArrays; + }); + }); + + it('moves instance', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expect(scene).toPickAndCall(function(result) { + var originalMatrix = result.modelMatrix; + result.modelMatrix = Matrix4.IDENTITY; + expect(scene).notToPick(); + result.modelMatrix = originalMatrix; + expect(scene).toPickPrimitive(collection); + }); + }); + }); + + it('moves instance when instancing is disabled', function() { + // Disable extension + var instancedArrays = scene.context._instancedArrays; + scene.context._instancedArrays = undefined; + + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expect(scene).toPickAndCall(function(result) { + var originalMatrix = result.modelMatrix; + var originalRadius = collection._boundingSphere.radius; + result.modelMatrix = Matrix4.IDENTITY; + expect(scene).notToPick(); + expect(collection._boundingSphere.radius).toBeGreaterThan(originalRadius); + result.modelMatrix = originalMatrix; + expect(scene).toPickPrimitive(collection); + }); + // Re-enable extension + scene.context._instancedArrays = instancedArrays; + }); + }); + + it('renders in 2D', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + scene.morphTo2D(0.0); + expectRender(collection); + }); + }); + + it('renders in 2D when instancing is disabled', function() { + // Disable extension + var instancedArrays = scene.context._instancedArrays; + scene.context._instancedArrays = undefined; + + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + scene.morphTo2D(0.0); + expectRender(collection); + + // Re-enable extension + scene.context._instancedArrays = instancedArrays; + }); + }); + + it('renders in CV', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + scene.morphToColumbusView(0.0); + expectRender(collection); + }); + }); + + it('renders in CV when instancing is disabled', function() { + // Disable extension + var instancedArrays = scene.context._instancedArrays; + scene.context._instancedArrays = undefined; + + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expectRender(collection); + scene.morphToColumbusView(0.0); + expectRender(collection); + + // Re-enable extension + scene.context._instancedArrays = instancedArrays; + }); + }); + + it('does not render during morph', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4), + cull : false + }).then(function() { + var commandList = scene.frameState.commandList; + scene.renderForSpecs(); + expect(commandList.length).toBeGreaterThan(0); + scene.morphToColumbusView(1.0); + scene.renderForSpecs(); + expect(commandList.length).toBe(0); + }); + }); + + it('destroys', function() { + return loadCollection({ + gltf : boxGltf, + instances : createInstances(4) + }).then(function(collection) { + expect(collection.isDestroyed()).toEqual(false); + scene.primitives.remove(collection); + expect(collection.isDestroyed()).toEqual(true); + }); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js index 305de2fb3807..09c8c2a3ef6d 100644 --- a/Specs/Scene/ModelSpec.js +++ b/Specs/Scene/ModelSpec.js @@ -350,6 +350,23 @@ defineSuite([ }); }); + it('does not render during morph', function() { + var commandList = scene.frameState.commandList; + var model = texturedBoxModel; + model.show = true; + model.cull = false; + expect(model.ready).toBe(true); + + scene.renderForSpecs(); + expect(commandList.length).toBeGreaterThan(0); + + scene.morphTo2D(1.0); + scene.renderForSpecs(); + expect(commandList.length).toBe(0); + scene.completeMorph(); + model.show = false; + }); + it('Renders x-up model', function() { return loadJson(boxEcefUrl).then(function(gltf) { // Model data is z-up. Edit the transform to be z-up to x-up. @@ -2312,24 +2329,24 @@ defineSuite([ it('gets memory usage', function() { // Texture is originally 211*211 but is scaled up to 256*256 to support its minification filter and then is mipmapped var expectedTextureMemory = Math.floor(256*256*4*(4/3)); - var expectedVertexMemory = 840; + var expectedGeometryMemory = 840; var options = { cacheKey : 'memory-usage-test', incrementallyLoadTextures : false }; return loadModel(texturedBoxUrl, options).then(function(model) { // The first model owns the resources - expect(model.vertexMemorySizeInBytes).toBe(expectedVertexMemory); - expect(model.textureMemorySizeInBytes).toBe(expectedTextureMemory); - expect(model.cachedVertexMemorySizeInBytes).toBe(0); - expect(model.cachedTextureMemorySizeInBytes).toBe(0); + expect(model.geometryByteLength).toBe(expectedGeometryMemory); + expect(model.texturesByteLength).toBe(expectedTextureMemory); + expect(model.cachedGeometryByteLength).toBe(0); + expect(model.cachedTexturesByteLength).toBe(0); return loadModel(texturedBoxUrl, options).then(function(model) { // The second model is sharing the resources, so its memory usage is reported as 0 - expect(model.vertexMemorySizeInBytes).toBe(0); - expect(model.textureMemorySizeInBytes).toBe(0); - expect(model.cachedVertexMemorySizeInBytes).toBe(expectedVertexMemory); - expect(model.cachedTextureMemorySizeInBytes).toBe(expectedTextureMemory); + expect(model.geometryByteLength).toBe(0); + expect(model.texturesByteLength).toBe(0); + expect(model.cachedGeometryByteLength).toBe(expectedGeometryMemory); + expect(model.cachedTexturesByteLength).toBe(expectedTextureMemory); }); }); }); diff --git a/Specs/Scene/PointCloud3DTileContentSpec.js b/Specs/Scene/PointCloud3DTileContentSpec.js new file mode 100644 index 000000000000..e4bf3023811a --- /dev/null +++ b/Specs/Scene/PointCloud3DTileContentSpec.js @@ -0,0 +1,693 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/PointCloud3DTileContent', + 'Core/Cartesian3', + 'Core/Color', + 'Core/ComponentDatatype', + 'Core/defined', + 'Core/HeadingPitchRange', + 'Core/HeadingPitchRoll', + 'Core/Math', + 'Core/Transforms', + 'Scene/Cesium3DTileStyle', + 'Scene/Expression', + 'Scene/PerspectiveFrustum', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene', + 'ThirdParty/when' + ], function( + PointCloud3DTileContent, + Cartesian3, + Color, + ComponentDatatype, + defined, + HeadingPitchRange, + HeadingPitchRoll, + CesiumMath, + Transforms, + Cesium3DTileStyle, + Expression, + PerspectiveFrustum, + Cesium3DTilesTester, + createScene, + when) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + var pointCloudRGBUrl = './Data/Cesium3DTiles/PointCloud/PointCloudRGB'; + var pointCloudRGBAUrl = './Data/Cesium3DTiles/PointCloud/PointCloudRGBA'; + var pointCloudRGB565Url = './Data/Cesium3DTiles/PointCloud/PointCloudRGB565'; + var pointCloudNoColorUrl = './Data/Cesium3DTiles/PointCloud/PointCloudNoColor'; + var pointCloudConstantColorUrl = './Data/Cesium3DTiles/PointCloud/PointCloudConstantColor'; + var pointCloudNormalsUrl = './Data/Cesium3DTiles/PointCloud/PointCloudNormals'; + var pointCloudNormalsOctEncodedUrl = './Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded'; + var pointCloudQuantizedUrl = './Data/Cesium3DTiles/PointCloud/PointCloudQuantized'; + var pointCloudQuantizedOctEncodedUrl = './Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded'; + var pointCloudWGS84Url = './Data/Cesium3DTiles/PointCloud/PointCloudWGS84'; + var pointCloudBatchedUrl = './Data/Cesium3DTiles/PointCloud/PointCloudBatched'; + var pointCloudWithPerPointPropertiesUrl = './Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties'; + var pointCloudWithTransformUrl = './Data/Cesium3DTiles/PointCloud/PointCloudWithTransform'; + + function setCamera(longitude, latitude) { + // Point the camera to the center of the tile + var center = Cartesian3.fromRadians(longitude, latitude, 5.0); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 5.0)); + } + + beforeAll(function() { + scene = createScene(); + scene.frameState.passes.render = true; + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + beforeEach(function() { + scene.morphTo3D(0.0); + + var camera = scene.camera; + camera.frustum = new PerspectiveFrustum(); + camera.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; + camera.frustum.fov = CesiumMath.toRadians(60.0); + + setCamera(centerLongitude, centerLatitude); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + it('throws with invalid version', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + version: 2 + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'pnts'); + }); + + it('throws if featureTableJsonByteLength is 0', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJsonByteLength : 0 + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'pnts'); + }); + + it('throws if the feature table does not contain POINTS_LENGTH', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson : { + POSITION : { + byteOffset : 0 + } + } + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'pnts'); + }); + + it('throws if the feature table does not contain POSITION or POSITION_QUANTIZED', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson : { + POINTS_LENGTH : 1 + } + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'pnts'); + }); + + it('throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_SCALE', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson : { + POINTS_LENGTH : 1, + POSITION_QUANTIZED : { + byteOffset : 0 + }, + QUANTIZED_VOLUME_OFFSET : [0.0, 0.0, 0.0] + } + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'pnts'); + }); + + it('throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_OFFSET', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson : { + POINTS_LENGTH : 1, + POSITION_QUANTIZED : { + byteOffset : 0 + }, + QUANTIZED_VOLUME_SCALE : [1.0, 1.0, 1.0] + } + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'pnts'); + }); + + it('throws if the BATCH_ID semantic is defined but BATCHES_LENGTH is not', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson : { + POINTS_LENGTH : 2, + POSITION : [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + BATCH_ID : [0, 1] + } + }); + Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, 'pnts'); + }); + + it('BATCH_ID semantic uses componentType of UNSIGNED_SHORT by default', function() { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson : { + POINTS_LENGTH : 2, + POSITION : [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + BATCH_ID : [0, 1], + BATCH_LENGTH : 2 + } + }); + var content = Cesium3DTilesTester.loadTile(scene, arrayBuffer, 'pnts'); + expect(content._drawCommand._vertexArray._attributes[1].componentDatatype).toEqual(ComponentDatatype.UNSIGNED_SHORT); + }); + + it('resolves readyPromise', function() { + return Cesium3DTilesTester.resolvesReadyPromise(scene, pointCloudRGBUrl); + }); + + it('renders point cloud with rgb colors', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with rgba colors', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBAUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with rgb565 colors', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGB565Url).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with no colors', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with constant colors', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudConstantColorUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with normals', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudNormalsUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with oct encoded normals', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudNormalsOctEncodedUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with quantized positions', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudQuantizedUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with quantized positions and oct-encoded normals', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudQuantizedOctEncodedUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud that are not defined relative to center', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudWGS84Url).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with per-point properties', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudWithPerPointPropertiesUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders point cloud with tile transform', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudWithTransformUrl).then(function(tileset) { + Cesium3DTilesTester.expectRender(scene, tileset); + + var newLongitude = -1.31962; + var newLatitude = 0.698874; + var newCenter = Cartesian3.fromRadians(newLongitude, newLatitude, 5.0); + var newHPR = new HeadingPitchRoll(); + var newTransform = Transforms.headingPitchRollToFixedFrame(newCenter, newHPR); + + // Update tile transform + tileset._root.transform = newTransform; + + // Move the camera to the new location + setCamera(newLongitude, newLatitude); + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders with debug color', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var color; + expect(scene).toRenderAndCall(function(rgba) { + color = rgba; + }); + tileset.debugColorizeTiles = true; + expect(scene).notToRender(color); + tileset.debugColorizeTiles = false; + expect(scene).toRender(color); + }); + }); + + it('renders in CV', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + scene.morphToColumbusView(0.0); + setCamera(centerLongitude, centerLatitude); + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('renders in 2D', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + scene.morphTo2D(0.0); + setCamera(centerLongitude, centerLatitude); + tileset.maximumScreenSpaceError = 3; + Cesium3DTilesTester.expectRender(scene, tileset); + }); + }); + + it('picks', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var content = tileset._root.content; + tileset.show = false; + expect(scene).toPickPrimitive(undefined); + tileset.show = true; + expect(scene).toPickAndCall(function(result) { + expect(result).toBeDefined(); + expect(result.primitive).toBe(tileset); + expect(result.content).toBe(content); + }); + }); + }); + + it('picks based on batchId', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(function(tileset) { + // Get the original color + var color; + expect(scene).toRenderAndCall(function(rgba) { + color = rgba; + }); + + // Change the color of the picked feature to yellow + expect(scene).toPickAndCall(function(first) { + expect(first).toBeDefined(); + + first.color = Color.clone(Color.YELLOW, first.color); + + // Expect the pixel color to be some shade of yellow + expect(scene).notToRender(color); + + // Turn show off. Expect a different feature to get picked. + first.show = false; + expect(scene).toPickAndCall(function(second) { + expect(second).toBeDefined(); + expect(second).not.toBe(first); + }); + }); + }); + }); + + it('point cloud without batch table works', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var content = tileset._root.content; + expect(content.featuresLength).toBe(0); + expect(content.innerContents).toBeUndefined(); + expect(content.hasProperty(0, 'name')).toBe(false); + expect(content.getFeature(0)).toBeUndefined(); + }); + }); + + it('batched point cloud works', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(function(tileset) { + var content = tileset._root.content; + expect(content.featuresLength).toBe(8); + expect(content.innerContents).toBeUndefined(); + expect(content.hasProperty(0, 'name')).toBe(true); + expect(content.getFeature(0)).toBeDefined(); + }); + }); + + it('point cloud with per-point properties work', function() { + // When the batch table contains per-point properties, aka no batching, then a Cesium3DTileBatchTable is not + // created. There is no per-point show/color/pickId because the overhead is too high. Instead points are styled + // based on their properties, and these are not accessible from the API. + return Cesium3DTilesTester.loadTileset(scene, pointCloudWithPerPointPropertiesUrl).then(function(tileset) { + var content = tileset._root.content; + expect(content.featuresLength).toBe(0); + expect(content.innerContents).toBeUndefined(); + expect(content.hasProperty(0, 'name')).toBe(false); + expect(content.getFeature(0)).toBeUndefined(); + }); + }); + + it('throws when calling getFeature with invalid index', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(function(tileset) { + var content = tileset._root.content; + expect(function(){ + content.getFeature(-1); + }).toThrowDeveloperError(); + expect(function(){ + content.getFeature(1000); + }).toThrowDeveloperError(); + expect(function(){ + content.getFeature(); + }).toThrowDeveloperError(); + }); + }); + + it('Supports back face culling when there are per-point normals', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(function(tileset) { + var content = tileset._root.content; + + // Get the number of picked sections with back face culling on + var pickedCountCulling = 0; + var pickedCount = 0; + var picked; + + expect(scene).toPickAndCall(function(result) { + // Set culling to true + content.backFaceCulling = true; + + expect(scene).toPickAndCall(function(result) { + picked = result; + }); + + /* jshint loopfunc: true */ + while (defined(picked)) { + picked.show = false; + expect(scene).toPickAndCall(function(result) { + picked = result; + }); + ++pickedCountCulling; + } + + // Set the shows back to true + var length = content.featuresLength; + for (var i = 0; i < length; ++i) { + var feature = content.getFeature(i); + feature.show = true; + } + + // Set culling to false + content.backFaceCulling = false; + + expect(scene).toPickAndCall(function(result) { + picked = result; + }); + + /* jshint loopfunc: true */ + while (defined(picked)) { + picked.show = false; + expect(scene).toPickAndCall(function(result) { + picked = result; + }); + ++pickedCount; + } + + expect(pickedCount).toBeGreaterThan(pickedCountCulling); + }); + }); + }); + + it('applies shader style', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudWithPerPointPropertiesUrl).then(function(tileset) { + var content = tileset._root.content; + + // Solid red color + tileset.style = new Cesium3DTileStyle({ + color : 'color("red")' + }); + expect(scene).toRender([255, 0, 0, 255]); + expect(content._styleTranslucent).toBe(false); + + // Applies translucency + tileset.style = new Cesium3DTileStyle({ + color : 'rgba(255, 0, 0, 0.005)' + }); + expect(scene).toRenderAndCall(function(rgba) { + // Pixel is a darker red + expect(rgba[0]).toBeLessThan(255); + expect(rgba[1]).toBe(0); + expect(rgba[2]).toBe(0); + expect(rgba[3]).toBe(255); + expect(content._styleTranslucent).toBe(true); + }); + + // Style with property + tileset.style = new Cesium3DTileStyle({ + color : 'color() * ${temperature}' + }); + expect(scene).toRenderAndCall(function(rgba) { + // Pixel color is some shade of gray + expect(rgba[0]).toBe(rgba[1]); + expect(rgba[0]).toBe(rgba[2]); + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[0]).toBeLessThan(255); + }); + + // When no conditions are met the default color is white + tileset.style = new Cesium3DTileStyle({ + color : { + conditions : [ + ['${secondaryColor}[0] > 1.0', 'color("red")'] // This condition will not be met + ] + } + }); + expect(scene).toRender([255, 255, 255, 255]); + + // Apply style with conditions + tileset.style = new Cesium3DTileStyle({ + color : { + conditions : [ + ['${temperature} < 0.1', 'color("#000099")'], + ['${temperature} < 0.2', 'color("#00cc99", 1.0)'], + ['${temperature} < 0.3', 'color("#66ff33", 0.5)'], + ['${temperature} < 0.4', 'rgba(255, 255, 0, 0.1)'], + ['${temperature} < 0.5', 'rgb(255, 128, 0)'], + ['${temperature} < 0.6', 'color("red")'], + ['${temperature} < 0.7', 'color("rgb(255, 102, 102)")'], + ['${temperature} < 0.8', 'hsl(0.875, 1.0, 0.6)'], + ['${temperature} < 0.9', 'hsla(0.83, 1.0, 0.5, 0.1)'], + ['true', 'color("#FFFFFF", 1.0)'] + ] + } + }); + expect(scene).notToRender([0, 0, 0, 255]); + + // Apply show style + tileset.style = new Cesium3DTileStyle({ + show : true + }); + expect(scene).notToRender([0, 0, 0, 255]); + + // Apply show style that hides all points + tileset.style = new Cesium3DTileStyle({ + show : false + }); + expect(scene).toRender([0, 0, 0, 255]); + + // Apply show style with property + tileset.style = new Cesium3DTileStyle({ + show : '${temperature} > 0.1' + }); + expect(scene).notToRender([0, 0, 0, 255]); + tileset.style = new Cesium3DTileStyle({ + show : '${temperature} > 1.0' + }); + expect(scene).toRender([0, 0, 0, 255]); + + // Apply style with point cloud semantics + tileset.style = new Cesium3DTileStyle({ + color : '${COLOR} / 2.0', + show : '${POSITION}[0] > 0.5' + }); + expect(scene).notToRender([0, 0, 0, 255]); + + // Apply pointSize style + tileset.style = new Cesium3DTileStyle({ + pointSize : 5.0 + }); + expect(scene).notToRender([0, 0, 0, 255]); + }); + }); + + it('rebuilds shader style when expression changes', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudWithPerPointPropertiesUrl).then(function(tileset) { + // Solid red color + tileset.style = new Cesium3DTileStyle({ + color : 'color("red")' + }); + expect(scene).toRender([255, 0, 0, 255]); + + tileset.style.color = new Expression('color("lime")'); + tileset.makeStyleDirty(); + expect(scene).toRender([0, 255, 0, 255]); + }); + }); + + it('applies shader style to point cloud with normals', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudQuantizedOctEncodedUrl).then(function(tileset) { + tileset.style = new Cesium3DTileStyle({ + color : 'color("red")' + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + expect(rgba[0]).toBeLessThan(255); + }); + }); + }); + + it('applies shader style to point cloud with normals', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudQuantizedOctEncodedUrl).then(function(tileset) { + tileset.style = new Cesium3DTileStyle({ + color : 'color("red")' + }); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba[0]).toBeGreaterThan(0); + }); + }); + }); + + it('applies shader style to point cloud without colors', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(function(tileset) { + tileset.style = new Cesium3DTileStyle({ + color : 'color("red")' + }); + expect(scene).toRender([255, 0, 0, 255]); + }); + }); + + it('throws if style references the NORMAL semantic but the point cloud does not have per-point normals', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var content = tileset._root.content; + expect(function() { + content.applyStyle(scene.frameState, new Cesium3DTileStyle({ + color : '${NORMAL}[0] > 0.5' + })); + }).toThrowRuntimeError(); + }); + }); + + it('throws when shader style reference a non-existent property', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudWithPerPointPropertiesUrl).then(function(tileset) { + var content = tileset._root.content; + expect(function() { + content.applyStyle(scene.frameState, new Cesium3DTileStyle({ + color : 'color() * ${non_existent_property}' + })); + }).toThrowRuntimeError(); + }); + }); + + it('does not apply shader style if the point cloud has a batch table', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(function(tileset) { + var content = tileset._root.content; + var shaderProgram = content._drawCommand.shaderProgram; + tileset.style = new Cesium3DTileStyle({ + color:'color("red")' + }); + expect(content._drawCommand.shaderProgram).toBe(shaderProgram); + + // Point cloud is styled through the batch table + expect(scene).notToRender([0, 0, 0, 255]); + }); + }); + + it('throws when shader style is invalid', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var content = tileset._root.content; + expect(function() { + content.applyStyle(scene.frameState, new Cesium3DTileStyle({ + show : '1 < "2"' + })); + }).toThrowRuntimeError(); + }); + }); + + it('gets memory usage', function() { + var promises = [ + Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl), + Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl), + Cesium3DTilesTester.loadTileset(scene, pointCloudNormalsUrl), + Cesium3DTilesTester.loadTileset(scene, pointCloudQuantizedOctEncodedUrl) + ]; + + // 1000 points + var expectedGeometryMemory = [ + 1000 * 12, // 3 floats (xyz) + 1000 * 15, // 3 floats (xyz), 3 bytes (rgb) + 1000 * 27, // 3 floats (xyz), 3 bytes (rgb), 3 floats (normal) + 1000 * 11 // 3 shorts (quantized xyz), 3 bytes (rgb), 2 bytes (oct-encoded normal) + ]; + + return when.all(promises).then(function(tilesets) { + var length = tilesets.length; + for (var i = 0; i < length; ++i) { + var content = tilesets[i]._root.content; + expect(content.geometryByteLength).toEqual(expectedGeometryMemory[i]); + expect(content.texturesByteLength).toEqual(0); + } + }); + }); + + it('gets memory usage for batch point cloud', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(function(tileset) { + var content = tileset._root.content; + + // Point cloud consists of positions, colors, normals, and batchIds + // 3 floats (xyz), 3 floats (normal), 1 byte (batchId) + var pointCloudGeometryMemory = 1000 * 25; + + // One RGBA byte pixel per feature + var batchTexturesByteLength = content.featuresLength * 4; + var pickTexturesByteLength = content.featuresLength * 4; + + // Features have not been picked or colored yet, so the batch table contribution is 0. + expect(content.geometryByteLength).toEqual(pointCloudGeometryMemory); + expect(content.texturesByteLength).toEqual(0); + expect(content.batchTableByteLength).toEqual(0); + + // Color a feature and expect the texture memory to increase + content.getFeature(0).color = Color.RED; + scene.renderForSpecs(); + expect(content.geometryByteLength).toEqual(pointCloudGeometryMemory); + expect(content.texturesByteLength).toEqual(0); + expect(content.batchTableByteLength).toEqual(batchTexturesByteLength); + + // Pick the tile and expect the texture memory to increase + scene.pickForSpecs(); + expect(content.geometryByteLength).toEqual(pointCloudGeometryMemory); + expect(content.texturesByteLength).toEqual(0); + expect(content.batchTableByteLength).toEqual(batchTexturesByteLength + pickTexturesByteLength); + }); + }); + + it('destroys', function() { + return Cesium3DTilesTester.tileDestroys(scene, pointCloudRGBUrl); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/StyleExpressionSpec.js b/Specs/Scene/StyleExpressionSpec.js new file mode 100644 index 000000000000..c48a33b53ccd --- /dev/null +++ b/Specs/Scene/StyleExpressionSpec.js @@ -0,0 +1,29 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/StyleExpression' + ], function( + StyleExpression) { + 'use strict'; + + var frameState = {}; + + function MockFeature() { + } + + MockFeature.prototype.getProperty = function(name) { + return undefined; + }; + + it('throws', function() { + var expression = new StyleExpression(); + var feature = new MockFeature(); + + expect(function() { + return expression.evaluate(frameState, feature); + }).toThrowDeveloperError(); + + expect(function() { + return expression.evaluateColor(frameState, feature); + }).toThrowDeveloperError(); + }); +}); diff --git a/Specs/Scene/Tileset3DTileContentSpec.js b/Specs/Scene/Tileset3DTileContentSpec.js new file mode 100644 index 000000000000..36b536d93db9 --- /dev/null +++ b/Specs/Scene/Tileset3DTileContentSpec.js @@ -0,0 +1,69 @@ +/*global defineSuite*/ +defineSuite([ + 'Scene/Tileset3DTileContent', + 'Core/Cartesian3', + 'Core/HeadingPitchRange', + 'Scene/Cesium3DTileContentState', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene' + ], function( + Tileset3DTileContent, + Cartesian3, + HeadingPitchRange, + Cesium3DTileContentState, + Cesium3DTilesTester, + createScene) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + var tilesetOfTilesetsUrl = './Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/'; + + beforeAll(function() { + scene = createScene(); + + // Point the camera at the center and far enough way to only load the root tile + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 100.0)); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + it('resolves readyPromise', function() { + return Cesium3DTilesTester.resolvesReadyPromise(scene, tilesetOfTilesetsUrl); + }); + + it('destroys', function() { + return Cesium3DTilesTester.tileDestroys(scene, tilesetOfTilesetsUrl); + }); + + it('gets properties', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { + var tile = tileset._root; + var content = tile.content; + expect(content.featuresLength).toBe(0); + expect(content.pointsLength).toBe(0); + expect(content.trianglesLength).toBe(0); + expect(content.geometryByteLength).toBe(0); + expect(content.texturesByteLength).toBe(0); + expect(content.batchTableByteLength).toBe(0); + expect(content.innerContents).toBeUndefined(); + expect(content.readyPromise).toBeDefined(); + expect(content.tileset).toBe(tileset); + expect(content.tile).toBe(tile); + expect(content.url).toBeDefined(); + expect(content.batchTable).toBeUndefined(); + expect(content.hasProperty(0, 'name')).toBe(false); + expect(content.getFeature(0)).toBeUndefined(); + }); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/WebMapServiceImageryProviderSpec.js b/Specs/Scene/WebMapServiceImageryProviderSpec.js index e838d04971d8..3f729fd64690 100644 --- a/Specs/Scene/WebMapServiceImageryProviderSpec.js +++ b/Specs/Scene/WebMapServiceImageryProviderSpec.js @@ -139,9 +139,10 @@ defineSuite([ deferred.resolve(true); }); - provider.requestImage(0, 0, 0); + return provider.requestImage(0, 0, 0).then(function(image) { + expect(loadImage.createImage).toHaveBeenCalled(); + }); - expect(loadImage.createImage).toHaveBeenCalled(); }); }); @@ -188,9 +189,9 @@ defineSuite([ deferred.resolve(true); }); - provider.requestImage(0, 0, 0); - - expect(loadImage.createImage).toHaveBeenCalled(); + return provider.requestImage(0, 0, 0).then(function(image) { + expect(loadImage.createImage).toHaveBeenCalled(); + }); }); }); @@ -206,16 +207,15 @@ defineSuite([ spyOn(loadImage, 'createImage').and.callFake(function(url, crossOrigin, deferred) { var questionMarkCount = url.match(/\?/g).length; expect(questionMarkCount).toEqual(1); - expect(url).not.toContain('&&'); // Don't need to actually load image, but satisfy the request. deferred.resolve(true); }); - provider.requestImage(0, 0, 0); - - expect(loadImage.createImage).toHaveBeenCalled(); + return provider.requestImage(0, 0, 0).then(function(image) { + expect(loadImage.createImage).toHaveBeenCalled(); + }); }); }); @@ -572,11 +572,14 @@ defineSuite([ expect(params.format).toEqual('foo'); expect(params.format).not.toEqual('image/jpeg'); - }); - provider.requestImage(0, 0, 0); + // Just return any old image. + loadImage.defaultCreateImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); - expect(loadImage.createImage).toHaveBeenCalled(); + return provider.requestImage(0, 0, 0).then(function(image) { + expect(loadImage.createImage).toHaveBeenCalled(); + }); }); }); diff --git a/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorSpec.js b/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorSpec.js new file mode 100644 index 000000000000..bb9a7ecc70b1 --- /dev/null +++ b/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorSpec.js @@ -0,0 +1,95 @@ +/*global defineSuite*/ +defineSuite([ + 'Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector', + 'Core/Ellipsoid', + 'Scene/Cesium3DTileset', + 'Scene/Globe', + 'Specs/createScene' + ], function( + Cesium3DTilesInspector, + Ellipsoid, + Cesium3DTileset, + Globe, + createScene) { + 'use strict'; + + // Parent tile with content and four child tiles with content + var tilesetUrl = './Data/Cesium3DTiles/Tilesets/Tileset/'; + + var scene; + beforeAll(function() { + scene = createScene(); + var ellipsoid = Ellipsoid.UNIT_SPHERE; + scene.globe = new Globe(ellipsoid); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + it('can create and destroy', function() { + var container = document.createElement('div'); + container.id = 'testContainer'; + document.body.appendChild(container); + + var widget = new Cesium3DTilesInspector('testContainer', scene); + expect(widget.container).toBe(container); + expect(widget.viewModel._scene).toBe(scene); + expect(widget.isDestroyed()).toEqual(false); + widget.destroy(); + expect(widget.isDestroyed()).toEqual(true); + + document.body.removeChild(container); + }); + + it('constructor throws with no element', function() { + expect(function() { + return new Cesium3DTilesInspector(); + }).toThrowDeveloperError(); + }); + + it('constructor throws with string element that does not exist', function() { + expect(function() { + return new Cesium3DTilesInspector('does not exist', scene); + }).toThrowDeveloperError(); + }); + + it('constructor throws with no scene', function() { + expect(function() { + return new Cesium3DTilesInspector(document.body); + }).toThrowDeveloperError(); + }); + + describe('logging', function() { + var widget; + var container; + + beforeAll(function() { + container = document.createElement('div'); + container.id = 'testContainer'; + document.body.appendChild(container); + widget = new Cesium3DTilesInspector('testContainer', scene); + + var viewModel = widget.viewModel; + viewModel.tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + return viewModel.tileset.readyPromise; + }); + + afterAll(function() { + widget.destroy(); + document.body.removeChild(container); + }); + + it('shows performance', function() { + var viewModel = widget.viewModel; + viewModel.performance = true; + expect(viewModel._performanceDisplay._container.className.indexOf('cesium-cesiumInspector-show') !== -1).toBe(true); + expect(viewModel._performanceDisplay._container.className.indexOf('cesium-cesiumInspector-hide') === -1).toBe(true); + viewModel.performance = false; + expect(viewModel._performanceDisplay._container.className.indexOf('cesium-cesiumInspector-show') === -1).toBe(true); + expect(viewModel._performanceDisplay._container.className.indexOf('cesium-cesiumInspector-hide') !== -1).toBe(true); + }); + }); +}, 'WebGL'); diff --git a/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js b/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js new file mode 100644 index 000000000000..53f77c679ed0 --- /dev/null +++ b/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js @@ -0,0 +1,265 @@ +/*global defineSuite*/ +defineSuite([ + 'Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel', + 'Scene/Cesium3DTileset', + 'Scene/Cesium3DTileStyle', + 'Core/defined', + 'Core/Math', + 'Scene/Globe', + 'Specs/createScene', + 'ThirdParty/when' + ], function( + Cesium3DTilesInspectorViewModel, + Cesium3DTileset, + Cesium3DTileStyle, + defined, + CesiumMath, + Globe, + createScene, + when) { + 'use strict'; + + // Parent tile with content and four child tiles with content + var tilesetUrl = './Data/Cesium3DTiles/Tilesets/Tileset/'; + + var scene; + var viewModel; + var performanceContainer = document.createElement('div'); + + beforeAll(function() { + scene = createScene(); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + beforeEach(function() { + scene.globe = new Globe(); + scene.initializeFrame(); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + it('can create and destroy', function() { + var viewModel = new Cesium3DTilesInspectorViewModel(scene, performanceContainer); + expect(viewModel._scene).toBe(scene); + expect(viewModel.isDestroyed()).toEqual(false); + viewModel.destroy(); + expect(viewModel.isDestroyed()).toEqual(true); + }); + + it('throws if scene is undefined', function() { + expect(function() { + return new Cesium3DTilesInspectorViewModel(); + }).toThrowDeveloperError(); + }); + + it('throws if performanceContainer is undefined', function() { + expect(function() { + return new Cesium3DTilesInspectorViewModel(scene); + }).toThrowDeveloperError(); + }); + + describe('tileset options', function() { + it('show properties', function() { + viewModel = new Cesium3DTilesInspectorViewModel(scene, performanceContainer); + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + viewModel.tileset = tileset; + var done = when.defer(); + tileset.readyPromise.then(function() { + expect(viewModel.properties.indexOf('id') !== -1).toBe(true); + expect(viewModel.properties.indexOf('Longitude') !== -1).toBe(true); + expect(viewModel.properties.indexOf('Latitude') !== -1).toBe(true); + expect(viewModel.properties.indexOf('Height') !== -1).toBe(true); + viewModel.destroy(); + done.resolve(); + }); + return done; + }); + }); + + describe('display options', function() { + beforeAll(function() { + viewModel = new Cesium3DTilesInspectorViewModel(scene, performanceContainer); + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + viewModel.tileset = tileset; + return tileset.readyPromise; + }); + + afterAll(function() { + viewModel.destroy(); + }); + + it('colorize', function() { + viewModel.colorize = true; + expect(viewModel.tileset.debugColorizeTiles).toBe(true); + viewModel.colorize = false; + expect(viewModel.tileset.debugColorizeTiles).toBe(false); + }); + + it('wireframe', function() { + viewModel.wireframe = true; + expect(viewModel.tileset.debugWireframe).toBe(true); + viewModel.wireframe = false; + expect(viewModel.tileset.debugWireframe).toBe(false); + }); + + it('showBoundingVolumes', function() { + viewModel.showBoundingVolumes = true; + expect(viewModel.tileset.debugShowBoundingVolume).toBe(true); + viewModel.showBoundingVolumes = false; + expect(viewModel.tileset.debugShowBoundingVolume).toBe(false); + }); + + it('showContentVolumes', function() { + viewModel.showContentBoundingVolumes = true; + expect(viewModel.tileset.debugShowContentBoundingVolume).toBe(true); + viewModel.showContentBoundingVolumes = false; + expect(viewModel.tileset.debugShowContentBoundingVolume).toBe(false); + }); + + it('showRequestVolumes', function() { + viewModel.showRequestVolumes = true; + expect(viewModel.tileset.debugShowViewerRequestVolume).toBe(true); + viewModel.showRequestVolumes = false; + expect(viewModel.tileset.debugShowViewerRequestVolume).toBe(false); + }); + + it('showOnlyPickedTileDebugLabel', function() { + viewModel.showOnlyPickedTileDebugLabel = true; + expect(viewModel.tileset.debugPickedTileLabelOnly).toBe(true); + viewModel.showOnlyPickedTileDebugLabel = false; + expect(viewModel.tileset.debugPickedTileLabelOnly).toBe(false); + }); + + it('showGeometricError', function() { + viewModel.showGeometricError = true; + expect(viewModel.tileset.debugShowGeometricError).toBe(true); + viewModel.showGeometricError = false; + expect(viewModel.tileset.debugShowGeometricError).toBe(false); + }); + + it('showRenderingStatistics', function() { + viewModel.showRenderingStatistics = true; + expect(viewModel.tileset.debugShowRenderingStatistics).toBe(true); + viewModel.showRenderingStatistics = false; + expect(viewModel.tileset.debugShowRenderingStatistics).toBe(false); + }); + + it('showMemoryUsage', function() { + viewModel.showMemoryUsage = true; + expect(viewModel.tileset.debugShowMemoryUsage).toBe(true); + viewModel.showMemoryUsage = false; + expect(viewModel.tileset.debugShowMemoryUsage).toBe(false); + }); + }); + + describe('update options', function() { + beforeAll(function() { + viewModel = new Cesium3DTilesInspectorViewModel(scene, performanceContainer); + viewModel.tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + return viewModel.tileset.readyPromise; + }); + + afterAll(function() { + viewModel.destroy(); + }); + + it('freeze frame', function() { + viewModel.freezeFrame = false; + expect(viewModel.tileset.debugFreezeFrame).toBe(false); + viewModel.freezeFrame = true; + expect(viewModel.tileset.debugFreezeFrame).toBe(true); + }); + + it('maximum screen space error', function() { + viewModel.dynamicScreenSpaceError = false; + viewModel.maximumScreenSpaceError = 10; + expect(viewModel.tileset.dynamicScreenSpaceError).toBe(false); + expect(viewModel.tileset.maximumScreenSpaceError).toBe(10); + }); + + it('dynamic screen space error', function() { + viewModel.dynamicScreenSpaceError = true; + viewModel.dynamicScreenSpaceErrorFactor = 2; + viewModel.dynamicScreenSpaceErrorDensity = 0.1; + expect(viewModel.tileset.dynamicScreenSpaceError).toBe(true); + expect(viewModel.tileset.dynamicScreenSpaceErrorFactor).toBe(2); + expect(viewModel.tileset.dynamicScreenSpaceErrorDensity).toBe(0.1); + }); + }); + + describe('style options', function() { + var style; + + beforeAll(function() { + style = new Cesium3DTileStyle({ + color : { + conditions : [ + ["${Height} >= 83", "color('purple', 0.5)"], + ["${Height} >= 80", "color('red')"], + ["${Height} >= 70", "color('orange')"], + ["${Height} >= 12", "color('yellow')"], + ["${Height} >= 7", "color('lime')"], + ["${Height} >= 1", "color('cyan')"], + ["true", "color('blue')"] + ] + }, + meta : { + description : "'Building id ${id} has height ${Height}.'" + } + }); + + viewModel = new Cesium3DTilesInspectorViewModel(scene, performanceContainer); + viewModel.tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + + return viewModel.tileset.readyPromise; + }); + + afterAll(function() { + viewModel.destroy(); + }); + + it('loads tileset style', function() { + viewModel.tileset.style = style; + viewModel._update(); + expect(JSON.stringify(style.style)).toBe(JSON.stringify(JSON.parse(viewModel.styleString))); + }); + + it('does not throw on invalid syntax', function() { + expect(function() { + viewModel.styleString = 'invalid'; + }).not.toThrowError(); + }); + + it('recompiles style', function() { + viewModel._style = undefined; + viewModel.tileset.style = style; + viewModel._update(); + var s = JSON.parse(viewModel.styleString); + s.color = "color('red')"; + viewModel.styleString = JSON.stringify(s); + viewModel.compileStyle(); + viewModel._update(); + expect(viewModel.tileset.style.style.color).toBe("color('red')"); + expect(viewModel.tileset.style.style.meta.description).toBe("'Building id ${id} has height ${Height}.'"); + }); + + it('does not throw on invalid value', function() { + expect(function() { + viewModel.styleString = '{ "color": "color(1)" }'; + }).not.toThrowError(); + }); + }); +}, 'WebGL'); diff --git a/server.js b/server.js index 8ce9725e8996..0aee619e1c73 100644 --- a/server.js +++ b/server.js @@ -43,6 +43,7 @@ 'image/ktx' : ['ktx'], 'model/gltf+json' : ['gltf'], 'model/gltf-binary' : ['bgltf', 'glb'], + 'application/octet-stream' : ['b3dm', 'pnts', 'i3dm', 'cmpt'], 'text/plain' : ['glsl'] }); diff --git a/web.config b/web.config index 5adcfa772328..46951578bb01 100644 --- a/web.config +++ b/web.config @@ -6,6 +6,14 @@ + + + + + + + +