diff --git a/CHANGES.md b/CHANGES.md index f329d8c2b58a..521ddcc61e8f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,10 @@ - Added `options.fadingEnabled` parameter to `ShadowMap` to control whether shadows fade out when the light source is close to the horizon. [#9565](https://github.com/CesiumGS/cesium/pull/9565) +### 1.82.1 - 2021-06-01 + +- This is an npm only release to fix the improperly published 1.82.0. + ### 1.82 - 2021-06-01 ##### Additions :tada: @@ -26,7 +30,7 @@ ##### Deprecated :hourglass_flowing_sand: -- `loadCRN` and `loadKTX` have been deprecated and will be removed in CesiumJS 1.82. They will be replaced with support for KTX2. [#9478](https://github.com/CesiumGS/cesium/pull/9478) +- `loadCRN` and `loadKTX` have been deprecated and will be removed in CesiumJS 1.83. They will be replaced with support for KTX2. [#9478](https://github.com/CesiumGS/cesium/pull/9478) ### 1.80 - 2021-04-01 diff --git a/LICENSE.md b/LICENSE.md index ad2025faf76b..f12698a068b7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -817,6 +817,212 @@ https://github.com/dy/bitmap-sdf > > Development supported by plot.ly. +### s2geometry + +https://github.com/google/s2geometry/blob/master/LICENSE + +> Apache License +> Version 2.0, January 2004 +> http://www.apache.org/licenses/ +> +> TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +> +> 1. Definitions. +> +> "License" shall mean the terms and conditions for use, reproduction, +> and distribution as defined by Sections 1 through 9 of this document. +> +> "Licensor" shall mean the copyright owner or entity authorized by +> the copyright owner that is granting the License. +> +> "Legal Entity" shall mean the union of the acting entity and all +> other entities that control, are controlled by, or are under common +> control with that entity. For the purposes of this definition, +> "control" means (i) the power, direct or indirect, to cause the +> direction or management of such entity, whether by contract or +> otherwise, or (ii) ownership of fifty percent (50%) or more of the +> outstanding shares, or (iii) beneficial ownership of such entity. +> +> "You" (or "Your") shall mean an individual or Legal Entity +> exercising permissions granted by this License. +> +> "Source" form shall mean the preferred form for making modifications, +> including but not limited to software source code, documentation +> source, and configuration files. +> +> "Object" form shall mean any form resulting from mechanical +> transformation or translation of a Source form, including but +> not limited to compiled object code, generated documentation, +> and conversions to other media types. +> +> "Work" shall mean the work of authorship, whether in Source or +> Object form, made available under the License, as indicated by a +> copyright notice that is included in or attached to the work +> (an example is provided in the Appendix below). +> +> "Derivative Works" shall mean any work, whether in Source or Object +> form, that is based on (or derived from) the Work and for which the +> editorial revisions, annotations, elaborations, or other modifications +> represent, as a whole, an original work of authorship. For the purposes +> of this License, Derivative Works shall not include works that remain +> separable from, or merely link (or bind by name) to the interfaces of, +> the Work and Derivative Works thereof. +> +> "Contribution" shall mean any work of authorship, including +> the original version of the Work and any modifications or additions +> to that Work or Derivative Works thereof, that is intentionally +> submitted to Licensor for inclusion in the Work by the copyright owner +> or by an individual or Legal Entity authorized to submit on behalf of +> the copyright owner. For the purposes of this definition, "submitted" +> means any form of electronic, verbal, or written communication sent +> to the Licensor or its representatives, including but not limited to +> communication on electronic mailing lists, source code control systems, +> and issue tracking systems that are managed by, or on behalf of, the +> Licensor for the purpose of discussing and improving the Work, but +> excluding communication that is conspicuously marked or otherwise +> designated in writing by the copyright owner as "Not a Contribution." +> +> "Contributor" shall mean Licensor and any individual or Legal Entity +> on behalf of whom a Contribution has been received by Licensor and +> subsequently incorporated within the Work. +> +> 2. Grant of Copyright License. Subject to the terms and conditions of +> this License, each Contributor hereby grants to You a perpetual, +> worldwide, non-exclusive, no-charge, royalty-free, irrevocable +> copyright license to reproduce, prepare Derivative Works of, +> publicly display, publicly perform, sublicense, and distribute the +> Work and such Derivative Works in Source or Object form. +> +> 3. Grant of Patent License. Subject to the terms and conditions of +> this License, each Contributor hereby grants to You a perpetual, +> worldwide, non-exclusive, no-charge, royalty-free, irrevocable +> (except as stated in this section) patent license to make, have made, +> use, offer to sell, sell, import, and otherwise transfer the Work, +> where such license applies only to those patent claims licensable +> by such Contributor that are necessarily infringed by their +> Contribution(s) alone or by combination of their Contribution(s) +> with the Work to which such Contribution(s) was submitted. If You +> institute patent litigation against any entity (including a +> cross-claim or counterclaim in a lawsuit) alleging that the Work +> or a Contribution incorporated within the Work constitutes direct +> or contributory patent infringement, then any patent licenses +> granted to You under this License for that Work shall terminate +> as of the date such litigation is filed. +> +> 4. Redistribution. You may reproduce and distribute copies of the +> Work or Derivative Works thereof in any medium, with or without +> modifications, and in Source or Object form, provided that You +> meet the following conditions: +> +> (a) You must give any other recipients of the Work or +> Derivative Works a copy of this License; and +> +> (b) You must cause any modified files to carry prominent notices +> stating that You changed the files; and +> +> (c) You must retain, in the Source form of any Derivative Works +> that You distribute, all copyright, patent, trademark, and +> attribution notices from the Source form of the Work, +> excluding those notices that do not pertain to any part of +> the Derivative Works; and +> +> (d) If the Work includes a "NOTICE" text file as part of its +> distribution, then any Derivative Works that You distribute must +> include a readable copy of the attribution notices contained +> within such NOTICE file, excluding those notices that do not +> pertain to any part of the Derivative Works, in at least one +> of the following places: within a NOTICE text file distributed +> as part of the Derivative Works; within the Source form or +> documentation, if provided along with the Derivative Works; or, +> within a display generated by the Derivative Works, if and +> wherever such third-party notices normally appear. The contents +> of the NOTICE file are for informational purposes only and +> do not modify the License. You may add Your own attribution +> notices within Derivative Works that You distribute, alongside +> or as an addendum to the NOTICE text from the Work, provided +> that such additional attribution notices cannot be construed +> as modifying the License. +> +> You may add Your own copyright statement to Your modifications and +> may provide additional or different license terms and conditions +> for use, reproduction, or distribution of Your modifications, or +> for any such Derivative Works as a whole, provided Your use, +> reproduction, and distribution of the Work otherwise complies with +> the conditions stated in this License. +> +> 5. Submission of Contributions. Unless You explicitly state otherwise, +> any Contribution intentionally submitted for inclusion in the Work +> by You to the Licensor shall be under the terms and conditions of +> this License, without any additional terms or conditions. +> Notwithstanding the above, nothing herein shall supersede or modify +> the terms of any separate license agreement you may have executed +> with Licensor regarding such Contributions. +> +> 6. Trademarks. This License does not grant permission to use the trade +> names, trademarks, service marks, or product names of the Licensor, +> except as required for reasonable and customary use in describing the +> origin of the Work and reproducing the content of the NOTICE file. +> +> 7. Disclaimer of Warranty. Unless required by applicable law or +> agreed to in writing, Licensor provides the Work (and each +> Contributor provides its Contributions) on an "AS IS" BASIS, +> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +> implied, including, without limitation, any warranties or conditions +> of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +> PARTICULAR PURPOSE. You are solely responsible for determining the +> appropriateness of using or redistributing the Work and assume any +> risks associated with Your exercise of permissions under this License. +> +> 8. Limitation of Liability. In no event and under no legal theory, +> whether in tort (including negligence), contract, or otherwise, +> unless required by applicable law (such as deliberate and grossly +> negligent acts) or agreed to in writing, shall any Contributor be +> liable to You for damages, including any direct, indirect, special, +> incidental, or consequential damages of any character arising as a +> result of this License or out of the use or inability to use the +> Work (including but not limited to damages for loss of goodwill, +> work stoppage, computer failure or malfunction, or any and all +> other commercial damages or losses), even if such Contributor +> has been advised of the possibility of such damages. +> +> 9. Accepting Warranty or Additional Liability. While redistributing +> the Work or Derivative Works thereof, You may choose to offer, +> and charge a fee for, acceptance of support, warranty, indemnity, +> or other liability obligations and/or rights consistent with this +> License. However, in accepting such obligations, You may act only +> on Your own behalf and on Your sole responsibility, not on behalf +> of any other Contributor, and only if You agree to indemnify, +> defend, and hold each Contributor harmless for any liability +> incurred by, or claims asserted against, such Contributor by reason +> of your accepting any such warranty or additional liability. +> +> END OF TERMS AND CONDITIONS +> +> APPENDIX: How to apply the Apache License to your work. +> +> To apply the Apache License to your work, attach the following +> boilerplate notice, with the fields enclosed by brackets "[]" +> replaced with your own identifying information. (Don't include +> the brackets!) The text should be enclosed in the appropriate +> comment syntax for the file format. We also recommend that a +> file or class name and description of purpose be included on the +> same "printed page" as the copyright notice for easier +> identification within third-party archives. +> +> Copyright [yyyy] [name of copyright owner] +> +> Licensed under the Apache License, Version 2.0 (the "License"); +> you may not use this file except in compliance with the License. +> You may obtain a copy of the License at +> +> http://www.apache.org/licenses/LICENSE-2.0 +> +> Unless required by applicable law or agreed to in writing, software +> distributed under the License is distributed on an "AS IS" BASIS, +> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +> See the License for the specific language governing permissions and +> limitations under the License. + # Tests The CesiumJS tests use the following third-party libraries and data. diff --git a/Source/Core/Check.js b/Source/Core/Check.js index 3308948bb253..4c3060036b96 100644 --- a/Source/Core/Check.js +++ b/Source/Core/Check.js @@ -204,6 +204,21 @@ Check.typeOf.bool = function (name, test) { } }; +/** + * Throws if test is not typeof 'bigint' + * + * @param {String} name The name of the variable being tested + * @param {*} test The value to test + * @exception {DeveloperError} test must be typeof 'bigint' + */ +Check.typeOf.bigint = function (name, test) { + if (typeof test !== "bigint") { + throw new DeveloperError( + getFailedTypeErrorMessage(typeof test, "bigint", name) + ); + } +}; + /** * Throws if test1 and test2 is not typeof 'number' and not equal in value * diff --git a/Source/Core/HilbertOrder.js b/Source/Core/HilbertOrder.js new file mode 100644 index 000000000000..45a652939ee4 --- /dev/null +++ b/Source/Core/HilbertOrder.js @@ -0,0 +1,112 @@ +import Check from "./Check.js"; +import DeveloperError from "./DeveloperError.js"; + +/** + * Hilbert Order helper functions. + * + * @namespace HilbertOrder + */ +var HilbertOrder = {}; + +/** + * Computes the Hilbert index at the given level from 2D coordinates. + * + * @param {Number} level The level of the curve + * @param {Number} x The X coordinate + * @param {Number} y The Y coordinate + * @returns {Number} The Hilbert index. + * @private + */ +HilbertOrder.encode2D = function (level, x, y) { + var n = Math.pow(2, level); + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number("level", level); + Check.typeOf.number("x", x); + Check.typeOf.number("y", y); + if (level < 1) { + throw new DeveloperError("Hilbert level cannot be less than 1."); + } + if (x < 0 || x >= n || y < 0 || y >= n) { + throw new DeveloperError("Invalid coordinates for given level."); + } + //>>includeEnd('debug'); + + var p = { + x: x, + y: y, + }; + var rx, + ry, + s, + index = 0; + + for (s = n / 2; s > 0; s /= 2) { + rx = (p.x & s) > 0 ? 1 : 0; + ry = (p.y & s) > 0 ? 1 : 0; + index += ((3 * rx) ^ ry) * s * s; + rotate(n, p, rx, ry); + } + + return index; +}; + +/** + * Computes the 2D coordinates from the Hilbert index at the given level. + * + * @param {Number} level The level of the curve + * @param {Number} index The Hilbert index + * @returns {Number[]} An array containing the 2D coordinates ([x, y]) corresponding to the Morton index. + * @private + */ +HilbertOrder.decode2D = function (level, index) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number("level", level); + Check.typeOf.number("index", index); + if (level < 1) { + throw new DeveloperError("Hilbert level cannot be less than 1."); + } + if (index < 0 || index >= Math.pow(4, level)) { + throw new DeveloperError( + "Hilbert index exceeds valid maximum for given level." + ); + } + //>>includeEnd('debug'); + + var n = Math.pow(2, level); + var p = { + x: 0, + y: 0, + }; + var rx, ry, s, t; + + for (s = 1, t = index; s < n; s *= 2) { + rx = 1 & (t / 2); + ry = 1 & (t ^ rx); + rotate(s, p, rx, ry); + p.x += s * rx; + p.y += s * ry; + t /= 4; + } + + return [p.x, p.y]; +}; + +/** + * @private + */ +function rotate(n, p, rx, ry) { + if (ry !== 0) { + return; + } + + if (rx === 1) { + p.x = n - 1 - p.x; + p.y = n - 1 - p.y; + } + + var t = p.x; + p.x = p.y; + p.y = t; +} + +export default HilbertOrder; diff --git a/Source/Core/Ion.js b/Source/Core/Ion.js index 782b811a04af..77aa860b6a55 100644 --- a/Source/Core/Ion.js +++ b/Source/Core/Ion.js @@ -4,7 +4,7 @@ import Resource from "./Resource.js"; var defaultTokenCredit; var defaultAccessToken = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2N2NjZjU1MS02ZDE5LTRjYzAtYjg0Zi0wODc2YjMxNGE1ZWEiLCJpZCI6MjU5LCJpYXQiOjE2MjAwNDczMDl9.159vCWIGDh0dexF393wvC4zUCbq4oZg6VkPAB5U6tg0"; + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMTQ1MTJkOC1kODA1LTQ3ZjMtYjFiMS1lNDljNGM3NDEzMTkiLCJpZCI6MjU5LCJpYXQiOjE2MjI1NjA5OTF9.DSp0vQUYQfm1d9ffL0PjA1WgnGTnmNdh3-JEl1Aiouw"; /** * Default settings for accessing the Cesium ion API. * diff --git a/Source/Core/S2Cell.js b/Source/Core/S2Cell.js new file mode 100644 index 000000000000..ab1a95434d24 --- /dev/null +++ b/Source/Core/S2Cell.js @@ -0,0 +1,764 @@ +/* eslint-disable new-cap */ +import Cartesian3 from "./Cartesian3.js"; +import Cartographic from "./Cartographic.js"; +import Check from "./Check.js"; +import defaultValue from "./defaultValue.js"; +import defined from "./defined.js"; +import DeveloperError from "./DeveloperError.js"; +import Ellipsoid from "./Ellipsoid.js"; +import FeatureDetection from "./FeatureDetection.js"; +import RuntimeError from "./RuntimeError.js"; + +/** + * S2 + * -- + * + * This implementation is based on the S2 C++ reference implementation: https://github.com/google/s2geometry + * + * + * Overview: + * --------- + * The S2 library decomposes the unit sphere into a hierarchy of cells. A cell is a quadrilateral bounded by 4 geodesics. + * The 6 root cells are obtained by projecting the six faces of a cube on a unit sphere. Each root cell follows a quadtree + * subdivision scheme, i.e. each cell subdivides into 4 smaller cells that cover the same area as the parent cell. The S2 cell + * hierarchy extends from level 0 (root cells) to level 30 (leaf cells). The root cells are rotated to enable a continuous Hilbert + * curve to map all 6 faces of the cube. + * + * + * Cell ID: + * -------- + * Each cell in S2 can be uniquely identified using a 64-bit unsigned integer, its cell ID. The first 3 bits of the cell ID are the face bits, i.e. + * they indicate which of the 6 faces of the cube a cell lies on. After the face bits are the position bits, i.e. they indicate the position + * of the cell along the Hilbert curve. After the positions bits is the sentinel bit, which is always set to 1, and it indicates the level of the + * cell. Again, the level can be between 0 and 30 in S2. + * + * Note: In the illustration below, the face bits are marked with 'f', the position bits are marked with 'p', the zero bits are marked with '-'. + * + * Cell ID (base 10): 3170534137668829184 + * Cell ID (base 2) : 0010110000000000000000000000000000000000000000000000000000000000 + * + * 001 0110000000000000000000000000000000000000000000000000000000000 + * fff pps---------------------------------------------------------- + * + * For the cell above, we can see that it lies on face 1 (01), with a Hilbert index of 1 (1). + * + * + * Cell Subdivision: + * ------------------ + * Cells in S2 subdivide recursively using quadtree subdivision. For each cell, you can get a child of index [0-3]. To compute the child at index i, + * insert the base 2 representation of i to the right of the parent's position bits. Ensure that the sentinel bit is also shifted two places to the right. + * + * Parent Cell ID (base 10) : 3170534137668829184 + * Parent Cell ID (base 2) : 0010110000000000000000000000000000000000000000000000000000000000 + * + * 001 0110000000000000000000000000000000000000000000000000000000000 + * fff pps---------------------------------------------------------- + * + * To get the 3rd child of the cell above, we insert the binary representation of 3 to the right of the parent's position bits: + * + * Note: In the illustration below, the bits to be added are highlighted with '^'. + * + * 001 0111100000000000000000000000000000000000000000000000000000000 + * fff pppps-------------------------------------------------------- + * ^^ + * + * Child(3) Cell ID (base 10) : 3386706919782612992 + * Child(3) Cell ID (base 2) : 0010111100000000000000000000000000000000000000000000000000000000 + * + * Cell Token: + * ----------- + * To provide a more concise representation of the S2 cell ID, we can use their hexadecimal representation. + * + * Cell ID (base 10): 3170534137668829184 + * Cell ID (base 2) : 0010110000000000000000000000000000000000000000000000000000000000 + * + * We remove all trailing zero bits, until we reach the nybble (4 bits) that contains the sentinel bit. + * + * Note: In the illustration below, the bits to be removed are highlighted with 'X'. + * + * 0010110000000000000000000000000000000000000000000000000000000000 + * fffpps--XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + * + * We convert the remaining bits to their hexadecimal representation. + * + * Base 2: 0010 1100 + * Base 16: "2" "c" + * + * Cell Token: "2c" + * + * To compute the cell ID from the token, we simply add enough zeros to the right to make the ID span 64 bits. + * + * Coordinate Transforms: + * ---------------------- + * + * To go from a cell in S2 to a point on the ellipsoid, the following order of transforms is applied: + * + * 1. (Cell ID): S2 cell ID + * 2. (Face, I, J): Leaf cell coordinates, where i and j are in range [0, 2^30 - 1] + * 3. (Face, S, T): Cell space coordinates, where s and t are in range [0, 1]. + * 4. (Face, Si, Ti): Discrete cell space coordinates, where si and ti are in range [0, 2^31] + * 5. (Face, U, V): Cube space coordinates, where u and v are in range [-1, 1]. We apply the non-linear quadratic transform here. + * 6. (X, Y, Z): Direction vector, where vector may not be unit length. Can be normalized to obtain point on unit sphere + * 7. (Latitude, Longitude): Direction vector, where latitude is in range [-90, 90] and longitude is in range [-180, 180] + * + * @ignore + */ + +// The maximum level supported within an S2 cell ID. Each level is represented by two bits in the final cell ID +var S2_MAX_LEVEL = 30; + +// The maximum index of a valid leaf cell plus one. The range of valid leaf cell indices is [0..S2_LIMIT_IJ-1]. +var S2_LIMIT_IJ = 1 << S2_MAX_LEVEL; + +// The maximum value of an si- or ti-coordinate. The range of valid (si,ti) values is [0..S2_MAX_SITI]. Use `>>>` to convert to unsigned. +var S2_MAX_SITI = (1 << (S2_MAX_LEVEL + 1)) >>> 0; + +// The number of bits in a S2 cell ID used for specifying the position along the Hilbert curve +var S2_POSITION_BITS = 2 * S2_MAX_LEVEL + 1; + +// The number of bits per I and J in the lookup tables +var S2_LOOKUP_BITS = 4; + +// Lookup table for mapping 10 bits of IJ + orientation to 10 bits of Hilbert curve position + orientation. +var S2_LOOKUP_POSITIONS = []; + +// Lookup table for mapping 10 bits of IJ + orientation to 10 bits of Hilbert curve position + orientation. +var S2_LOOKUP_IJ = []; + +// Lookup table of two bits of IJ from two bits of curve position, based also on the current curve orientation from the swap and invert bits +var S2_POSITION_TO_IJ = [ + [0, 1, 3, 2], // 0: Normal order, no swap or invert + [0, 2, 3, 1], // 1: Swap bit set, swap I and J bits + [3, 2, 0, 1], // 2: Invert bit set, invert bits + [3, 1, 0, 2], // 3: Swap and invert bits set +]; + +// Mask that specifies the swap orientation bit for the Hilbert curve +var S2_SWAP_MASK = 1; + +// Mask that specifies the invert orientation bit for the Hilbert curve +var S2_INVERT_MASK = 2; + +// Lookup for the orientation update mask of one of the four sub-cells within a higher level cell. +// This mask is XOR'ed with the current orientation to get the sub-cell orientation. +var S2_POSITION_TO_ORIENTATION_MASK = [ + S2_SWAP_MASK, + 0, + 0, + S2_SWAP_MASK | S2_INVERT_MASK, +]; + +/** + * Represents a cell in the S2 geometry library. + * + * @alias S2Cell + * @constructor + * + * @param {BigInt} [cellId] The 64-bit S2CellId. + * @private + */ +function S2Cell(cellId) { + if (!FeatureDetection.supportsBigInt()) { + throw new RuntimeError("S2 required BigInt support"); + } + //>>includeStart('debug', pragmas.debug); + if (!defined(cellId)) { + throw new DeveloperError("cell ID is required."); + } + if (!S2Cell.isValidId(cellId)) { + throw new DeveloperError("cell ID is invalid."); + } + //>>includeEnd('debug'); + + this._cellId = cellId; + this._level = S2Cell.getLevel(cellId); +} + +/** + * Creates a new S2Cell from a token. A token is a hexadecimal representation of the 64-bit S2CellId. + * + * @param {String} token The token for the S2 Cell. + * @returns {S2Cell} Returns a new S2Cell. + * @private + */ +S2Cell.fromToken = function (token) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string("token", token); + if (!S2Cell.isValidToken(token)) { + throw new DeveloperError("token is invalid."); + } + //>>includeEnd('debug'); + + return new S2Cell(S2Cell.getIdFromToken(token)); +}; + +/** + * Validates an S2 cell ID. + * + * @param {BigInt} [cellId] The S2CellId. + * @returns {Boolean} Returns true if the cell ID is valid, returns false otherwise. + * @private + */ +S2Cell.isValidId = function (cellId) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bigint("cellId", cellId); + //>>includeEnd('debug'); + + // Check if sentinel bit is missing. + if (cellId <= 0) { + return false; + } + + // Check if face bits indicate a valid value, in range [0-5]. + // eslint-disable-next-line + if (cellId >> BigInt(S2_POSITION_BITS) > 5) { + return false; + } + + // Check trailing 1 bit is in one of the even bit positions allowed for the 30 levels, using a bitmask. + var lowestSetBit = cellId & (~cellId + BigInt(1)); // eslint-disable-line + // eslint-disable-next-line + if (!(lowestSetBit & BigInt("0x1555555555555555"))) { + return false; + } + + return true; +}; + +/** + * Validates an S2 cell token. + * + * @param {String} [token] The hexadecimal representation of an S2CellId. + * @returns {Boolean} Returns true if the token is valid, returns false otherwise. + * @private + */ +S2Cell.isValidToken = function (token) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string("token", token); + //>>includeEnd('debug'); + + if (!/^[0-9a-fA-F]{1,16}$/.test(token)) { + return false; + } + + return S2Cell.isValidId(S2Cell.getIdFromToken(token)); +}; + +/** + * Converts an S2 cell token to a 64-bit S2 cell ID. + * + * @param {String} [token] The hexadecimal representation of an S2CellId. Expected to be a valid S2 token. + * @returns {BigInt} Returns the S2 cell ID. + * @private + */ +S2Cell.getIdFromToken = function (token) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string("token", token); + //>>includeEnd('debug'); + + return BigInt("0x" + token + "0".repeat(16 - token.length)); // eslint-disable-line +}; + +/** + * Converts a 64-bit S2 cell ID to an S2 cell token. + * + * @param {BigInt} [cellId] The S2 cell ID. + * @returns {BigInt} Returns hexadecimal representation of an S2CellId. + * @private + */ +S2Cell.getTokenFromId = function (cellId) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bigint("cellId", cellId); + //>>includeEnd('debug'); + + var trailingZeroHexChars = Math.floor(countTrailingZeroBits(cellId) / 4); + var hexString = cellId.toString(16).replace(/0*$/, ""); + + var zeroString = Array(17 - trailingZeroHexChars - hexString.length).join( + "0" + ); + return zeroString + hexString; +}; + +/** + * Gets the level of the cell from the cell ID. + * + * @param {BigInt} [cellId] The S2 cell ID. + * @returns {number} Returns the level of the cell. + * @private + */ +S2Cell.getLevel = function (cellId) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bigint("cellId", cellId); + if (!S2Cell.isValidId(cellId)) { + throw new DeveloperError(); + } + //>>includeEnd('debug'); + + var lsbPosition = 0; + // eslint-disable-next-line + while (cellId !== BigInt(0)) { + // eslint-disable-next-line + if (cellId & BigInt(1)) { + break; + } + lsbPosition++; + cellId = cellId >> BigInt(1); // eslint-disable-line + } + + // We use (>> 1) because there are 2 bits per level. + return S2_MAX_LEVEL - (lsbPosition >> 1); +}; + +/** + * Gets the child cell of the cell at the given index. + * + * @param {Number} index An integer index of the child. + * @returns {S2Cell} The child of the S2Cell. + * @private + */ +S2Cell.prototype.getChild = function (index) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number("index", index); + if (index < 0 || index > 3) { + throw new DeveloperError("child index must be in the range [0-3]."); + } + if (this._level === 30) { + throw new DeveloperError("cannot get child of leaf cell."); + } + //>>includeEnd('debug'); + + // Shift sentinel bit 2 positions to the right. + var newLsb = lsb(this._cellId) >> BigInt(2); // eslint-disable-line + // Insert child index before the sentinel bit. + var childCellId = this._cellId + BigInt(2 * index + 1 - 4) * newLsb; // eslint-disable-line + return new S2Cell(childCellId); +}; + +/** + * Gets the parent cell of an S2Cell. + * + * @returns {S2Cell} Returns the parent of the S2Cell. + * @private + */ +S2Cell.prototype.getParent = function () { + //>>includeStart('debug', pragmas.debug); + if (this._level === 0) { + throw new DeveloperError("cannot get parent of root cell."); + } + //>>includeEnd('debug'); + // Shift the sentinel bit 2 positions to the left. + var newLsb = lsb(this._cellId) << BigInt(2); // eslint-disable-line + // Erase the left over bits to the right of the sentinel bit. + return new S2Cell((this._cellId & (~newLsb + BigInt(1))) | newLsb); // eslint-disable-line +}; + +/** + * Gets the parent cell at the given level. + * + * @returns {S2Cell} Returns the parent of the S2Cell. + * @private + */ +S2Cell.prototype.getParentAtLevel = function (level) { + //>>includeStart('debug', pragmas.debug); + if (this._level === 0 || level < 0 || this._level < level) { + throw new DeveloperError("cannot get parent at invalid level."); + } + //>>includeEnd('debug'); + var newLsb = lsbForLevel(level); + return new S2Cell((this._cellId & -newLsb) | newLsb); +}; + +/** + * Get center of the S2 cell. + * + * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid. + * @returns {Cartesian} The position of center of the S2 cell. + * @private + */ +S2Cell.prototype.getCenter = function (ellipsoid) { + ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); + + var center = getS2Center(this._cellId); + // Normalize XYZ. + center = Cartesian3.normalize(center, center); + var cartographic = new Cartographic.fromCartesian( + center, + Ellipsoid.UNIT_SPHERE + ); + // Interpret as geodetic coordinates on the ellipsoid. + return Cartographic.toCartesian(cartographic, ellipsoid, new Cartesian3()); +}; + +/** + * Get vertex of the S2 cell. Vertices are indexed in CCW order. + * + * @param {Number} index An integer index of the vertex. Must be in the range [0-3]. + * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid. + * @returns {Cartesian} The position of the vertex of the S2 cell. + * @private + */ +S2Cell.prototype.getVertex = function (index, ellipsoid) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number("index", index); + if (index < 0 || index > 3) { + throw new DeveloperError("vertex index must be in the range [0-3]."); + } + //>>includeEnd('debug'); + + ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); + + var vertex = getS2Vertex(this._cellId, index); + // Normalize XYZ. + vertex = Cartesian3.normalize(vertex, vertex); + var cartographic = new Cartographic.fromCartesian( + vertex, + Ellipsoid.UNIT_SPHERE + ); + // Interpret as geodetic coordinates on the ellipsoid. + return Cartographic.toCartesian(cartographic, ellipsoid, new Cartesian3()); +}; + +/** + * @private + */ +function getS2Center(cellId) { + var faceSiTi = convertCellIdToFaceSiTi(cellId); + return convertFaceSiTitoXYZ(faceSiTi[0], faceSiTi[1], faceSiTi[2]); +} +/** + * @private + */ +function getS2Vertex(cellId, index) { + var faceIJ = convertCellIdToFaceIJ(cellId); + var uv = convertIJLeveltoBoundUV( + [faceIJ[1], faceIJ[2]], + S2Cell.getLevel(cellId) + ); + // Handles CCW ordering of the vertices. + var y = (index >> 1) & 1; + return convertFaceUVtoXYZ(faceIJ[0], uv[0][y ^ (index & 1)], uv[1][y]); +} + +// S2 Coordinate Conversions + +/** + * @private + */ +function convertCellIdToFaceSiTi(cellId) { + var faceIJ = convertCellIdToFaceIJ(cellId); + var face = faceIJ[0]; + var i = faceIJ[1]; + var j = faceIJ[2]; + + // We're resolving the center when we do the coordinate transform here. For the leaf cells, we're adding half the cell size + // (remember that this space has 31 levels - which allows us to pick center and edges of the leaf cells). For non leaf cells, + // we get one of either two cells diagonal to the cell center. The correction is used to make sure we pick the leaf cell edges + // that represent the parent cell center. + var isLeaf = S2Cell.getLevel(cellId) === 30; + var shouldCorrect = + !isLeaf && (BigInt(i) ^ (cellId >> BigInt(2))) & BigInt(1); // eslint-disable-line + var correction = isLeaf ? 1 : shouldCorrect ? 2 : 0; + var si = (i << 1) + correction; + var ti = (j << 1) + correction; + return [face, si, ti]; +} + +/** + * @private + */ +function convertCellIdToFaceIJ(cellId) { + if (S2_LOOKUP_POSITIONS.length === 0) { + generateLookupTable(); + } + + var face = Number(cellId >> BigInt(S2_POSITION_BITS)); // eslint-disable-line + var bits = face & S2_SWAP_MASK; + var lookupMask = (1 << S2_LOOKUP_BITS) - 1; + + var i = 0; + var j = 0; + + for (var k = 7; k >= 0; k--) { + var numberOfBits = + k === 7 ? S2_MAX_LEVEL - 7 * S2_LOOKUP_BITS : S2_LOOKUP_BITS; + var extractMask = (1 << (2 * numberOfBits)) - 1; + bits += + Number( + (cellId >> BigInt(k * 2 * S2_LOOKUP_BITS + 1)) & BigInt(extractMask) // eslint-disable-line + ) << 2; + + bits = S2_LOOKUP_IJ[bits]; + + var offset = k * S2_LOOKUP_BITS; + i += (bits >> (S2_LOOKUP_BITS + 2)) << offset; + j += ((bits >> 2) & lookupMask) << offset; + + bits &= S2_SWAP_MASK | S2_INVERT_MASK; + } + + return [face, i, j]; +} + +/** + * @private + */ +function convertFaceSiTitoXYZ(face, si, ti) { + var s = convertSiTitoST(si); + var t = convertSiTitoST(ti); + + var u = convertSTtoUV(s); + var v = convertSTtoUV(t); + return convertFaceUVtoXYZ(face, u, v); +} + +/** + * @private + */ +function convertFaceUVtoXYZ(face, u, v) { + switch (face) { + case 0: + return new Cartesian3(1, u, v); + case 1: + return new Cartesian3(-u, 1, v); + case 2: + return new Cartesian3(-u, -v, 1); + case 3: + return new Cartesian3(-1, -v, -u); + case 4: + return new Cartesian3(v, -1, -u); + default: + return new Cartesian3(v, u, -1); + } +} + +/** + * S2 provides 3 methods for the non-linear transform: linear, quadratic and tangential. + * This implementation uses the quadratic method because it provides a good balance of + * accuracy and speed. + * + * For a more detailed comparison of these transform methods, see + * {@link https://github.com/google/s2geometry/blob/0c4c460bdfe696da303641771f9def900b3e440f/src/s2/s2metrics.cc} + * @private + */ +function convertSTtoUV(s) { + if (s >= 0.5) return (1 / 3) * (4 * s * s - 1); + return (1 / 3) * (1 - 4 * (1 - s) * (1 - s)); +} + +/** + * @private + */ +function convertSiTitoST(si) { + return (1.0 / S2_MAX_SITI) * si; +} + +/** + * @private + */ +function convertIJLeveltoBoundUV(ij, level) { + var result = [[], []]; + var cellSize = getSizeIJ(level); + for (var d = 0; d < 2; ++d) { + var ijLow = ij[d] & -cellSize; + var ijHigh = ijLow + cellSize; + result[d][0] = convertSTtoUV(convertIJtoSTMinimum(ijLow)); + result[d][1] = convertSTtoUV(convertIJtoSTMinimum(ijHigh)); + } + return result; +} + +/** + * @private + */ +function getSizeIJ(level) { + return (1 << (S2_MAX_LEVEL - level)) >>> 0; +} + +/** + * @private + */ +function convertIJtoSTMinimum(i) { + return (1.0 / S2_LIMIT_IJ) * i; +} + +// Utility Functions + +/** + * This function generates 4 variations of a Hilbert curve of level 4, based on the S2_POSITION_TO_IJ table, for fast lookups of (i, j) + * to position along Hilbert curve. The reference C++ implementation uses an iterative approach, however, this function is implemented + * recursively. + * + * See {@link https://github.com/google/s2geometry/blob/c59d0ca01ae3976db7f8abdc83fcc871a3a95186/src/s2/s2cell_id.cc#L75-L109} + * @private + */ +function generateLookupCell( + level, + i, + j, + originalOrientation, + position, + orientation +) { + if (level === S2_LOOKUP_BITS) { + var ij = (i << S2_LOOKUP_BITS) + j; + S2_LOOKUP_POSITIONS[(ij << 2) + originalOrientation] = + (position << 2) + orientation; + S2_LOOKUP_IJ[(position << 2) + originalOrientation] = + (ij << 2) + orientation; + } else { + level++; + i <<= 1; + j <<= 1; + position <<= 2; + var r = S2_POSITION_TO_IJ[orientation]; + generateLookupCell( + level, + i + (r[0] >> 1), + j + (r[0] & 1), + originalOrientation, + position, + orientation ^ S2_POSITION_TO_ORIENTATION_MASK[0] + ); + generateLookupCell( + level, + i + (r[1] >> 1), + j + (r[1] & 1), + originalOrientation, + position + 1, + orientation ^ S2_POSITION_TO_ORIENTATION_MASK[1] + ); + generateLookupCell( + level, + i + (r[2] >> 1), + j + (r[2] & 1), + originalOrientation, + position + 2, + orientation ^ S2_POSITION_TO_ORIENTATION_MASK[2] + ); + generateLookupCell( + level, + i + (r[3] >> 1), + j + (r[3] & 1), + originalOrientation, + position + 3, + orientation ^ S2_POSITION_TO_ORIENTATION_MASK[3] + ); + } +} + +/** + * @private + */ +function generateLookupTable() { + generateLookupCell(0, 0, 0, 0, 0, 0); + generateLookupCell(0, 0, 0, S2_SWAP_MASK, 0, S2_SWAP_MASK); + generateLookupCell(0, 0, 0, S2_INVERT_MASK, 0, S2_INVERT_MASK); + generateLookupCell( + 0, + 0, + 0, + S2_SWAP_MASK | S2_INVERT_MASK, + 0, + S2_SWAP_MASK | S2_INVERT_MASK + ); +} + +/** + * Return the lowest-numbered bit that is on for this cell id + * @private + */ +function lsb(cellId) { + return cellId & (~cellId + BigInt(1)); // eslint-disable-line +} + +/** + * Return the lowest-numbered bit that is on for cells at the given level. + * @private + */ +function lsbForLevel(level) { + return BigInt(1) << BigInt(2 * (S2_MAX_LEVEL - level)); // eslint-disable-line +} + +// Lookup table for getting trailing zero bits. +// https://graphics.stanford.edu/~seander/bithacks.html +var Mod67BitPosition = [ + 64, + 0, + 1, + 39, + 2, + 15, + 40, + 23, + 3, + 12, + 16, + 59, + 41, + 19, + 24, + 54, + 4, + 64, + 13, + 10, + 17, + 62, + 60, + 28, + 42, + 30, + 20, + 51, + 25, + 44, + 55, + 47, + 5, + 32, + 65, + 38, + 14, + 22, + 11, + 58, + 18, + 53, + 63, + 9, + 61, + 27, + 29, + 50, + 43, + 46, + 31, + 37, + 21, + 57, + 52, + 8, + 26, + 49, + 45, + 36, + 56, + 7, + 48, + 35, + 6, + 34, + 33, + 0, +]; + +/** + * Return the number of trailing zeros in number. + * @private + */ +function countTrailingZeroBits(x) { + return Mod67BitPosition[(-x & x) % BigInt(67)]; // eslint-disable-line +} + +export default S2Cell; diff --git a/Source/Core/loadCRN.js b/Source/Core/loadCRN.js index 777c2475413c..0bd835c33030 100644 --- a/Source/Core/loadCRN.js +++ b/Source/Core/loadCRN.js @@ -37,12 +37,12 @@ var transcodeTaskProcessor = new TaskProcessor("transcodeCRNToDXT"); * @see {@link https://github.com/BinomialLLC/crunch|crunch DXTc texture compression and transcoding library} * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} - * @deprecated This function has been deprecated and will be removed in CesiumJS 1.82. + * @deprecated This function has been deprecated and will be removed in CesiumJS 1.83. */ function loadCRN(resourceOrUrlOrBuffer) { deprecationWarning( "loadCRN", - "loadCRN is deprecated and will be removed in CesiumJS 1.82." + "loadCRN is deprecated and will be removed in CesiumJS 1.83." ); //>>includeStart('debug', pragmas.debug); if (!defined(resourceOrUrlOrBuffer)) { diff --git a/Source/Core/loadKTX.js b/Source/Core/loadKTX.js index 4013c776cbd0..e3905f01573f 100644 --- a/Source/Core/loadKTX.js +++ b/Source/Core/loadKTX.js @@ -58,12 +58,12 @@ import deprecationWarning from "./deprecationWarning.js"; * @see {@link https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/|KTX file format} * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} - * @deprecated This function has been deprecated and will be removed in CesiumJS 1.82. + * @deprecated This function has been deprecated and will be removed in CesiumJS 1.83. */ function loadKTX(resourceOrUrlOrBuffer) { deprecationWarning( "loadKTX", - "loadKTX is deprecated and will be removed in CesiumJS 1.82." + "loadKTX is deprecated and will be removed in CesiumJS 1.83." ); //>>includeStart('debug', pragmas.debug); Check.defined("resourceOrUrlOrBuffer", resourceOrUrlOrBuffer); diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 450c4e24dc21..b21e120e59e8 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -36,6 +36,7 @@ import Multiple3DTileContent from "./Multiple3DTileContent.js"; import preprocess3DTileContent from "./preprocess3DTileContent.js"; import SceneMode from "./SceneMode.js"; import TileBoundingRegion from "./TileBoundingRegion.js"; +import TileBoundingS2Cell from "./TileBoundingS2Cell.js"; import TileBoundingSphere from "./TileBoundingSphere.js"; import TileMetadata from "./TileMetadata.js"; import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; @@ -1644,6 +1645,12 @@ Cesium3DTile.prototype.createBoundingVolume = function ( if (!defined(boundingVolumeHeader)) { throw new RuntimeError("boundingVolume must be defined"); } + if (has3DTilesExtension(boundingVolumeHeader, "3DTILES_bounding_volume_S2")) { + return new TileBoundingS2Cell( + boundingVolumeHeader.extensions["3DTILES_bounding_volume_S2"] + ); + } + if (defined(boundingVolumeHeader.box)) { return createBox(boundingVolumeHeader.box, transform, result); } diff --git a/Source/Scene/Implicit3DTileContent.js b/Source/Scene/Implicit3DTileContent.js index b95b68c66b41..2d51003d9e31 100644 --- a/Source/Scene/Implicit3DTileContent.js +++ b/Source/Scene/Implicit3DTileContent.js @@ -5,11 +5,17 @@ import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import CesiumMath from "../Core/Math.js"; +import HilbertOrder from "../Core/HilbertOrder.js"; import Matrix3 from "../Core/Matrix3.js"; +import MortonOrder from "../Core/MortonOrder.js"; import Rectangle from "../Core/Rectangle.js"; +import S2Cell from "../Core/S2Cell.js"; import when from "../ThirdParty/when.js"; +import ImplicitSubdivisionScheme from "./ImplicitSubdivisionScheme.js"; import ImplicitSubtree from "./ImplicitSubtree.js"; import ImplicitTileMetadata from "./ImplicitTileMetadata.js"; +import has3DTilesExtension from "./has3DTilesExtension.js"; +import parseBoundingVolumeSemantics from "./parseBoundingVolumeSemantics.js"; /** * A specialized {@link Cesium3DTileContent} that lazily evaluates an implicit @@ -377,6 +383,24 @@ function deriveChildTile( ); } + // Parse metadata and bounding volume semantics at the beginning + // as the bounding volumes are needed below. + var tileMetadata; + var tileBounds; + var contentBounds; + if (defined(subtree.metadataExtension)) { + var metadataTable = subtree.metadataTable; + tileMetadata = new ImplicitTileMetadata({ + class: metadataTable.class, + implicitCoordinates: implicitCoordinates, + implicitSubtree: subtree, + }); + + var boundingVolumeSemantics = parseBoundingVolumeSemantics(tileMetadata); + tileBounds = boundingVolumeSemantics.tile; + contentBounds = boundingVolumeSemantics.content; + } + var contentJsons = []; for (var i = 0; i < implicitTileset.contentCount; i++) { if (!subtree.contentIsAvailableAtIndex(childBitIndex, i)) { @@ -389,15 +413,51 @@ function deriveChildTile( var contentJson = { uri: childContentUri, }; + + // content bounding volumes can only be specified via + // metadata semantics such as CONTENT_BOUNDING_BOX + if (defined(contentBounds) && defined(contentBounds.boundingVolume)) { + contentJson.boundingVolume = contentBounds.boundingVolume; + } + // combine() is used to pass through any additional properties the // user specified such as extras or extensions contentJsons.push(combine(contentJson, implicitTileset.contentHeaders[i])); } - var boundingVolume = deriveBoundingVolume( - implicitTileset, - implicitCoordinates - ); + var boundingVolume; + + if (defined(tileBounds) && defined(tileBounds.boundingVolume)) { + boundingVolume = tileBounds.boundingVolume; + } else { + boundingVolume = deriveBoundingVolume( + implicitTileset, + implicitCoordinates, + childIndex, + defaultValue(parentIsPlaceholderTile, false), + parentTile + ); + + // The TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT metadata semantics + // can be used to tighten the bounding volume + if ( + has3DTilesExtension(boundingVolume, "3DTILES_bounding_volume_S2") && + defined(tileBounds) + ) { + updateS2CellHeights( + boundingVolume.extensions["3DTILES_bounding_volume_S2"], + tileBounds.minimumHeight, + tileBounds.maximumHeight + ); + } else if (defined(boundingVolume.region) && defined(tileBounds)) { + updateRegionHeights( + boundingVolume.region, + tileBounds.minimumHeight, + tileBounds.maximumHeight + ); + } + } + var childGeometricError = implicitTileset.geometricError / Math.pow(2, implicitCoordinates.level); @@ -428,17 +488,53 @@ function deriveChildTile( ); childTile.implicitCoordinates = implicitCoordinates; childTile.implicitSubtree = subtree; + childTile.metadata = tileMetadata; - if (defined(subtree.metadataExtension)) { - var metadataTable = subtree.metadataTable; - childTile.metadata = new ImplicitTileMetadata({ - class: metadataTable.class, - implicitCoordinates: implicitCoordinates, - implicitSubtree: subtree, - }); + return childTile; +} + +/** + * For a derived bounding region, update the minimum and maximum height. This + * is typically used to tighten a bounding volume using the + * TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT + * semantics. Heights are only updated if the respective + * minimumHeight/maximumHeight parameter is defined. + * + * @param {Array} region A 6-element array describing the bounding region + * @param {Number} [minimumHeight] The new minimum height + * @param {Number} [maximumHeight] The new maximum height + * @private + */ +function updateRegionHeights(region, minimumHeight, maximumHeight) { + if (defined(minimumHeight)) { + region[4] = minimumHeight; } - return childTile; + if (defined(maximumHeight)) { + region[5] = maximumHeight; + } +} + +/** + * For a derived bounding S2 cell, update the minimum and maximum height. This + * is typically used to tighten a bounding volume using the + * TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT + * semantics. Heights are only updated if the respective + * minimumHeight/maximumHeight parameter is defined. + * + * @param {Array} region A 6-element array describing the bounding region + * @param {Number} [minimumHeight] The new minimum height + * @param {Number} [maximumHeight] The new maximum height + * @private + */ +function updateS2CellHeights(s2CellVolume, minimumHeight, maximumHeight) { + if (defined(minimumHeight)) { + s2CellVolume.minimumHeight = minimumHeight; + } + + if (defined(maximumHeight)) { + s2CellVolume.maximumHeight = maximumHeight; + } } /** @@ -446,11 +542,30 @@ function deriveChildTile( * * @param {ImplicitTileset} implicitTileset The implicit tileset struct which holds the root bounding volume * @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the child tile + * @param {Number} childIndex The morton index of the child tile relative to its parent + * @param {Boolean} parentIsPlaceholderTile True if parentTile is a placeholder tile. This is true for the root of each subtree. + * @param {Cesium3DTile} parentTile The parent of the new child tile * @returns {Object} An object containing the JSON for a bounding volume * @private */ -function deriveBoundingVolume(implicitTileset, implicitCoordinates) { +function deriveBoundingVolume( + implicitTileset, + implicitCoordinates, + childIndex, + parentIsPlaceholderTile, + parentTile +) { var rootBoundingVolume = implicitTileset.boundingVolume; + + if (has3DTilesExtension(rootBoundingVolume, "3DTILES_bounding_volume_S2")) { + return deriveBoundingVolumeS2( + implicitTileset, + parentTile, + parentIsPlaceholderTile, + childIndex + ); + } + if (defined(rootBoundingVolume.region)) { var childRegion = deriveBoundingRegion( rootBoundingVolume.region, @@ -478,6 +593,82 @@ function deriveBoundingVolume(implicitTileset, implicitCoordinates) { }; } +/** + * Derive a bounding volume for a child tile from a parent tile, + * assuming a quadtree or octree implicit tiling scheme. + *

+ * If implicitSubdivisionScheme is OCTREE, octree subdivision is used. + * Otherwise, quadtree subdivision is used. Quadtrees are always divided + * using the S2 cell hierarchy. Octrees have an additional split at the midpoint + * of the the vertical (z) dimension. + *

+ * + * @param {ImplicitTileset} implicitTileset The implicit tileset struct which holds the root bounding volume + * @param {Cesium3DTile} parentTile The parent of the new child tile + * @param {Boolean} parentIsPlaceholderTile True if parentTile is a placeholder tile. This is true for the root of each subtree. + * @param {Number} childIndex The morton index of the child tile relative to its parent + * @private + */ +function deriveBoundingVolumeS2( + implicitTileset, + parentTile, + parentIsPlaceholderTile, + childIndex +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("implicitTileset", implicitTileset); + Check.typeOf.object("parentTile", parentTile); + Check.typeOf.bool("parentIsPlaceholderTile", parentIsPlaceholderTile); + Check.typeOf.number("childIndex", childIndex); + //>>includeEnd('debug'); + + var boundingVolumeS2; + + // Handle the placeholder tile case. + if (parentIsPlaceholderTile) { + boundingVolumeS2 = parentTile._boundingVolume; + return { + extensions: { + "3DTILES_bounding_volume_S2": { + token: S2Cell.getTokenFromId(boundingVolumeS2.s2Cell._cellId), + minimumHeight: boundingVolumeS2.minimumHeight, + maximumHeight: boundingVolumeS2.maximumHeight, + }, + }, + }; + } + boundingVolumeS2 = parentTile._boundingVolume; + + // Decode Morton index. The modulus 4 ensures that it works for both quadtrees and octrees. + var childCoords = MortonOrder.decode2D(childIndex % 4); + // Encode Hilbert index. + var hilbertIndex = HilbertOrder.encode2D(1, childCoords[0], childCoords[1]); + var childCell = boundingVolumeS2.s2Cell.getChild(hilbertIndex); + + var minHeight, maxHeight; + if (implicitTileset.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { + var midpointHeight = + (boundingVolumeS2.maximumHeight + boundingVolumeS2.minimumHeight) / 2; + minHeight = + childIndex < 4 ? boundingVolumeS2.minimumHeight : midpointHeight; + maxHeight = + childIndex < 4 ? midpointHeight : boundingVolumeS2.maximumHeight; + } else { + minHeight = boundingVolumeS2.minimumHeight; + maxHeight = boundingVolumeS2.maximumHeight; + } + + return { + extensions: { + "3DTILES_bounding_volume_S2": { + token: S2Cell.getTokenFromId(childCell._cellId), + minimumHeight: minHeight, + maximumHeight: maxHeight, + }, + }, + }; +} + var scratchScaleFactors = new Cartesian3(); var scratchRootCenter = new Cartesian3(); var scratchCenter = new Cartesian3(); @@ -594,7 +785,7 @@ function deriveBoundingRegion(rootRegion, level, x, y, z) { //>>includeEnd('debug'); if (level === 0) { - return rootRegion; + return rootRegion.slice(); } var rectangle = Rectangle.unpack(rootRegion, 0, scratchRectangle); @@ -640,7 +831,10 @@ function makePlaceholderChildSubtree(content, parentTile, childIndex) { var childBoundingVolume = deriveBoundingVolume( implicitTileset, - implicitCoordinates + implicitCoordinates, + childIndex, + false, + parentTile ); var childGeometricError = implicitTileset.geometricError / Math.pow(2, implicitCoordinates.level); @@ -723,3 +917,4 @@ Implicit3DTileContent.prototype.destroy = function () { // Exposed for testing Implicit3DTileContent._deriveBoundingBox = deriveBoundingBox; Implicit3DTileContent._deriveBoundingRegion = deriveBoundingRegion; +Implicit3DTileContent._deriveBoundingVolumeS2 = deriveBoundingVolumeS2; diff --git a/Source/Scene/ImplicitTileset.js b/Source/Scene/ImplicitTileset.js index 6dff371f2bbc..ab313e104bcb 100644 --- a/Source/Scene/ImplicitTileset.js +++ b/Source/Scene/ImplicitTileset.js @@ -66,10 +66,11 @@ export default function ImplicitTileset( if ( !defined(tileJson.boundingVolume.box) && - !defined(tileJson.boundingVolume.region) + !defined(tileJson.boundingVolume.region) && + !has3DTilesExtension(tileJson.boundingVolume, "3DTILES_bounding_volume_S2") ) { throw new RuntimeError( - "Only box and region are supported for implicit tiling" + "Only box, region and 3DTILES_bounding_volume_S2 are supported for implicit tiling" ); } diff --git a/Source/Scene/MetadataSemantic.js b/Source/Scene/MetadataSemantic.js index 9082a1455c9d..894563051116 100644 --- a/Source/Scene/MetadataSemantic.js +++ b/Source/Scene/MetadataSemantic.js @@ -26,37 +26,101 @@ var MetadataSemantic = { */ BOUNDING_SPHERE: "BOUNDING_SPHERE", /** - * A minimum height relative to some ellipsoid, stored as a FLOAT32 or a FLOAT64 + * A name, stored as a STRING. This does not have to be unique * * @type {String} * @constant * @private */ - MINIMUM_HEIGHT: "MINIMUM_HEIGHT", + NAME: "NAME", /** - * A maximum height relative to some ellipsoid, stored as a FLOAT32 or a FLOAT64 + * A unique identifier, stored as a STRING. * * @type {String} * @constant * @private */ - MAXIMUM_HEIGHT: "MAXIMUM_HEIGHT", + ID: "ID", /** - * A name, stored as a STRING. This does not have to be unique + * A bounding box for a tile, stored as an array of 12 FLOAT32 or FLOAT64 components. The components are the same format as for boundingVolume.box in 3D Tiles 1.0. This semantic is used to provide a tighter bounding volume than the one implicitly calculated in 3DTILES_implicit_tiling * * @type {String} * @constant * @private */ - NAME: "NAME", + TILE_BOUNDING_BOX: "TILE_BOUNDING_BOX", /** - * A unique identifier, stored as a STRING. + * A bounding region for a tile, stored as an array of 6 FLOAT64 components. The components are [west, south, east, north, minimumHeight, maximumHeight]. This semantic is used to provide a tighter bounding volume than the one implicitly calculated in 3DTILES_implicit_tiling * * @type {String} * @constant * @private */ - ID: "ID", + TILE_BOUNDING_REGION: "TILE_BOUNDING_REGION", + /** + * A bounding sphere for a tile, stored as an array of 4 FLOAT32 or FLOAT64 components. The components are [centerX, centerY, centerZ, radius]. This semantic is used to provide a tighter bounding volume than the one implicitly calculated in 3DTILES_implicit_tiling + * + * @type {String} + * @constant + * @private + */ + TILE_BOUNDING_SPHERE: "TILE_BOUNDING_SPHERE", + /** + * The minimum height of a tile above (or below) the WGS84 ellipsoid, stored as a FLOAT32 or a FLOAT64. This semantic is used to tighten bounding regions implicitly calculated in 3DTILES_implicit_tiling. + * + * @type {String} + * @constant + * @private + */ + TILE_MINIMUM_HEIGHT: "TILE_MINIMUM_HEIGHT", + /** + * The maximum height of a tile above (or below) the WGS84 ellipsoid, stored as a FLOAT32 or a FLOAT64. This semantic is used to tighten bounding regions implicitly calculated in 3DTILES_implicit_tiling. + * + * @type {String} + * @constant + * @private + */ + TILE_MAXIMUM_HEIGHT: "TILE_MINIMUM_HEIGHT", + /** + * A bounding box for the content of a tile, stored as an array of 12 FLOAT32 or FLOAT64 components. The components are the same format as for boundingVolume.box in 3D Tiles 1.0. This semantic is used to provide a tighter bounding volume than the one implicitly calculated in 3DTILES_implicit_tiling + * + * @type {String} + * @constant + * @private + */ + CONTENT_BOUNDING_BOX: "CONTENT_BOUNDING_BOX", + /** + * A bounding region for the content of a tile, stored as an array of 6 FLOAT64 components. The components are [west, south, east, north, minimumHeight, maximumHeight]. This semantic is used to provide a tighter bounding volume than the one implicitly calculated in 3DTILES_implicit_tiling + * + * @type {String} + * @constant + * @private + */ + CONTENT_BOUNDING_REGION: "CONTENT_BOUNDING_REGION", + /** + * A bounding sphere for the content of a tile, stored as an array of 4 FLOAT32 or FLOAT64 components. The components are [centerX, centerY, centerZ, radius]. This semantic is used to provide a tighter bounding volume than the one implicitly calculated in 3DTILES_implicit_tiling + * + * @type {String} + * @constant + * @private + */ + CONTENT_BOUNDING_SPHERE: "CONTENT_BOUNDING_SPHERE", + /** + * The minimum height of the content of a tile above (or below) the WGS84 ellipsoid, stored as a FLOAT32 or a FLOAT64 + * + * @type {String} + * @constant + * @private + */ + CONTENT_MINIMUM_HEIGHT: "CONTENT_MINIMUM_HEIGHT", + /** + * The maximum height of the content of a tile above (or below) the WGS84 ellipsoid, stored as a FLOAT32 or a FLOAT64 + * + * @type {String} + * @constant + * @private + */ + CONTENT_MAXIMUM_HEIGHT: "CONTENT_MINIMUM_HEIGHT", }; export default Object.freeze(MetadataSemantic); diff --git a/Source/Scene/MetadataType.js b/Source/Scene/MetadataType.js index c83d727c5f9b..e51568bded8e 100644 --- a/Source/Scene/MetadataType.js +++ b/Source/Scene/MetadataType.js @@ -433,4 +433,40 @@ MetadataType.unnormalize = function (value, type) { return value; }; +/** + * Gets the size in bytes for the numeric type. + * + * @param {MetadataType} type The type. + * @returns {Number} The size in bytes. + * + * @exception {DeveloperError} type must be a numeric type + * + * @private + */ +MetadataType.getSizeInBytes = function (type) { + //>>includeStart('debug', pragmas.debug); + if (!MetadataType.isNumericType(type)) { + throw new DeveloperError("type must be a numeric type"); + } + //>>includeEnd('debug'); + switch (type) { + case MetadataType.INT8: + case MetadataType.UINT8: + return 1; + case MetadataType.INT16: + case MetadataType.UINT16: + return 2; + case MetadataType.INT32: + case MetadataType.UINT32: + return 4; + case MetadataType.INT64: + case MetadataType.UINT64: + return 8; + case MetadataType.FLOAT32: + return 4; + case MetadataType.FLOAT64: + return 8; + } +}; + export default Object.freeze(MetadataType); diff --git a/Source/Scene/TileBoundingS2Cell.js b/Source/Scene/TileBoundingS2Cell.js new file mode 100644 index 000000000000..071d073460bc --- /dev/null +++ b/Source/Scene/TileBoundingS2Cell.js @@ -0,0 +1,706 @@ +import Cartesian3 from "../Core/Cartesian3.js"; +import defined from "../Core/defined.js"; +import Cartographic from "../Core/Cartographic.js"; +import Ellipsoid from "../Core/Ellipsoid.js"; +import Intersect from "../Core/Intersect.js"; +import Matrix3 from "../Core/Matrix3.js"; +import Plane from "../Core/Plane.js"; +import CoplanarPolygonOutlineGeometry from "../Core/CoplanarPolygonOutlineGeometry.js"; +import BoundingSphere from "../Core/BoundingSphere.js"; +import Check from "../Core/Check.js"; +import ColorGeometryInstanceAttribute from "../Core/ColorGeometryInstanceAttribute.js"; +import defaultValue from "../Core/defaultValue.js"; +import GeometryInstance from "../Core/GeometryInstance.js"; +import Matrix4 from "../Core/Matrix4.js"; +import PerInstanceColorAppearance from "./PerInstanceColorAppearance.js"; +import Primitive from "./Primitive.js"; +import S2Cell from "../Core/S2Cell.js"; + +var centerCartographicScratch = new Cartographic(); +/** + * A tile bounding volume specified as an S2 cell token with minimum and maximum heights. + * The bounding volume is a k DOP. A k-DOP is the Boolean intersection of extents along k directions. + * + * @alias TileBoundingS2Cell + * @constructor + * + * @param {Object} options Object with the following properties: + * @param {String} options.token The token of the S2 cell. + * @param {Number} [options.minimumHeight=0.0] The minimum height of the bounding volume. + * @param {Number} [options.maximumHeight=0.0] The maximum height of the bounding volume. + * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid. + * @param {Boolean} [options.computeBoundingVolumes=true] True to compute the {@link TileBoundingS2Cell#boundingVolume} and + * {@link TileBoundingS2Cell#boundingSphere}. If false, these properties will be undefined. + * + * @private + */ +function TileBoundingS2Cell(options) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("options", options); + Check.typeOf.string("options.token", options.token); + //>>includeEnd('debug'); + + var s2Cell = S2Cell.fromToken(options.token); + var minimumHeight = defaultValue(options.minimumHeight, 0.0); + var maximumHeight = defaultValue(options.maximumHeight, 0.0); + var ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); + + this.s2Cell = s2Cell; + this.minimumHeight = minimumHeight; + this.maximumHeight = maximumHeight; + this.ellipsoid = ellipsoid; + + var boundingPlanes = computeBoundingPlanes( + s2Cell, + minimumHeight, + maximumHeight, + ellipsoid + ); + this._boundingPlanes = boundingPlanes; + + // Pre-compute vertices to speed up the plane intersection test. + var vertices = computeVertices(boundingPlanes); + this._vertices = vertices; + + // Pre-compute edge normals to speed up the point-polygon distance check in distanceToCamera. + this._edgeNormals = new Array(6); + + this._edgeNormals[0] = computeEdgeNormals( + boundingPlanes[0], + vertices.slice(0, 4) + ); + var i; + // Based on the way the edge normals are computed, the edge normals all point away from the "face" + // of the polyhedron they surround, except the plane for the top plane. Therefore, we negate the normals + // for the top plane. + for (i = 0; i < 4; i++) { + this._edgeNormals[0][i] = Cartesian3.negate( + this._edgeNormals[0][i], + this._edgeNormals[0][i] + ); + } + + this._edgeNormals[1] = computeEdgeNormals( + boundingPlanes[1], + vertices.slice(4, 8) + ); + for (i = 0; i < 4; i++) { + // For each plane, iterate through the vertices in CCW order. + this._edgeNormals[2 + i] = computeEdgeNormals(boundingPlanes[2 + i], [ + vertices[i % 4], + vertices[(i + 1) % 4], + vertices[4 + ((i + 1) % 4)], + vertices[4 + i], + ]); + } + + var center = s2Cell.getCenter(); + centerCartographicScratch = ellipsoid.cartesianToCartographic( + center, + centerCartographicScratch + ); + centerCartographicScratch.height = (maximumHeight + minimumHeight) / 2; + this.center = ellipsoid.cartographicToCartesian( + centerCartographicScratch, + center + ); + + this._boundingSphere = BoundingSphere.fromPoints(vertices); +} + +var centerGeodeticNormalScratch = new Cartesian3(); +var topCartographicScratch = new Cartographic(); +var topScratch = new Cartesian3(); +var vertexCartographicScratch = new Cartographic(); +var vertexScratch = new Cartesian3(); +var vertexGeodeticNormalScratch = new Cartesian3(); +var sideNormalScratch = new Cartesian3(); +var sideScratch = new Cartesian3(); +var topPlaneScratch = new Plane(Cartesian3.UNIT_X, 0.0); +var bottomPlaneScratch = new Plane(Cartesian3.UNIT_X, 0.0); +/** + * Computes bounding planes of the kDOP. + * @private + */ +function computeBoundingPlanes( + s2Cell, + minimumHeight, + maximumHeight, + ellipsoid +) { + var planes = new Array(6); + var centerPoint = s2Cell.getCenter(); + + // Compute top plane. + // - Get geodetic surface normal at the center of the S2 cell. + // - Get center point at maximum height of bounding volume. + // - Create top plane from surface normal and top point. + var centerSurfaceNormal = ellipsoid.geodeticSurfaceNormal( + centerPoint, + centerGeodeticNormalScratch + ); + var topCartographic = ellipsoid.cartesianToCartographic( + centerPoint, + topCartographicScratch + ); + topCartographic.height = maximumHeight; + var top = ellipsoid.cartographicToCartesian(topCartographic, topScratch); + var topPlane = Plane.fromPointNormal( + top, + centerSurfaceNormal, + topPlaneScratch + ); + planes[0] = topPlane; + + // Compute bottom plane. + // - Iterate through bottom vertices + // - Get distance from vertex to top plane + // - Find longest distance from vertex to top plane + // - Translate top plane by the distance + var maxDistance = 0; + var i; + var vertices = []; + var vertex, vertexCartographic; + for (i = 0; i < 4; i++) { + vertex = s2Cell.getVertex(i); + vertices[i] = vertex; + vertexCartographic = ellipsoid.cartesianToCartographic( + vertex, + vertexCartographicScratch + ); + vertexCartographic.height = minimumHeight; + var distance = Plane.getPointDistance( + topPlane, + ellipsoid.cartographicToCartesian(vertexCartographic, vertexScratch) + ); + if (distance < maxDistance) { + maxDistance = distance; + } + } + var bottomPlane = Plane.clone(topPlane, bottomPlaneScratch); + // Negate the normal of the bottom plane since we want all normals to point "outwards". + bottomPlane.normal = Cartesian3.negate( + bottomPlane.normal, + bottomPlane.normal + ); + bottomPlane.distance = bottomPlane.distance * -1 + maxDistance; + planes[1] = bottomPlane; + + // Compute side planes. + // - Iterate through vertices (in CCW order, by default) + // - Get a vertex and another vertex adjacent to it. + // - Compute geodetic surface normal at one vertex. + // - Compute vector between vertices. + // - Compute normal of side plane. (cross product of top dir and side dir) + for (i = 0; i < 4; i++) { + vertex = vertices[i]; + var adjacentVertex = vertices[(i + 1) % 4]; + var geodeticNormal = ellipsoid.geodeticSurfaceNormal( + vertex, + vertexGeodeticNormalScratch + ); + var side = Cartesian3.subtract(adjacentVertex, vertex, sideScratch); + var sideNormal = Cartesian3.cross(side, geodeticNormal, sideNormalScratch); + sideNormal = Cartesian3.normalize(sideNormal, sideNormal); + planes[2 + i] = Plane.fromPointNormal(vertex, sideNormal); + } + + return planes; +} + +var n0Scratch = new Cartesian3(); +var n1Scratch = new Cartesian3(); +var n2Scratch = new Cartesian3(); +var x0Scratch = new Cartesian3(); +var x1Scratch = new Cartesian3(); +var x2Scratch = new Cartesian3(); +var t0Scratch = new Cartesian3(); +var t1Scratch = new Cartesian3(); +var t2Scratch = new Cartesian3(); +var f0Scratch = new Cartesian3(); +var f1Scratch = new Cartesian3(); +var f2Scratch = new Cartesian3(); +var sScratch = new Cartesian3(); +var matrixScratch = new Matrix3(); +/** + * Computes intersection of 3 planes. + * @private + */ +function computeIntersection(p0, p1, p2) { + n0Scratch = p0.normal; + n1Scratch = p1.normal; + n2Scratch = p2.normal; + + x0Scratch = Cartesian3.multiplyByScalar(p0.normal, -p0.distance, x0Scratch); + x1Scratch = Cartesian3.multiplyByScalar(p1.normal, -p1.distance, x1Scratch); + x2Scratch = Cartesian3.multiplyByScalar(p2.normal, -p2.distance, x2Scratch); + + f0Scratch = Cartesian3.multiplyByScalar( + Cartesian3.cross(n1Scratch, n2Scratch, t0Scratch), + Cartesian3.dot(x0Scratch, n0Scratch), + f0Scratch + ); + f1Scratch = Cartesian3.multiplyByScalar( + Cartesian3.cross(n2Scratch, n0Scratch, t1Scratch), + Cartesian3.dot(x1Scratch, n1Scratch), + f1Scratch + ); + f2Scratch = Cartesian3.multiplyByScalar( + Cartesian3.cross(n0Scratch, n1Scratch, t2Scratch), + Cartesian3.dot(x2Scratch, n2Scratch), + f2Scratch + ); + + matrixScratch[0] = n0Scratch.x; + matrixScratch[1] = n1Scratch.x; + matrixScratch[2] = n2Scratch.x; + matrixScratch[3] = n0Scratch.y; + matrixScratch[4] = n1Scratch.y; + matrixScratch[5] = n2Scratch.y; + matrixScratch[6] = n0Scratch.z; + matrixScratch[7] = n1Scratch.z; + matrixScratch[8] = n2Scratch.z; + var determinant = Matrix3.determinant(matrixScratch); + sScratch = Cartesian3.add(f0Scratch, f1Scratch, sScratch); + sScratch = Cartesian3.add(sScratch, f2Scratch, sScratch); + return new Cartesian3( + sScratch.x / determinant, + sScratch.y / determinant, + sScratch.z / determinant + ); +} +/** + * Compute the vertices of the kDOP. + * @private + */ +function computeVertices(boundingPlanes) { + var vertices = new Array(8); + for (var i = 0; i < 4; i++) { + // Vertices on the top plane. + vertices[i] = computeIntersection( + boundingPlanes[0], + boundingPlanes[2 + ((i + 3) % 4)], + boundingPlanes[2 + (i % 4)] + ); + // Vertices on the bottom plane. + vertices[i + 4] = computeIntersection( + boundingPlanes[1], + boundingPlanes[2 + ((i + 3) % 4)], + boundingPlanes[2 + (i % 4)] + ); + } + return vertices; +} + +var edgeScratch = new Cartesian3(); +var edgeNormalScratch = new Cartesian3(); +/** + * Compute edge normals on a plane. + * @private + */ +function computeEdgeNormals(plane, vertices) { + var edgeNormals = []; + for (var i = 0; i < 4; i++) { + edgeScratch = Cartesian3.subtract( + vertices[(i + 1) % 4], + vertices[i], + edgeScratch + ); + edgeNormalScratch = Cartesian3.cross( + plane.normal, + edgeScratch, + edgeNormalScratch + ); + edgeNormalScratch = Cartesian3.normalize( + edgeNormalScratch, + edgeNormalScratch + ); + edgeNormals[i] = Cartesian3.clone(edgeNormalScratch); + } + return edgeNormals; +} + +Object.defineProperties(TileBoundingS2Cell.prototype, { + /** + * The underlying bounding volume. + * + * @memberof TileOrientedBoundingBox.prototype + * + * @type {Object} + * @readonly + */ + boundingVolume: { + get: function () { + return this; + }, + }, + /** + * The underlying bounding sphere. + * + * @memberof TileOrientedBoundingBox.prototype + * + * @type {BoundingSphere} + * @readonly + */ + boundingSphere: { + get: function () { + return this._boundingSphere; + }, + }, +}); + +var facePointScratch = new Cartesian3(); +/** + * The distance to point check for this kDOP involves checking the signed distance of the point to each bounding + * plane. A plane qualifies for a distance check if the point being tested against is in the half-space in the direction + * of the normal i.e. if the signed distance of the point from the plane is greater than 0. + * + * There are 4 possible cases for a point if it is outside the polyhedron: + * + * \ X / X \ / \ / \ / + * ---\---------/--- ---\---------/--- ---X---------/--- ---\---------/--- + * \ / \ / \ / \ / + * ---\-----/--- ---\-----/--- ---\-----/--- ---\-----/--- + * \ / \ / \ / \ / + * \ / + * \ + * / \ + * / X \ + * + * I II III IV + * + * Case I: There is only one plane selected. + * In this case, we project the point onto the plane and do a point polygon distance check to find the closest point on the polygon. + * The point may lie inside the "face" of the polygon or outside. If it is outside, we need to determine which edges to test against. + * + * Case II: There are two planes selected. + * In this case, the point will lie somewhere on the line created at the intersection of the selected planes or one of the planes. + * + * Case III: There are three planes selected. + * In this case, the point will lie on the vertex, at the intersection of the selected planes. + * + * Case IV: There are more than three planes selected. + * Since we are on an ellipsoid, this will only happen in the bottom plane, which is what we will use for the distance test. + */ +TileBoundingS2Cell.prototype.distanceToCamera = function (frameState) { + //>>includeStart('debug', pragmas.debug); + Check.defined("frameState", frameState); + //>>includeEnd('debug'); + + var point = frameState.camera.positionWC; + + var selectedPlaneIndices = []; + var vertices = []; + var edgeNormals; + + // PERFORMANCE_IDEA: Look into removing any unnecessary allocations here. Pre-compute dihedral angles. + if (Plane.getPointDistance(this._boundingPlanes[0], point) > 0) { + selectedPlaneIndices.push(0); + vertices.push(this._vertices.slice(0, 4)); + edgeNormals = this._edgeNormals[0]; + } else if (Plane.getPointDistance(this._boundingPlanes[1], point) > 0) { + selectedPlaneIndices.push(1); + vertices.push(this._vertices.slice(4, 8)); + edgeNormals = this._edgeNormals[1]; + } + + var i; + var sidePlaneIndex; + for (i = 0; i < 4; i++) { + sidePlaneIndex = 2 + i; + if ( + Plane.getPointDistance(this._boundingPlanes[sidePlaneIndex], point) > 0 + ) { + selectedPlaneIndices.push(2 + i); + // Store vertices in CCW order. + vertices.push([ + this._vertices[i % 4], + this._vertices[(i + 1) % 4], + this._vertices[4 + ((i + 1) % 4)], + this._vertices[4 + i], + ]); + edgeNormals = this._edgeNormals[2 + i]; + } + } + + // Check if inside all planes. + if (selectedPlaneIndices.length === 0) { + return 0.0; + } + + // We use the skip variable when the side plane indices are non-consecutive. + var skip; + var facePoint; + var selectedPlane; + if (selectedPlaneIndices.length === 1) { + // Handles Case I + selectedPlane = this._boundingPlanes[selectedPlaneIndices[0]]; + facePoint = closestPointPolygon( + Plane.projectPointOntoPlane(selectedPlane, point, facePointScratch), + vertices[0], + selectedPlane, + edgeNormals + ); + return Cartesian3.distance(facePoint, point); + } else if (selectedPlaneIndices.length === 2) { + // Handles Case II + // Since we are on the ellipsoid, the dihedral angle between a top plane and a side plane + // will always be acute, so we can do a faster check there. + if (selectedPlaneIndices[0] === 0) { + var edge = [ + this._vertices[ + 4 * selectedPlaneIndices[0] + (selectedPlaneIndices[1] - 2) + ], + this._vertices[ + 4 * selectedPlaneIndices[0] + ((selectedPlaneIndices[1] - 2 + 1) % 4) + ], + ]; + facePoint = closestPointLineSegment(point, edge[0], edge[1]); + return Cartesian3.distance(facePoint, point); + } + var minimumDistance = Number.MAX_VALUE; + var distance; + for (i = 0; i < 2; i++) { + selectedPlane = this._boundingPlanes[selectedPlaneIndices[i]]; + facePoint = closestPointPolygon( + Plane.projectPointOntoPlane(selectedPlane, point, facePointScratch), + vertices[i], + selectedPlane, + this._edgeNormals[selectedPlaneIndices[i]] + ); + + distance = Cartesian3.distanceSquared(facePoint, point); + if (distance < minimumDistance) { + minimumDistance = distance; + } + } + return Math.sqrt(minimumDistance); + } else if (selectedPlaneIndices.length > 3) { + // Handles Case IV + facePoint = closestPointPolygon( + Plane.projectPointOntoPlane( + this._boundingPlanes[1], + point, + facePointScratch + ), + this._vertices.slice(4, 8), + this._boundingPlanes[1], + this._edgeNormals[1] + ); + return Cartesian3.distance(facePoint, point); + } + + // Handles Case III + skip = selectedPlaneIndices[1] === 2 && selectedPlaneIndices[2] === 5 ? 0 : 1; + + // Vertex is on top plane. + if (selectedPlaneIndices[0] === 0) { + return Cartesian3.distance( + point, + this._vertices[(selectedPlaneIndices[1] - 2 + skip) % 4] + ); + } + + // Vertex is on bottom plane. + return Cartesian3.distance( + point, + this._vertices[4 + ((selectedPlaneIndices[1] - 2 + skip) % 4)] + ); +}; + +var dScratch = new Cartesian3(); +var pL0Scratch = new Cartesian3(); +/** + * Finds point on a line segment closest to a given point. + * @private + */ +function closestPointLineSegment(p, l0, l1) { + var d = Cartesian3.subtract(l1, l0, dScratch); + var pL0 = Cartesian3.subtract(p, l0, pL0Scratch); + var t = Cartesian3.dot(d, pL0); + + if (t <= 0) { + return l0; + } + + var dMag = Cartesian3.dot(d, d); + if (t >= dMag) { + return l1; + } + + t = t / dMag; + return new Cartesian3( + (1 - t) * l0.x + t * l1.x, + (1 - t) * l0.y + t * l1.y, + (1 - t) * l0.z + t * l1.z + ); +} + +var edgePlaneScratch = new Plane(Cartesian3.UNIT_X, 0.0); +/** + * Finds closes point on the polygon, created by the given vertices, from + * a point. The test point and the polygon are all on the same plane. + * @private + */ +function closestPointPolygon(p, vertices, plane, edgeNormals) { + var minDistance = Number.MAX_VALUE; + var distance; + var closestPoint; + var closestPointOnEdge; + + for (var i = 0; i < vertices.length; i++) { + var edgePlane = Plane.fromPointNormal( + vertices[i], + edgeNormals[i], + edgePlaneScratch + ); + var edgePlaneDistance = Plane.getPointDistance(edgePlane, p); + + // Skip checking against the edge if the point is not in the half-space that the + // edgePlane's normal points towards i.e. if the edgePlane is facing away from the point. + if (edgePlaneDistance < 0) { + continue; + } + + closestPointOnEdge = closestPointLineSegment( + p, + vertices[i], + vertices[(i + 1) % 4] + ); + + distance = Cartesian3.distance(p, closestPointOnEdge); + if (distance < minDistance) { + minDistance = distance; + closestPoint = closestPointOnEdge; + } + } + + if (!defined(closestPoint)) { + return p; + } + return closestPoint; +} + +/** + * Determines which side of a plane this volume is located. + * + * @param {Plane} plane The plane to test against. + * @returns {Intersect} {@link Intersect.INSIDE} if the entire volume is on the side of the plane + * the normal is pointing, {@link Intersect.OUTSIDE} if the entire volume is + * on the opposite side, and {@link Intersect.INTERSECTING} if the volume + * intersects the plane. + */ +TileBoundingS2Cell.prototype.intersectPlane = function (plane) { + //>>includeStart('debug', pragmas.debug); + Check.defined("plane", plane); + //>>includeEnd('debug'); + + var plusCount = 0; + var negCount = 0; + for (var i = 0; i < this._vertices.length; i++) { + var distanceToPlane = + Cartesian3.dot(plane.normal, this._vertices[i]) + plane.distance; + if (distanceToPlane < 0) { + negCount++; + } else { + plusCount++; + } + } + + if (plusCount === this._vertices.length) { + return Intersect.INSIDE; + } else if (negCount === this._vertices.length) { + return Intersect.OUTSIDE; + } + return Intersect.INTERSECTING; +}; + +/** + * 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} + */ +TileBoundingS2Cell.prototype.createDebugVolume = function (color) { + //>>includeStart('debug', pragmas.debug); + Check.defined("color", color); + //>>includeEnd('debug'); + + var modelMatrix = new Matrix4.clone(Matrix4.IDENTITY); + var topPlanePolygon = new CoplanarPolygonOutlineGeometry({ + polygonHierarchy: { + positions: this._vertices.slice(0, 4), + }, + }); + var topPlaneGeometry = CoplanarPolygonOutlineGeometry.createGeometry( + topPlanePolygon + ); + var topPlaneInstance = new GeometryInstance({ + geometry: topPlaneGeometry, + id: "topPlane", + modelMatrix: modelMatrix, + attributes: { + color: ColorGeometryInstanceAttribute.fromColor(color), + }, + }); + + var bottomPlanePolygon = new CoplanarPolygonOutlineGeometry({ + polygonHierarchy: { + positions: this._vertices.slice(4), + }, + }); + var bottomPlaneGeometry = CoplanarPolygonOutlineGeometry.createGeometry( + bottomPlanePolygon + ); + var bottomPlaneInstance = new GeometryInstance({ + geometry: bottomPlaneGeometry, + id: "outline", + modelMatrix: modelMatrix, + attributes: { + color: ColorGeometryInstanceAttribute.fromColor(color), + }, + }); + + var sideInstances = []; + for (var i = 0; i < 4; i++) { + var sidePlanePolygon = new CoplanarPolygonOutlineGeometry({ + polygonHierarchy: { + positions: [ + this._vertices[i % 4], + this._vertices[4 + i], + this._vertices[4 + ((i + 1) % 4)], + this._vertices[(i + 1) % 4], + ], + }, + }); + var sidePlaneGeometry = CoplanarPolygonOutlineGeometry.createGeometry( + sidePlanePolygon + ); + sideInstances[i] = new GeometryInstance({ + geometry: sidePlaneGeometry, + id: "outline", + modelMatrix: modelMatrix, + attributes: { + color: ColorGeometryInstanceAttribute.fromColor(color), + }, + }); + } + + return new Primitive({ + geometryInstances: [ + sideInstances[0], + sideInstances[1], + sideInstances[2], + sideInstances[3], + bottomPlaneInstance, + topPlaneInstance, + ], + appearance: new PerInstanceColorAppearance({ + translucent: false, + flat: true, + }), + asynchronous: false, + }); +}; +export default TileBoundingS2Cell; diff --git a/Source/Scene/parseBoundingVolumeSemantics.js b/Source/Scene/parseBoundingVolumeSemantics.js new file mode 100644 index 000000000000..eb6c5d0b370a --- /dev/null +++ b/Source/Scene/parseBoundingVolumeSemantics.js @@ -0,0 +1,124 @@ +import Check from "../Core/Check.js"; +import defined from "../Core/defined.js"; + +/** + * Parse the bounding volume-related semantics such as + * TILE_BOUNDING_BOX and CONTENT_BOUNDING_REGION from + * implicit tile metadata. Results are returned as a JSON object for use when + * transcoding tiles (see {@link Implicit3DTileContent}). + *

+ * Bounding volumes are checked in the order box, region, then sphere. Only + * the first valid bounding volume is returned. + *

+ * + * @see {@link https://github.com/CesiumGS/3d-tiles/tree/3d-tiles-next/specification/Metadata/Semantics|Semantics Specification} for the various bounding volumes and minimum/maximum heights. + * + * @param {TileMetadata} tileMetadata The metadata object for looking up values by semantic. In practice, this will typically be a {@link ImplicitTileMetadata} + * @return {Object} An object containing a tile property and a content property. These contain the bounding volume, and any minimum or maximum height. + * + * @private + * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. + */ +export default function parseBoundingVolumeSemantics(tileMetadata) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("tileMetadata", tileMetadata); + //>>includeEnd('debug'); + + return { + tile: { + boundingVolume: parseBoundingVolume("TILE", tileMetadata), + minimumHeight: parseMinimumHeight("TILE", tileMetadata), + maximumHeight: parseMaximumHeight("TILE", tileMetadata), + }, + content: { + boundingVolume: parseBoundingVolume("CONTENT", tileMetadata), + minimumHeight: parseMinimumHeight("CONTENT", tileMetadata), + maximumHeight: parseMaximumHeight("CONTENT", tileMetadata), + }, + }; +} + +/** + * Parse the bounding volume from a tile metadata. If the metadata specify + * multiple bounding volumes, only the first one is returned. Bounding volumes + * are checked in the order box, region, then sphere. + * + * This handles both tile and content bounding volumes, as the only difference + * is the prefix. e.g. TILE_BOUNDING_BOX and + * CONTENT_BOUNDING_BOX have the same memory layout. + * + * @param {String} prefix Either "TILE" or "CONTENT" + * @param {TileMetadata} tileMetadata The tileMetadata for looking up values + * @return {Object} An object representing the JSON description of the tile metadata + * @private + */ +function parseBoundingVolume(prefix, tileMetadata) { + var boundingBoxSemantic = prefix + "_BOUNDING_BOX"; + var boundingBox = tileMetadata.getPropertyBySemantic(boundingBoxSemantic); + + if (defined(boundingBox)) { + return { + box: boundingBox, + }; + } + + var boundingRegionSemantic = prefix + "_BOUNDING_REGION"; + var boundingRegion = tileMetadata.getPropertyBySemantic( + boundingRegionSemantic + ); + + if (defined(boundingRegion)) { + return { + region: boundingRegion, + }; + } + + var boundingSphereSemantic = prefix + "_BOUNDING_SPHERE"; + var boundingSphere = tileMetadata.getPropertyBySemantic( + boundingSphereSemantic + ); + + if (defined(boundingSphere)) { + // ARRAY with 4 elements is automatically converted to a Cartesian4 + return { + sphere: [ + boundingSphere.x, + boundingSphere.y, + boundingSphere.z, + boundingSphere.w, + ], + }; + } + + return undefined; +} + +/** + * Parse the minimum height from tile metadata. This is used for making tighter + * quadtree bounds for implicit tiling. This works for both + * TILE_MINIMUM_HEIGHT and CONTENT_MINIMUM_HEIGHT + * + * @param {String} prefix Either "TILE" or "CONTENT" + * @param {TileMetadata} tileMetadata The tileMetadata for looking up values + * @return {Number} The minimum height + * @private + */ +function parseMinimumHeight(prefix, tileMetadata) { + var minimumHeightSemantic = prefix + "_MINIMUM_HEIGHT"; + return tileMetadata.getPropertyBySemantic(minimumHeightSemantic); +} + +/** + * Parse the maximum height from tile metadata. This is used for making tighter + * quadtree bounds for implicit tiling. This works for both + * TILE_MAXIMUM_HEIGHT and CONTENT_MAXIMUM_HEIGHT + * + * @param {String} prefix Either "TILE" or "CONTENT" + * @param {TileMetadata} tileMetadata The tileMetadata for looking up values + * @return {Number} The maximum height + * @private + */ +function parseMaximumHeight(prefix, tileMetadata) { + var maximumHeightSemantic = prefix + "_MAXIMUM_HEIGHT"; + return tileMetadata.getPropertyBySemantic(maximumHeightSemantic); +} diff --git a/Source/Shaders/Builtin/Functions/defaultPbrMaterial.glsl b/Source/Shaders/Builtin/Functions/defaultPbrMaterial.glsl new file mode 100644 index 000000000000..b60e8e2b5a33 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/defaultPbrMaterial.glsl @@ -0,0 +1,16 @@ +/** + * Get default parameters for physically based rendering. These defaults + * describe a rough dielectric (non-metal) surface (e.g. rough plastic). + * + * @return {czm_pbrParameters} Default parameters for {@link czm_pbrLighting} + */ +czm_pbrParameters czm_defaultPbrMaterial() +{ + czm_pbrParameters results; + results.diffuseColor = vec3(1.0); + results.roughness = 1.0; + + const vec3 REFLECTANCE_DIELECTRIC = vec3(0.04); + results.f0 = REFLECTANCE_DIELECTRIC; + return results; +} diff --git a/Source/Shaders/Builtin/Functions/pbrLighting.glsl b/Source/Shaders/Builtin/Functions/pbrLighting.glsl new file mode 100644 index 000000000000..a22b5f76ddf9 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/pbrLighting.glsl @@ -0,0 +1,98 @@ +vec3 lambertianDiffuse(vec3 diffuseColor) +{ + return diffuseColor / czm_pi; +} + +vec3 fresnelSchlick2(vec3 f0, vec3 f90, float VdotH) +{ + return f0 + (f90 - f0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0); +} + +float smithVisibilityG1(float NdotV, float roughness) +{ + // this is the k value for direct lighting. + // for image based lighting it will be roughness^2 / 2 + float k = (roughness + 1.0) * (roughness + 1.0) / 8.0; + return NdotV / (NdotV * (1.0 - k) + k); +} + +float smithVisibilityGGX(float roughness, float NdotL, float NdotV) +{ + return ( + smithVisibilityG1(NdotL, roughness) * + smithVisibilityG1(NdotV, roughness) + ); +} + +float GGX(float roughness, float NdotH) +{ + float roughnessSquared = roughness * roughness; + float f = (NdotH * roughnessSquared - NdotH) * NdotH + 1.0; + return roughnessSquared / (czm_pi * f * f); +} + +/** + * Compute the diffuse and specular contributions using physically based + * rendering. This function only handles direct lighting. + *

+ * This function only handles the lighting calculations. Metallic/roughness + * and specular/glossy must be handled separately. See {@czm_pbrMetallicRoughnessMaterial}, {@czm_pbrSpecularGlossinessMaterial} and {@czm_defaultPbrMaterial} + *

+ * + * @name czm_pbrlighting + * @glslFunction + * + * @param {vec3} positionEC The position of the fragment in eye coordinates + * @param {vec3} normalEC The surface normal in eye coordinates + * @param {vec3} lightDirectionEC Unit vector pointing to the light source in eye coordinates. + * @param {vec3} lightColorHdr radiance of the light source. This is a HDR value. + * @param {czm_pbrParameters} The computed PBR parameters. + * @return {vec3} The computed HDR color + * + * @example + * czm_pbrParameters pbrParameters = czm_pbrMetallicRoughnessMaterial( + * baseColor, + * metallic, + * roughness + * ); + * vec3 color = czm_pbrlighting( + * positionEC, + * normalEC, + * lightDirectionEC, + * lightColorHdr, + * pbrParameters); + */ +vec3 czm_pbrLighting( + vec3 positionEC, + vec3 normalEC, + vec3 lightDirectionEC, + vec3 lightColorHdr, + czm_pbrParameters pbrParameters +) { + vec3 v = -normalize(positionEC); + vec3 l = normalize(lightDirectionEC); + vec3 h = normalize(v + l); + vec3 n = normalEC; + float NdotL = clamp(dot(n, l), 0.001, 1.0); + float NdotV = abs(dot(n, v)) + 0.001; + float NdotH = clamp(dot(n, h), 0.0, 1.0); + float LdotH = clamp(dot(l, h), 0.0, 1.0); + float VdotH = clamp(dot(v, h), 0.0, 1.0); + + vec3 f0 = pbrParameters.f0; + float reflectance = max(max(f0.r, f0.g), f0.b); + vec3 f90 = vec3(clamp(reflectance * 25.0, 0.0, 1.0)); + vec3 F = fresnelSchlick2(f0, f90, VdotH); + + float alpha = pbrParameters.roughness; + float G = smithVisibilityGGX(alpha, NdotL, NdotV); + float D = GGX(alpha, NdotH); + vec3 specularContribution = F * G * D / (4.0 * NdotL * NdotV); + + vec3 diffuseColor = pbrParameters.diffuseColor; + // F here represents the specular contribution + vec3 diffuseContribution = (1.0 - F) * lambertianDiffuse(diffuseColor); + + // Lo = kD * albedo/pi + specular * Li * NdotL + return (diffuseContribution + specularContribution) + NdotL * lightColorHdr; +} diff --git a/Source/Shaders/Builtin/Functions/pbrMetallicRoughnessMaterial.glsl b/Source/Shaders/Builtin/Functions/pbrMetallicRoughnessMaterial.glsl new file mode 100644 index 000000000000..fbea4ae5f2fb --- /dev/null +++ b/Source/Shaders/Builtin/Functions/pbrMetallicRoughnessMaterial.glsl @@ -0,0 +1,36 @@ +/** + * Compute parameters for physically based rendering using the + * metallic/roughness workflow. All inputs are linear; sRGB texture values must + * be decoded beforehand + * + * @name czm_pbrMetallicRoughnessMaterial + * @glslFunction + * + * @param {vec3} baseColor For dielectrics, this is the base color. For metals, this is the f0 value (reflectance at normal incidence) + * @param {float} metallic 0.0 indicates dielectric. 1.0 indicates metal. Values in between are allowed (e.g. to model rust or dirt); + * @param {float} roughness A value between 0.0 and 1.0 + * @return {czm_pbrParameters} parameters to pass into {@link czm_pbrLighting} + */ +czm_pbrParameters czm_pbrMetallicRoughnessMaterial( + vec3 baseColor, + float metallic, + float roughness +) { + czm_pbrParameters results; + + // roughness is authored as perceptual roughness + // square it to get material roughness + roughness = clamp(roughness, 0.0, 1.0); + results.roughness = roughness * roughness; + + // dielectrics us f0 = 0.04, metals use albedo as f0 + metallic = clamp(metallic, 0.0, 1.0); + const vec3 REFLECTANCE_DIELECTRIC = vec3(0.04); + vec3 f0 = mix(REFLECTANCE_DIELECTRIC, baseColor, metallic); + results.f0 = f0; + + // diffuse only applies to dielectrics. + results.diffuseColor = baseColor * (1.0 - f0) * (1.0 - metallic); + + return results; +} diff --git a/Source/Shaders/Builtin/Functions/pbrSpecularGlossinessMaterial.glsl b/Source/Shaders/Builtin/Functions/pbrSpecularGlossinessMaterial.glsl new file mode 100644 index 000000000000..c960a1792f11 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/pbrSpecularGlossinessMaterial.glsl @@ -0,0 +1,29 @@ +/** + * Compute parameters for physically based rendering using the + * specular/glossy workflow. All inputs are linear; sRGB texture values must + * be decoded beforehand + * + * @name czm_pbrSpecularGlossinessMaterial + * @glslFunction + * + * @param {vec3} diffuse The diffuse color for dielectrics (non-metals) + * @param {vec3} specular The reflectance at normal incidence (f0) + * @param {float} glossiness A number from 0.0 to 1.0 indicating how smooth the surface is. + * @return {czm_pbrParameters} parameters to pass into {@link czm_pbrLighting} + */ +czm_pbrParameters czm_pbrSpecularGlossinessMaterial( + vec3 diffuse, + vec3 specular, + float glossiness +) { + czm_pbrParameters results; + + // glossiness is the opposite of roughness, but easier for artists to use. + float roughness = 1.0 - glossiness; + results.roughness = roughness * roughness; + + results.diffuseColor = diffuse * (1.0 - max(max(specular.r, specular.g), specular.b)); + results.f0 = specular; + + return results; +} diff --git a/Source/Shaders/Builtin/Structs/pbrParameters.glsl b/Source/Shaders/Builtin/Structs/pbrParameters.glsl new file mode 100644 index 000000000000..e1d33418d834 --- /dev/null +++ b/Source/Shaders/Builtin/Structs/pbrParameters.glsl @@ -0,0 +1,16 @@ +/** + * Parameters for {@link czm_pbrLighting} + * + * @name czm_material + * @glslStruct + * + * @property {vec3} diffuseColor the diffuse color of the material for the lambert term of the rendering equation + * @property {float} roughness a value from 0.0 to 1.0 that indicates how rough the surface of the material is. + * @property {vec3} f0 The reflectance of the material at normal incidence + */ +struct czm_pbrParameters +{ + vec3 diffuseColor; + float roughness; + vec3 f0; +}; diff --git a/Specs/Core/CheckSpec.js b/Specs/Core/CheckSpec.js index a6682ffac320..449a6d3a102f 100644 --- a/Specs/Core/CheckSpec.js +++ b/Specs/Core/CheckSpec.js @@ -1,4 +1,4 @@ -import { Check } from "../../Source/Cesium.js"; +import { Check, FeatureDetection } from "../../Source/Cesium.js"; describe("Core/Check", function () { describe("type checks", function () { @@ -28,6 +28,43 @@ describe("Core/Check", function () { }).toThrowDeveloperError(); }); + it("Check.typeOf.bigint does not throw when passed a bigint", function () { + if (!FeatureDetection.supportsBigInt()) { + return; + } + + expect(function () { + Check.typeOf.bigint("bigint", BigInt()); // eslint-disable-line + }).not.toThrowDeveloperError(); + }); + + it("Check.typeOf.bigint throws when passed a non-bigint", function () { + if (!FeatureDetection.supportsBigInt()) { + return; + } + + expect(function () { + Check.typeOf.bigint("mockName", {}); + }).toThrowDeveloperError(); + expect(function () { + Check.typeOf.bigint("mockName", []); + }).toThrowDeveloperError(); + expect(function () { + Check.typeOf.bigint("mockName", 1); + }).toThrowDeveloperError(); + expect(function () { + Check.typeOf.bigint("mockName", true); + }).toThrowDeveloperError(); + expect(function () { + Check.typeOf.bigint("mockName", "snth"); + }).toThrowDeveloperError(); + expect(function () { + Check.typeOf.bigint("mockName", function () { + return true; + }); + }).toThrowDeveloperError(); + }); + it("Check.typeOf.func does not throw when passed a function", function () { expect(function () { Check.typeOf.func("mockName", function () { diff --git a/Specs/Core/HilbertOrderSpec.js b/Specs/Core/HilbertOrderSpec.js new file mode 100644 index 000000000000..b67ac86f61d9 --- /dev/null +++ b/Specs/Core/HilbertOrderSpec.js @@ -0,0 +1,111 @@ +import { HilbertOrder } from "../../Source/Cesium.js"; + +describe("Core/HilbertOrder", function () { + it("encode2D throws for undefined inputs", function () { + expect(function () { + return HilbertOrder.encode2D(undefined, 0, 0); + }).toThrowDeveloperError(); + expect(function () { + return HilbertOrder.encode2D(0, undefined, 0); + }).toThrowDeveloperError(); + expect(function () { + return HilbertOrder.encode2D(0, 0, undefined); + }).toThrowDeveloperError(); + }); + + it("encode2D throws for invalid level", function () { + expect(function () { + return HilbertOrder.encode2D(-1, 0, 0); + }).toThrowDeveloperError(); + expect(function () { + return HilbertOrder.encode2D(0, 0, 0); + }).toThrowDeveloperError(); + }); + + it("encode2D throws for invalid coordinates", function () { + expect(function () { + return HilbertOrder.encode2D(1, -1, 0); + }).toThrowDeveloperError(); + + expect(function () { + return HilbertOrder.encode2D(0, -1, 0); + }).toThrowDeveloperError(); + + expect(function () { + return HilbertOrder.encode2D(-1, -1, 0); + }).toThrowDeveloperError(); + + expect(function () { + return HilbertOrder.encode2D(1, 2, 0); + }).toThrowDeveloperError(); + expect(function () { + return HilbertOrder.encode2D(1, 0, 2); + }).toThrowDeveloperError(); + expect(function () { + return HilbertOrder.encode2D(1, 2, 2); + }).toThrowDeveloperError(); + }); + + it("encode2D works", function () { + expect(HilbertOrder.encode2D(1, 0, 0)).toEqual(0); + expect(HilbertOrder.encode2D(1, 0, 1)).toEqual(1); + expect(HilbertOrder.encode2D(1, 1, 1)).toEqual(2); + expect(HilbertOrder.encode2D(1, 1, 0)).toEqual(3); + + expect(HilbertOrder.encode2D(2, 0, 0)).toEqual(0); + expect(HilbertOrder.encode2D(2, 1, 0)).toEqual(1); + expect(HilbertOrder.encode2D(2, 1, 1)).toEqual(2); + expect(HilbertOrder.encode2D(2, 0, 1)).toEqual(3); + expect(HilbertOrder.encode2D(2, 0, 2)).toEqual(4); + expect(HilbertOrder.encode2D(2, 0, 3)).toEqual(5); + expect(HilbertOrder.encode2D(2, 1, 3)).toEqual(6); + expect(HilbertOrder.encode2D(2, 1, 2)).toEqual(7); + expect(HilbertOrder.encode2D(2, 2, 2)).toEqual(8); + expect(HilbertOrder.encode2D(2, 2, 3)).toEqual(9); + expect(HilbertOrder.encode2D(2, 3, 3)).toEqual(10); + expect(HilbertOrder.encode2D(2, 3, 2)).toEqual(11); + expect(HilbertOrder.encode2D(2, 3, 1)).toEqual(12); + expect(HilbertOrder.encode2D(2, 2, 1)).toEqual(13); + expect(HilbertOrder.encode2D(2, 2, 0)).toEqual(14); + expect(HilbertOrder.encode2D(2, 3, 0)).toEqual(15); + }); + + it("decode2D throws for invalid level", function () { + expect(function () { + return HilbertOrder.decode2D(-1, 0, 0); + }).toThrowDeveloperError(); + expect(function () { + return HilbertOrder.decode2D(0, 0, 0); + }).toThrowDeveloperError(); + }); + + it("decode2D throws for invalid index", function () { + expect(function () { + return HilbertOrder.decode2D(1, 4); + }).toThrowDeveloperError(); + }); + + it("decode2D works", function () { + expect(HilbertOrder.decode2D(1, 0)).toEqual([0, 0]); + expect(HilbertOrder.decode2D(1, 1)).toEqual([0, 1]); + expect(HilbertOrder.decode2D(1, 2)).toEqual([1, 1]); + expect(HilbertOrder.decode2D(1, 3)).toEqual([1, 0]); + + expect(HilbertOrder.decode2D(2, 0)).toEqual([0, 0]); + expect(HilbertOrder.decode2D(2, 1)).toEqual([1, 0]); + expect(HilbertOrder.decode2D(2, 2)).toEqual([1, 1]); + expect(HilbertOrder.decode2D(2, 3)).toEqual([0, 1]); + expect(HilbertOrder.decode2D(2, 4)).toEqual([0, 2]); + expect(HilbertOrder.decode2D(2, 5)).toEqual([0, 3]); + expect(HilbertOrder.decode2D(2, 6)).toEqual([1, 3]); + expect(HilbertOrder.decode2D(2, 7)).toEqual([1, 2]); + expect(HilbertOrder.decode2D(2, 8)).toEqual([2, 2]); + expect(HilbertOrder.decode2D(2, 9)).toEqual([2, 3]); + expect(HilbertOrder.decode2D(2, 10)).toEqual([3, 3]); + expect(HilbertOrder.decode2D(2, 11)).toEqual([3, 2]); + expect(HilbertOrder.decode2D(2, 12)).toEqual([3, 1]); + expect(HilbertOrder.decode2D(2, 13)).toEqual([2, 1]); + expect(HilbertOrder.decode2D(2, 14)).toEqual([2, 0]); + expect(HilbertOrder.decode2D(2, 15)).toEqual([3, 0]); + }); +}); diff --git a/Specs/Core/S2CellSpec.js b/Specs/Core/S2CellSpec.js new file mode 100644 index 000000000000..aaa4214d34bc --- /dev/null +++ b/Specs/Core/S2CellSpec.js @@ -0,0 +1,291 @@ +/* eslint-disable new-cap */ +/* eslint-disable no-undef */ +import { Cartesian3 } from "../../Source/Cesium.js"; +import { FeatureDetection } from "../../Source/Cesium.js"; +import { Math as CesiumMath } from "../../Source/Cesium.js"; +import { S2Cell } from "../../Source/Cesium.js"; + +describe("Core/S2Cell", function () { + if (!FeatureDetection.supportsBigInt()) { + return; + } + + it("constructor", function () { + var cell = new S2Cell(BigInt("3458764513820540928")); + expect(cell._cellId).toEqual(BigInt("3458764513820540928")); + }); + + it("throws for invalid cell ID in constructor", function () { + // eslint-disable-next-line new-cap + expect(function () { + S2Cell(BigInt(-1)); + }).toThrowDeveloperError(); + }); + + it("throws for missing cell ID in constructor", function () { + // eslint-disable-next-line new-cap + expect(function () { + S2Cell(); + }).toThrowDeveloperError(); + }); + + it("creates cell from valid token", function () { + var cell = S2Cell.fromToken("3"); + expect(cell._cellId).toEqual(BigInt("3458764513820540928")); + }); + + it("throws for creating cell from invalid token", function () { + expect(function () { + S2Cell.fromToken("XX"); + }).toThrowDeveloperError(); + }); + + it("accepts valid token", function () { + var tokenValidity = S2Cell.isValidToken("1"); + expect(tokenValidity).toBe(true); + + tokenValidity = S2Cell.isValidToken("2ef59bd34"); + expect(tokenValidity).toBe(true); + + tokenValidity = S2Cell.isValidToken("2ef59bd352b93ac3"); + expect(tokenValidity).toBe(true); + }); + + it("rejects token of invalid value", function () { + var tokenValidity = S2Cell.isValidToken("LOL"); + expect(tokenValidity).toBe(false); + + tokenValidity = S2Cell.isValidToken("----"); + expect(tokenValidity).toBe(false); + + tokenValidity = S2Cell.isValidToken("9".repeat(17)); + expect(tokenValidity).toBe(false); + + tokenValidity = S2Cell.isValidToken("0"); + expect(tokenValidity).toBe(false); + + tokenValidity = S2Cell.isValidToken("🤡"); + expect(tokenValidity).toBe(false); + }); + + it("throws for token of invalid type", function () { + expect(function () { + S2Cell.isValidToken(420); + }).toThrowDeveloperError(); + expect(function () { + S2Cell.isValidToken({}); + }).toThrowDeveloperError(); + }); + + it("accepts valid cell ID", function () { + var cellIdValidity = S2Cell.isValidId(BigInt("3383782026967071428")); + expect(cellIdValidity).toBe(true); + + cellIdValidity = S2Cell.isValidId(BigInt("3458764513820540928")); + expect(cellIdValidity).toBe(true); + }); + + it("rejects cell ID of invalid value", function () { + var cellIdValidity = S2Cell.isValidId(BigInt("0")); + expect(cellIdValidity).toBe(false); + + cellIdValidity = S2Cell.isValidId(BigInt("-1")); + expect(cellIdValidity).toBe(false); + + cellIdValidity = S2Cell.isValidId(BigInt("18446744073709551619995")); + expect(cellIdValidity).toBe(false); + + cellIdValidity = S2Cell.isValidId(BigInt("222446744073709551619995")); + expect(cellIdValidity).toBe(false); + + cellIdValidity = S2Cell.isValidId( + BigInt( + "0b0010101000000000000000000000000000000000000000000000000000000000" + ) + ); + expect(cellIdValidity).toBe(false); + }); + + it("throws for cell ID of invalid type", function () { + expect(function () { + S2Cell.isValidId(420); + }).toThrowDeveloperError(); + expect(function () { + S2Cell.isValidId("2ef"); + }).toThrowDeveloperError(); + }); + + it("correctly converts cell ID to token", function () { + expect(S2Cell.getIdFromToken("04")).toEqual(BigInt("288230376151711744")); + expect(S2Cell.getIdFromToken("3")).toEqual(BigInt("3458764513820540928")); + expect(S2Cell.getIdFromToken("2ef59bd352b93ac3")).toEqual( + BigInt("3383782026967071427") + ); + }); + + it("correctly converts token to cell ID", function () { + expect(S2Cell.getTokenFromId(BigInt("288230376151711744"))).toEqual("04"); + expect(S2Cell.getTokenFromId(BigInt("3458764513820540928"))).toEqual("3"); + expect(S2Cell.getTokenFromId(BigInt("3383782026967071427"))).toEqual( + "2ef59bd352b93ac3" + ); + }); + + it("gets correct level of cell", function () { + expect(S2Cell.getLevel(BigInt("3170534137668829184"))).toEqual(1); + expect(S2Cell.getLevel(BigInt("3383782026921377792"))).toEqual(16); + expect(S2Cell.getLevel(BigInt("3383782026967071427"))).toEqual(30); + }); + + it("throws on missing/invalid cell ID in getting level of cell", function () { + expect(function () { + S2Cell.getLevel(BigInt("-1")); + }).toThrowDeveloperError(); + expect(function () { + S2Cell.getLevel(); + }).toThrowDeveloperError(); + expect(function () { + S2Cell.getLevel(BigInt("3170534137668829184444")); + }).toThrowDeveloperError(); + expect(function () { + S2Cell.getLevel(BigInt(0)); + }).toThrowDeveloperError(); + }); + + it("gets correct parent of cell", function () { + var cell = new S2Cell(BigInt("3383782026967515136")); + var parent = cell.getParent(); + expect(parent._cellId).toEqual(BigInt("3383782026971709440")); + }); + + it("gets correct parent of cell at given level", function () { + var cell = new S2Cell(BigInt("3383782026967056384")); + var parent = cell.getParentAtLevel(21); + expect(parent._cellId).toEqual(BigInt("3383782026967252992")); + parent = cell.getParentAtLevel(7); + expect(parent._cellId).toEqual(BigInt("3383821801271328768")); + parent = cell.getParentAtLevel(0); + expect(parent._cellId).toEqual(BigInt("3458764513820540928")); + }); + + it("throws on getting parent of cell at invalid level", function () { + var cell = new S2Cell(BigInt("3458764513820540928")); + expect(function () { + cell.getParentAtLevel(0); + }).toThrowDeveloperError(); + + cell = new S2Cell(BigInt("3383782026967072768")); + expect(function () { + cell.getParentAtLevel(-1); + }).toThrowDeveloperError(); + + expect(function () { + cell.getParentAtLevel(30); + }).toThrowDeveloperError(); + }); + + it("throws on getting parent of level 0 cells", function () { + var cell = S2Cell.fromToken("3"); + expect(function () { + cell.getParent(); + }).toThrowDeveloperError(); + }); + + it("gets correct children of cell", function () { + var cell = new S2Cell(BigInt("3383782026971709440")); + var expectedChildCellIds = [ + BigInt(3383782026959126528), + BigInt(3383782026967515136), + BigInt(3383782026975903744), + BigInt(3383782026984292352), + ]; + var i; + for (i = 0; i < 4; i++) { + expect(cell.getChild(i)._cellId).toEqual(expectedChildCellIds[i]); + } + }); + + it("throws on invalid child index in getting children of cell", function () { + var cell = new S2Cell(BigInt("3383782026971709440")); + expect(function () { + cell.getChild(4); + }).toThrowDeveloperError(); + expect(function () { + cell.getChild(-1); + }).toThrowDeveloperError(); + }); + + it("throws on getting children of level 30 cell", function () { + var cell = new S2Cell(BigInt("3383782026967071427")); + expect(cell._level).toEqual(30); + expect(function () { + cell.getChild(0); + }).toThrowDeveloperError(); + }); + + it("gets correct center of cell", function () { + expect(S2Cell.fromToken("1").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(0.0, 0.0), + CesiumMath.EPSILON15 + ); + expect(S2Cell.fromToken("3").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(90.0, 0.0), + CesiumMath.EPSILON15 + ); + expect(S2Cell.fromToken("5").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(-180.0, 90.0), + CesiumMath.EPSILON15 + ); + expect(S2Cell.fromToken("7").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(-180.0, 0.0), + CesiumMath.EPSILON15 + ); + expect(S2Cell.fromToken("9").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(-90.0, 0.0), + CesiumMath.EPSILON15 + ); + expect(S2Cell.fromToken("b").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(0.0, -90.0), + CesiumMath.EPSILON15 + ); + expect(S2Cell.fromToken("2ef59bd352b93ac3").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(105.64131803774308, -10.490091033598308), + CesiumMath.EPSILON15 + ); + expect(S2Cell.fromToken("1234567").getCenter()).toEqualEpsilon( + Cartesian3.fromDegrees(9.868307318504081, 27.468392925827605), + CesiumMath.EPSILON15 + ); + }); + + it("throws on invalid vertex index", function () { + var cell = new S2Cell(BigInt("3383782026971709440")); + expect(function () { + cell.getVertex(-1); + }).toThrowDeveloperError(); + + expect(function () { + cell.getVertex(4); + }).toThrowDeveloperError(); + }); + + it("gets correct vertices of cell", function () { + var cell = S2Cell.fromToken("2ef59bd352b93ac3"); + expect(cell.getVertex(0)).toEqualEpsilon( + Cartesian3.fromDegrees(105.64131799299665, -10.490091077431977), + CesiumMath.EPSILON15 + ); + expect(cell.getVertex(1)).toEqualEpsilon( + Cartesian3.fromDegrees(105.64131808248949, -10.490091072946313), + CesiumMath.EPSILON15 + ); + expect(cell.getVertex(2)).toEqualEpsilon( + Cartesian3.fromDegrees(105.64131808248948, -10.490090989764633), + CesiumMath.EPSILON15 + ); + expect(cell.getVertex(3)).toEqualEpsilon( + Cartesian3.fromDegrees(105.64131799299665, -10.4900909942503), + CesiumMath.EPSILON15 + ); + }); +}); diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/empty.gltf b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/empty.gltf new file mode 100644 index 000000000000..a5e47391db87 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/empty.gltf @@ -0,0 +1,14 @@ +{ + "asset": { + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [0] + } + ], + "nodes": [ + {} + ] +} diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/subtrees/0.0.0.subtree b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/subtrees/0.0.0.subtree new file mode 100644 index 000000000000..2d9763605394 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/subtrees/0.0.0.subtree differ diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/subtrees/metadata.bin b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/subtrees/metadata.bin new file mode 100644 index 000000000000..25f5c756588f Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/subtrees/metadata.bin differ diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/tileset.json b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/tileset.json new file mode 100644 index 000000000000..9dd42396bf7d --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/tileset.json @@ -0,0 +1,74 @@ +{ + "asset": { + "version": "1.0" + }, + "extensionsUsed": [ + "3DTILES_implicit_tiling", + "3DTILES_metadata" + ], + "extensionsRequired": [ + "3DTILES_implicit_tiling" + ], + "extensions": { + "3DTILES_metadata": { + "schema": { + "classes": { + "tile": { + "properties": { + "contentBounds": { + "type": "ARRAY", + "componentType": "FLOAT64", + "componentCount": 4, + "semantic": "CONTENT_BOUNDING_SPHERE" + } + } + } + } + } + } + }, + "geometricError": 4096.0, + "root": { + "transform": [ + 2048, 0, 0, 0, + 0, 2048, 0, 0, + 0, 0, 2048, 0, + 6500000, 0, 0, 1 + ], + "geometricError": 2048.0, + "boundingVolume": { + "box": [ + 0, 0, 0, + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + }, + "children": [ + { + "geometricError": 2048.0, + "boundingVolume": { + "box": [ + 0, 0, 0, + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + }, + "content": { + "uri": "empty.gltf" + }, + "extensions": { + "3DTILES_implicit_tiling": { + "subdivisionScheme": "OCTREE", + "subtreeLevels": 3, + "maximumLevel": 2, + "subtrees": { + "uri": "subtrees/{level}.{x}.{y}.subtree" + } + } + } + } + ] + } +} diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/s2-tileset.json b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/s2-tileset.json new file mode 100644 index 000000000000..66254e7b970f --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/s2-tileset.json @@ -0,0 +1,55 @@ +{ + "asset": { + "version": "1.0" + }, + "extensionsUsed": [ + "3DTILES_implicit_tiling", + "3DTILES_metadata" + ], + "extensionsRequired": [ + "3DTILES_implicit_tiling" + ], + "extensions": { + "3DTILES_metadata": { + "schema": { + "classes": { + "tile": { + "properties": { + "minimumHeight": { + "type": "FLOAT64", + "semantic": "TILE_MINIMUM_HEIGHT" + }, + "maximumHeight": { + "type": "FLOAT64", + "semantic": "TILE_MAXIMUM_HEIGHT" + } + } + } + } + } + } + }, + "geometricError": 4096.0, + "root": { + "geometricError": 2048.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": "1", + "minimumHeight": 0, + "maximumHeight": 10000 + } + } + }, + "extensions": { + "3DTILES_implicit_tiling": { + "subdivisionScheme": "QUADTREE", + "subtreeLevels": 3, + "maximumLevel": 2, + "subtrees": { + "uri": "subtrees/{level}.{x}.{y}.subtree" + } + } + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/subtrees/0.0.0.subtree b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/subtrees/0.0.0.subtree new file mode 100644 index 000000000000..8accd740bc2f Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/subtrees/0.0.0.subtree differ diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/subtrees/metadata.bin b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/subtrees/metadata.bin new file mode 100644 index 000000000000..45332bfe7e1a Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/subtrees/metadata.bin differ diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/tileset.json b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/tileset.json new file mode 100644 index 000000000000..1affd456ff28 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/tileset.json @@ -0,0 +1,56 @@ +{ + "asset": { + "version": "1.0" + }, + "extensionsUsed": [ + "3DTILES_implicit_tiling", + "3DTILES_metadata" + ], + "extensionsRequired": [ + "3DTILES_implicit_tiling" + ], + "extensions": { + "3DTILES_metadata": { + "schema": { + "classes": { + "tile": { + "properties": { + "minimumHeight": { + "type": "FLOAT64", + "semantic": "TILE_MINIMUM_HEIGHT" + }, + "maximumHeight": { + "type": "FLOAT64", + "semantic": "TILE_MAXIMUM_HEIGHT" + } + } + } + } + } + } + }, + "geometricError": 4096.0, + "root": { + "geometricError": 2048.0, + "boundingVolume": { + "region": [ + 0, + 0, + 0.00314, + 0.00314, + 0, + 10000 + ] + }, + "extensions": { + "3DTILES_implicit_tiling": { + "subdivisionScheme": "QUADTREE", + "subtreeLevels": 3, + "maximumLevel": 2, + "subtrees": { + "uri": "subtrees/{level}.{x}.{y}.subtree" + } + } + } + } +} diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/subtrees/0.0.0.subtree b/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/subtrees/0.0.0.subtree new file mode 100644 index 000000000000..4756f1441ebb Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/subtrees/0.0.0.subtree differ diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/subtrees/metadata.bin b/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/subtrees/metadata.bin new file mode 100644 index 000000000000..d799952e5415 Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/subtrees/metadata.bin differ diff --git a/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/tileset.json b/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/tileset.json new file mode 100644 index 000000000000..87b3e7763480 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/tileset.json @@ -0,0 +1,71 @@ +{ + "asset": { + "version": "1.0" + }, + "extensionsUsed": [ + "3DTILES_implicit_tiling", + "3DTILES_metadata" + ], + "extensionsRequired": [ + "3DTILES_implicit_tiling" + ], + "extensions": { + "3DTILES_metadata": { + "schema": { + "classes": { + "tile": { + "properties": { + "tileBounds": { + "type": "ARRAY", + "componentType": "FLOAT64", + "componentCount": 12, + "semantic": "TILE_BOUNDING_BOX" + } + } + } + } + } + } + }, + "geometricError": 4096.0, + "root": { + "transform": [ + 2048, 0, 0, 0, + 0, 2048, 0, 0, + 0, 0, 2048, 0, + 6500000, 0, 0, 1 + ], + "geometricError": 2048.0, + "boundingVolume": { + "box": [ + 0, 0, 0, + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + }, + "children": [ + { + "geometricError": 2048.0, + "boundingVolume": { + "box": [ + 0, 0, 0, + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ] + }, + "extensions": { + "3DTILES_implicit_tiling": { + "subdivisionScheme": "OCTREE", + "subtreeLevels": 4, + "maximumLevel": 3, + "subtrees": { + "uri": "subtrees/{level}.{x}.{y}.subtree" + } + } + } + } + ] + } +} diff --git a/Specs/Scene/Implicit3DTileContentSpec.js b/Specs/Scene/Implicit3DTileContentSpec.js index 2d59de4ef4d9..031a37f62fbe 100644 --- a/Specs/Scene/Implicit3DTileContentSpec.js +++ b/Specs/Scene/Implicit3DTileContentSpec.js @@ -4,8 +4,10 @@ import { Cesium3DTile, Cesium3DTileRefine, Cesium3DTileset, + Ellipsoid, HeadingPitchRange, Implicit3DTileContent, + ImplicitSubdivisionScheme, ImplicitTileCoordinates, ImplicitTileset, Matrix3, @@ -14,6 +16,8 @@ import { GroupMetadata, Multiple3DTileContent, Resource, + TileBoundingSphere, + TileBoundingS2Cell, } from "../../Source/Cesium.js"; import CesiumMath from "../../Source/Core/Math.js"; import ImplicitTilingTester from "../ImplicitTilingTester.js"; @@ -121,9 +125,6 @@ describe( 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, 26.0)); }); afterAll(function () { @@ -140,6 +141,10 @@ describe( }); mockPlaceholderTile.implicitCoordinates = rootCoordinates; mockPlaceholderTile.implicitTileset = implicitTileset; + + // 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, 26.0)); }); afterEach(function () { @@ -564,6 +569,126 @@ describe( }); }); + describe("_deriveBoundingVolumeS2", function () { + var deriveBoundingVolumeS2 = + Implicit3DTileContent._deriveBoundingVolumeS2; + var simpleBoundingVolumeS2 = { + token: "1", + minimumHeight: 0, + maximumHeight: 10, + }; + var simpleBoundingVolumeS2Cell = new TileBoundingS2Cell( + simpleBoundingVolumeS2 + ); + var implicitTilesetS2 = { + boundingVolume: { + extensions: { + "3DTILES_bounding_volume_S2": simpleBoundingVolumeS2, + }, + }, + subdivisionScheme: ImplicitSubdivisionScheme.QUADTREE, + }; + + it("throws if implicitTileset is undefined", function () { + expect(function () { + deriveBoundingVolumeS2(undefined, {}, false, 0); + }).toThrowDeveloperError(); + }); + + it("throws if parentTile is undefined", function () { + expect(function () { + deriveBoundingVolumeS2({}, undefined, false, 0); + }).toThrowDeveloperError(); + }); + + it("throws if parentIsPlaceholderTile is undefined", function () { + expect(function () { + deriveBoundingVolumeS2({}, {}, undefined, 0); + }).toThrowDeveloperError(); + }); + + it("throws if childIndex is undefined", function () { + expect(function () { + deriveBoundingVolumeS2({}, {}, false, undefined); + }).toThrowDeveloperError(); + }); + + it("returns implicit tileset boundingVolume if parentIsPlaceholderTile is true", function () { + var placeholderTile = { + _boundingVolume: simpleBoundingVolumeS2Cell, + }; + var result = deriveBoundingVolumeS2( + implicitTilesetS2, + placeholderTile, + true, + 0 + ); + expect(result).toEqual(implicitTilesetS2.boundingVolume); + expect(result).not.toBe(implicitTilesetS2.boundingVolume); + }); + + it("subdivides correctly using QUADTREE", function () { + var parentTile = { + _boundingVolume: simpleBoundingVolumeS2Cell, + }; + var expected = { + token: "04", + minimumHeight: 0, + maximumHeight: 10, + }; + var result = deriveBoundingVolumeS2( + implicitTilesetS2, + parentTile, + false, + 0 + ); + expect(result).toEqual({ + extensions: { + "3DTILES_bounding_volume_S2": expected, + }, + }); + }); + + it("subdivides correctly using OCTREE", function () { + implicitTilesetS2.subdivisionScheme = ImplicitSubdivisionScheme.OCTREE; + var parentTile = { + _boundingVolume: simpleBoundingVolumeS2Cell, + }; + var expected0 = { + token: "04", + minimumHeight: 0, + maximumHeight: 5, + }; + var expected1 = { + token: "04", + minimumHeight: 5, + maximumHeight: 10, + }; + var result0 = deriveBoundingVolumeS2( + implicitTilesetS2, + parentTile, + false, + 0 + ); + expect(result0).toEqual({ + extensions: { + "3DTILES_bounding_volume_S2": expected0, + }, + }); + var result1 = deriveBoundingVolumeS2( + implicitTilesetS2, + parentTile, + false, + 4 + ); + expect(result1).toEqual({ + extensions: { + "3DTILES_bounding_volume_S2": expected1, + }, + }); + }); + }); + describe("_deriveBoundingBox", function () { var deriveBoundingBox = Implicit3DTileContent._deriveBoundingBox; var simpleBoundingBox = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]; @@ -803,6 +928,14 @@ describe( "Data/Cesium3DTiles/Implicit/ImplicitTileset/tileset.json"; var implicitGroupMetadataUrl = "Data/Cesium3DTiles/Metadata/ImplicitGroupMetadata/tileset.json"; + var implicitHeightSemanticsUrl = + "Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/tileset.json"; + var implicitS2HeightSemanticsUrl = + "Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/s2-tileset.json"; + var implicitTileBoundingVolumeSemanticsUrl = + "Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/tileset.json"; + var implicitContentBoundingVolumeSemanticsUrl = + "Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/tileset.json"; var metadataClass = new MetadataClass({ id: "test", @@ -874,6 +1007,175 @@ describe( }); }); }); + + // view (lon, lat, height) = (0, 0, 0) from height meters above + function viewCartographicOrigin(height) { + var center = Cartesian3.fromDegrees(0.0, 0.0); + var offset = new Cartesian3(0, 0, height); + scene.camera.lookAt(center, offset); + } + + it("uses height semantics to adjust region bounding volumes", function () { + viewCartographicOrigin(10000); + return Cesium3DTilesTester.loadTileset( + scene, + implicitHeightSemanticsUrl + ).then(function (tileset) { + var placeholderTile = tileset.root; + var subtreeRootTile = placeholderTile.children[0]; + + var implicitRegion = + placeholderTile.implicitTileset.boundingVolume.region; + var minimumHeight = implicitRegion[4]; + var maximumHeight = implicitRegion[5]; + + // This tileset uses TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT + // to set tighter bounding volumes + var tiles = []; + gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); + for (var i = 0; i < tiles.length; i++) { + var tileRegion = tiles[i].boundingVolume; + expect(tileRegion.minimumHeight).toBeGreaterThan(minimumHeight); + expect(tileRegion.maximumHeight).toBeLessThan(maximumHeight); + } + }); + }); + + it("uses height semantics to adjust S2 bounding volumes", function () { + viewCartographicOrigin(10000); + return Cesium3DTilesTester.loadTileset( + scene, + implicitS2HeightSemanticsUrl + ).then(function (tileset) { + var placeholderTile = tileset.root; + var subtreeRootTile = placeholderTile.children[0]; + + var implicitS2Volume = + placeholderTile.implicitTileset.boundingVolume.extensions[ + "3DTILES_bounding_volume_S2" + ]; + var minimumHeight = implicitS2Volume.minimumHeight; + var maximumHeight = implicitS2Volume.maximumHeight; + + // This tileset uses TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT + // to set tighter bounding volumes + var tiles = []; + gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); + for (var i = 0; i < tiles.length; i++) { + var tileS2Volume = tiles[i].boundingVolume; + expect(tileS2Volume.minimumHeight).toBeGreaterThan(minimumHeight); + expect(tileS2Volume.maximumHeight).toBeLessThan(maximumHeight); + } + }); + }); + + // get half the bounding cube width from the bounding box's + // halfAxes matrix + function getHalfWidth(boundingBox) { + return boundingBox.boundingVolume.halfAxes[0]; + } + + it("ignores height semantics if the implicit volume is a box", function () { + var cameraHeight = 100; + var rootHalfWidth = 10; + var originalLoadJson = Cesium3DTileset.loadJson; + spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) { + return originalLoadJson(tilesetUrl).then(function (tilesetJson) { + tilesetJson.root.boundingVolume = { + box: [ + Ellipsoid.WGS84.radii.x + cameraHeight, + 0, + 0, + rootHalfWidth, + 0, + 0, + 0, + rootHalfWidth, + 0, + 0, + 0, + rootHalfWidth, + ], + }; + return tilesetJson; + }); + }); + + viewCartographicOrigin(cameraHeight); + return Cesium3DTilesTester.loadTileset( + scene, + implicitHeightSemanticsUrl + ).then(function (tileset) { + var placeholderTile = tileset.root; + var subtreeRootTile = placeholderTile.children[0]; + + var tiles = []; + gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); + + // TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT only apply to + // regions, so this will check that they are not used. + tiles.forEach(function (tile) { + var level = tile.implicitCoordinates.level; + var halfWidth = getHalfWidth(tile.boundingVolume); + // Even for floats, divide by 2 operations are exact as long + // as there is no overflow. + expect(halfWidth).toEqual(rootHalfWidth / Math.pow(2, level)); + }); + }); + }); + + it("uses tile bounding volume from metadata semantics if present", function () { + viewCartographicOrigin(124000); + return Cesium3DTilesTester.loadTileset( + scene, + implicitTileBoundingVolumeSemanticsUrl + ).then(function (tileset) { + var placeholderTile = tileset.root.children[0]; + var subtreeRootTile = placeholderTile.children[0]; + + var rootHalfWidth = 2048; + expect(getHalfWidth(subtreeRootTile.boundingVolume)).toBe( + rootHalfWidth + ); + + for (var level = 1; level < 4; level++) { + var halfWidthAtLevel = rootHalfWidth / (1 << level); + var tiles = []; + gatherTilesPreorder(subtreeRootTile, level, level, tiles); + for (var i = 0; i < tiles.length; i++) { + // In this tileset, each tile's TILE_BOUNDING_BOX is + // smaller than the implicit tile bounds. Make sure + // this is true. + var tile = tiles[i]; + var halfWidth = getHalfWidth(tile.boundingVolume); + expect(halfWidth).toBeLessThan(halfWidthAtLevel); + } + } + }); + }); + + it("uses content bounding volume from metadata semantics if present", function () { + viewCartographicOrigin(124000); + return Cesium3DTilesTester.loadTileset( + scene, + implicitContentBoundingVolumeSemanticsUrl + ).then(function (tileset) { + var placeholderTile = tileset.root.children[0]; + var subtreeRootTile = placeholderTile.children[0]; + + // This tileset defines the content bounding spheres in a + // property with metadata semantic CONTENT_BOUNDING_SPHERE. + // Check that each tile has a content bounding volume. + var tiles = []; + gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); + tiles.forEach(function (tile) { + expect( + tile.contentBoundingVolume instanceof TileBoundingSphere + ).toBe(true); + expect(tile.contentBoundingVolume).not.toBe(tile.boundingVolume); + }); + }); + }); }); }, "WebGL" diff --git a/Specs/Scene/ImplicitTilesetSpec.js b/Specs/Scene/ImplicitTilesetSpec.js index bf424b88e0af..81def66397b0 100644 --- a/Specs/Scene/ImplicitTilesetSpec.js +++ b/Specs/Scene/ImplicitTilesetSpec.js @@ -115,6 +115,33 @@ describe("Scene/ImplicitTileset", function () { expect(implicitTileset.contentUriTemplates).toEqual([]); }); + it("accepts tilesets with 3DTILES_bounding_volume_S2", function () { + var tileJson = clone(implicitTileJson, true); + tileJson.boundingVolume = { + extensions: { + "3DTILES_bounding_volume_S2": { + token: "1", + minimumHeight: 0, + maximumHeight: 100, + }, + }, + }; + var tileJsonS2 = + tileJson.boundingVolume.extensions["3DTILES_bounding_volume_S2"]; + + var metadataSchema; + var implicitTileset = new ImplicitTileset( + baseResource, + tileJson, + metadataSchema + ); + var implicitTilesetS2 = + implicitTileset.boundingVolume.extensions["3DTILES_bounding_volume_S2"]; + expect(implicitTilesetS2.token).toEqual(tileJsonS2.token); + expect(implicitTilesetS2.minimumHeight).toEqual(tileJsonS2.minimumHeight); + expect(implicitTilesetS2.maximumHeight).toEqual(tileJsonS2.maximumHeight); + }); + it("rejects bounding spheres", function () { var sphereJson = { boundingVolume: { diff --git a/Specs/Scene/MetadataTypeSpec.js b/Specs/Scene/MetadataTypeSpec.js index 5f078c8e355e..8eac0e489cf6 100644 --- a/Specs/Scene/MetadataTypeSpec.js +++ b/Specs/Scene/MetadataTypeSpec.js @@ -353,4 +353,27 @@ describe("Scene/MetadataType", function () { MetadataType.unnormalize(10.0, MetadataType.STRING); }).toThrowDeveloperError(); }); + + it("getSizeInBytes", function () { + expect(MetadataType.getSizeInBytes(MetadataType.INT8)).toBe(1); + expect(MetadataType.getSizeInBytes(MetadataType.UINT8)).toBe(1); + expect(MetadataType.getSizeInBytes(MetadataType.INT16)).toBe(2); + expect(MetadataType.getSizeInBytes(MetadataType.UINT16)).toBe(2); + expect(MetadataType.getSizeInBytes(MetadataType.INT32)).toBe(4); + expect(MetadataType.getSizeInBytes(MetadataType.UINT32)).toBe(4); + expect(MetadataType.getSizeInBytes(MetadataType.FLOAT32)).toBe(4); + expect(MetadataType.getSizeInBytes(MetadataType.FLOAT64)).toBe(8); + }); + + it("getSizeInBytes throws without type", function () { + expect(function () { + MetadataType.getSizeInBytes(); + }).toThrowDeveloperError(); + }); + + it("getSizeInBytes throws if type is not a numeric type", function () { + expect(function () { + MetadataType.getSizeInBytes(MetadataType.STRING); + }).toThrowDeveloperError(); + }); }); diff --git a/Specs/Scene/TileBoundingS2CellSpec.js b/Specs/Scene/TileBoundingS2CellSpec.js new file mode 100644 index 000000000000..85c39c4e2262 --- /dev/null +++ b/Specs/Scene/TileBoundingS2CellSpec.js @@ -0,0 +1,211 @@ +import { Cartesian3 } from "../../Source/Cesium.js"; +import { Color } from "../../Source/Cesium.js"; +import { Ellipsoid } from "../../Source/Cesium.js"; +import { Intersect } from "../../Source/Cesium.js"; +import { Math as CesiumMath } from "../../Source/Cesium.js"; +import { Plane } from "../../Source/Cesium.js"; +import { S2Cell } from "../../Source/Cesium.js"; +import { TileBoundingS2Cell } from "../../Source/Cesium.js"; +import createFrameState from "../createFrameState.js"; + +describe("Scene/TileBoundingS2Cell", function () { + var s2Cell = S2Cell.fromToken("1"); + var s2Options = { + token: "1", + minimumHeight: 0, + maximumHeight: 100000, + }; + + var tileS2Cell = new TileBoundingS2Cell(s2Options); + + var frameState; + var camera; + + beforeEach(function () { + frameState = createFrameState(); + camera = frameState.camera; + }); + + it("throws when options.token is undefined", function () { + expect(function () { + return new TileBoundingS2Cell(); + }).toThrowDeveloperError(); + }); + + it("can be instantiated with S2 cell", function () { + var tS2Cell = new TileBoundingS2Cell({ + token: "1", + }); + expect(tS2Cell).toBeDefined(); + expect(tS2Cell.boundingVolume).toBeDefined(); + expect(tS2Cell.boundingSphere).toBeDefined(); + expect(tS2Cell.s2Cell).toBeDefined(); + expect(tS2Cell.center).toBeDefined(); + expect(tS2Cell.minimumHeight).toBeDefined(); + expect(tS2Cell.maximumHeight).toBeDefined(); + }); + + it("can be instantiated with S2 cell and heights", function () { + var tS2Cell = new TileBoundingS2Cell(s2Options); + expect(tS2Cell).toBeDefined(); + expect(tS2Cell.boundingVolume).toBeDefined(); + expect(tS2Cell.boundingSphere).toBeDefined(); + expect(tS2Cell.s2Cell).toBeDefined(); + expect(tS2Cell.center).toBeDefined(); + expect(tS2Cell.minimumHeight).toBeDefined(); + expect(tS2Cell.maximumHeight).toBeDefined(); + }); + + it("distanceToCamera throws when frameState is undefined", function () { + expect(function () { + return tileS2Cell.distanceToCamera(); + }).toThrowDeveloperError(); + }); + + it("distance to camera is 0 when camera is inside bounding volume", function () { + camera.position = s2Cell.getCenter(); + expect(tileS2Cell.distanceToCamera(frameState)).toEqual(0.0); + }); + + var edgeOneScratch = new Cartesian3(); + var edgeTwoScratch = new Cartesian3(); + var faceCenterScratch = new Cartesian3(); + var topPlaneScratch = new Plane(Cartesian3.UNIT_X, 0.0, 0.0); + var sidePlane0Scratch = new Plane(Cartesian3.UNIT_X, 0.0, 0.0); + // Testing for Case I + it("distanceToCamera works when camera is facing only one plane", function () { + var testDistance = 100; + + // Test against the top plane. + var topPlane = Plane.clone(tileS2Cell._boundingPlanes[0], topPlaneScratch); + topPlane.distance -= testDistance; + camera.position = Plane.projectPointOntoPlane(topPlane, tileS2Cell.center); + expect(tileS2Cell.distanceToCamera(frameState)).toEqualEpsilon( + testDistance, + CesiumMath.EPSILON7 + ); + + // Test against the first side plane. + var sidePlane0 = Plane.clone( + tileS2Cell._boundingPlanes[2], + sidePlane0Scratch + ); + var edgeOne = Cartesian3.midpoint( + tileS2Cell._vertices[0], + tileS2Cell._vertices[1], + edgeOneScratch + ); + + var edgeTwo = Cartesian3.midpoint( + tileS2Cell._vertices[4], + tileS2Cell._vertices[5], + edgeTwoScratch + ); + + var faceCenter = Cartesian3.midpoint(edgeOne, edgeTwo, faceCenterScratch); + + sidePlane0.distance -= testDistance; + camera.position = Plane.projectPointOntoPlane(sidePlane0, faceCenter); + expect(tileS2Cell.distanceToCamera(frameState)).toEqualEpsilon( + testDistance, + CesiumMath.EPSILON7 + ); + }); + + var edgeMidpointScratch = new Cartesian3(); + // Testing for Case II + it("distanceToCamera works when camera is facing two planes", function () { + var testDistance = 5; + + // Test with the top plane and the first side plane. + camera.position = Cartesian3.midpoint( + tileS2Cell._vertices[0], + tileS2Cell._vertices[1], + edgeMidpointScratch + ); + camera.position.z -= testDistance; + expect(tileS2Cell.distanceToCamera(frameState)).toEqualEpsilon( + testDistance, + CesiumMath.EPSILON7 + ); + + // Test with first and second side planes. + camera.position = Cartesian3.midpoint( + tileS2Cell._vertices[0], + tileS2Cell._vertices[4], + edgeMidpointScratch + ); + camera.position.x -= 1; + camera.position.z -= 1; + expect(tileS2Cell.distanceToCamera(frameState)).toEqualEpsilon( + Math.SQRT2, + CesiumMath.EPSILON7 + ); + + // Test with bottom plane and second side plane. Handles the obtuse dihedral angle case. + camera.position = Cartesian3.midpoint( + tileS2Cell._vertices[5], + tileS2Cell._vertices[6], + edgeMidpointScratch + ); + camera.position.x -= 10000; + camera.position.y -= 1; + expect(tileS2Cell.distanceToCamera(frameState)).toEqualEpsilon( + 10000, + CesiumMath.EPSILON7 + ); + }); + + var vertex2Scratch = new Cartesian3(); + // Testing for Case III + it("distanceToCamera works when camera is facing three planes", function () { + camera.position = Cartesian3.clone(tileS2Cell._vertices[2], vertex2Scratch); + camera.position.x += 1; + camera.position.y += 1; + camera.position.z += 1; + expect(tileS2Cell.distanceToCamera(frameState)).toEqualEpsilon( + Math.sqrt(3), + CesiumMath.EPSILON7 + ); + }); + + // Testing for Case IV + it("distanceToCamera works when camera is facing more than three planes", function () { + camera.position = new Cartesian3(-Ellipsoid.WGS84.maximumRadius, 0, 0); + expect(tileS2Cell.distanceToCamera(frameState)).toEqualEpsilon( + Ellipsoid.WGS84.maximumRadius + tileS2Cell._boundingPlanes[1].distance, + CesiumMath.EPSILON7 + ); + }); + + it("can create a debug volume", function () { + var debugVolume = tileS2Cell.createDebugVolume(Color.BLUE); + expect(debugVolume).toBeDefined(); + }); + + it("createDebugVolume throws when color is undefined", function () { + expect(function () { + return tileS2Cell.createDebugVolume(); + }).toThrowDeveloperError(); + }); + + it("intersectPlane throws when plane is undefined", function () { + expect(function () { + return tileS2Cell.intersectPlane(); + }).toThrowDeveloperError(); + }); + + it("intersects plane", function () { + expect(tileS2Cell.intersectPlane(Plane.ORIGIN_ZX_PLANE)).toEqual( + Intersect.INTERSECTING + ); + + var outsidePlane = Plane.clone(Plane.ORIGIN_YZ_PLANE); + outsidePlane.distance -= 2 * Ellipsoid.WGS84.maximumRadius; + expect(tileS2Cell.intersectPlane(outsidePlane)).toEqual(Intersect.OUTSIDE); + + expect(tileS2Cell.intersectPlane(Plane.ORIGIN_YZ_PLANE)).toEqual( + Intersect.INSIDE + ); + }); +}); diff --git a/Specs/Scene/parseBoundingVolumeSemanticsSpec.js b/Specs/Scene/parseBoundingVolumeSemanticsSpec.js new file mode 100644 index 000000000000..6a1bcd547c6a --- /dev/null +++ b/Specs/Scene/parseBoundingVolumeSemanticsSpec.js @@ -0,0 +1,204 @@ +import { + TileMetadata, + Math, + MetadataClass, + parseBoundingVolumeSemantics, +} from "../../Source/Cesium.js"; + +describe("Scene/parseBoundingVolumeSemantics", function () { + var boundingBox = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]; + var boundingRegion = [0, 0, Math.PI_OVER_SIX, Math.PI_OVER_TWO, 0, 50]; + var boundingSphere = [0, 0, 0, 1]; + var minimumHeight = -10; + var maximumHeight = 10; + + it("throws without tileMetadata", function () { + expect(function () { + return parseBoundingVolumeSemantics(undefined); + }).toThrowDeveloperError(); + }); + + it("works if no semantics are present", function () { + // Note: TileMetadata is used in unit tests instead of ImplicitTileMetadata + // as the former is more straightforward to construct + var emptyMetadata = new TileMetadata({ + tile: { + properties: {}, + }, + }); + expect(parseBoundingVolumeSemantics(emptyMetadata)).toEqual({ + tile: { + boundingVolume: undefined, + minimumHeight: undefined, + maximumHeight: undefined, + }, + content: { + boundingVolume: undefined, + minimumHeight: undefined, + maximumHeight: undefined, + }, + }); + }); + + it("parses minimum and maximum height", function () { + var tileClass = new MetadataClass({ + id: "tile", + class: { + properties: { + tileMinimumHeight: { + type: "FLOAT32", + semantic: "TILE_MINIMUM_HEIGHT", + }, + tileMaximumHeight: { + type: "FLOAT32", + semantic: "TILE_MAXIMUM_HEIGHT", + }, + contentMinimumHeight: { + type: "FLOAT32", + semantic: "CONTENT_MINIMUM_HEIGHT", + }, + contentMaximumHeight: { + type: "FLOAT32", + semantic: "CONTENT_MAXIMUM_HEIGHT", + }, + }, + }, + }); + + var tileMetadata = new TileMetadata({ + class: tileClass, + tile: { + properties: { + tileMinimumHeight: minimumHeight, + tileMaximumHeight: maximumHeight, + contentMinimumHeight: minimumHeight, + contentMaximumHeight: maximumHeight, + }, + }, + }); + + expect(parseBoundingVolumeSemantics(tileMetadata)).toEqual({ + tile: { + boundingVolume: undefined, + minimumHeight: minimumHeight, + maximumHeight: maximumHeight, + }, + content: { + boundingVolume: undefined, + minimumHeight: minimumHeight, + maximumHeight: maximumHeight, + }, + }); + }); + + it("parses bounding volumes", function () { + var tileClass = new MetadataClass({ + id: "tile", + class: { + properties: { + tileBoundingBox: { + type: "ARRAY", + componentType: "FLOAT64", + componentCount: 12, + semantic: "TILE_BOUNDING_BOX", + }, + contentBoundingSphere: { + type: "ARRAY", + componentType: "FLOAT64", + componentCount: 4, + semantic: "CONTENT_BOUNDING_SPHERE", + }, + }, + }, + }); + + var tileMetadata = new TileMetadata({ + class: tileClass, + tile: { + properties: { + tileBoundingBox: boundingBox, + contentBoundingSphere: boundingSphere, + }, + }, + }); + + expect(parseBoundingVolumeSemantics(tileMetadata)).toEqual({ + tile: { + boundingVolume: { + box: boundingBox, + }, + minimumHeight: undefined, + maximumHeight: undefined, + }, + content: { + boundingVolume: { + sphere: boundingSphere, + }, + minimumHeight: undefined, + maximumHeight: undefined, + }, + }); + }); + + it("bounding volumes are parsed with the precedence box, region, then sphere", function () { + var tileClass = new MetadataClass({ + id: "tile", + class: { + properties: { + tileBoundingBox: { + type: "ARRAY", + componentType: "FLOAT64", + componentCount: 12, + semantic: "TILE_BOUNDING_BOX", + }, + tileBoundingRegion: { + type: "ARRAY", + componentType: "FLOAT64", + componentCount: 6, + semantic: "TILE_BOUNDING_REGION", + }, + contentBoundingRegion: { + type: "ARRAY", + componentType: "FLOAT64", + componentCount: 6, + semantic: "CONTENT_BOUNDING_REGION", + }, + contentBoundingSphere: { + type: "ARRAY", + componentType: "FLOAT64", + componentCount: 4, + semantic: "CONTENT_BOUNDING_SPHERE", + }, + }, + }, + }); + + var tileMetadata = new TileMetadata({ + class: tileClass, + tile: { + properties: { + tileBoundingBox: boundingBox, + tileBoundingRegion: boundingRegion, + contentBoundingRegion: boundingRegion, + contentBoundingSphere: boundingSphere, + }, + }, + }); + expect(parseBoundingVolumeSemantics(tileMetadata)).toEqual({ + tile: { + boundingVolume: { + box: boundingBox, + }, + minimumHeight: undefined, + maximumHeight: undefined, + }, + content: { + boundingVolume: { + region: boundingRegion, + }, + minimumHeight: undefined, + maximumHeight: undefined, + }, + }); + }); +}); diff --git a/package.json b/package.json index 998f54fdddb7..121ae71408dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cesium", - "version": "1.81.0", + "version": "1.82.1", "description": "CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.", "homepage": "http://cesium.com/cesiumjs/", "license": "Apache-2.0",