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 aFLOAT32
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 atile
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",