diff --git "a/Apps/Sandcastle/gallery/Globe Materials \342\200\223 3D Tiles Terrain.html" "b/Apps/Sandcastle/gallery/Globe Materials \342\200\223 3D Tiles Terrain.html"
new file mode 100644
index 000000000000..3c7a7f080184
--- /dev/null
+++ "b/Apps/Sandcastle/gallery/Globe Materials \342\200\223 3D Tiles Terrain.html"
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+ Cesium Demo
+
+
+
+
+
+
+
+
Loading...
+
+
+
+
+
+
diff --git "a/Apps/Sandcastle/gallery/Globe Materials \342\200\223 3D Tiles Terrain.jpg" "b/Apps/Sandcastle/gallery/Globe Materials \342\200\223 3D Tiles Terrain.jpg"
new file mode 100644
index 000000000000..c6750e9243d6
Binary files /dev/null and "b/Apps/Sandcastle/gallery/Globe Materials \342\200\223 3D Tiles Terrain.jpg" differ
diff --git a/CHANGES.md b/CHANGES.md
index 5e8fd07ee03e..1d98795c5c61 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -11,6 +11,7 @@
#### Additions :tada:
+- Added experimental support for loading 3D Tiles as terrain, via `Cesium3DTilesTerrainProvider`. See [the PR](https://github.com/CesiumGS/cesium/pull/12963) for limitations on the types of 3D Tiles that can be used. [#12296](https://github.com/CesiumGS/cesium/issues/12296)
- Added support for [EXT_mesh_primitive_edge_visibility](https://github.com/KhronosGroup/glTF/pull/2479) glTF extension. [#12765](https://github.com/CesiumGS/cesium/issues/12765)
#### Fixes :wrench:
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/0/0/0/0.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/0/0/0/0.glb
new file mode 100644
index 000000000000..90e621554a4f
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/0/0/0/0.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/0/0/0/0.subtree b/Specs/Data/Cesium3DTiles/Terrain/Test/0/0/0/0.subtree
new file mode 100644
index 000000000000..36edc28bbc87
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/0/0/0/0.subtree differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/0/0.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/0/0.glb
new file mode 100644
index 000000000000..7c05ec46789f
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/0/0.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/0/1.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/0/1.glb
new file mode 100644
index 000000000000..34c486d50067
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/0/1.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/1/0.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/1/0.glb
new file mode 100644
index 000000000000..26eed17e8cef
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/1/0.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/1/1.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/1/1.glb
new file mode 100644
index 000000000000..e56abe62efd4
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/0/1/1/1.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/1/0/0/0.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/1/0/0/0.glb
new file mode 100644
index 000000000000..d71592d12edb
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/1/0/0/0.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/1/0/0/0.subtree b/Specs/Data/Cesium3DTiles/Terrain/Test/1/0/0/0.subtree
new file mode 100644
index 000000000000..8d67a0c1135f
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/1/0/0/0.subtree differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/0/0.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/0/0.glb
new file mode 100644
index 000000000000..25e1bea3f654
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/0/0.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/0/1.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/0/1.glb
new file mode 100644
index 000000000000..94bbfc52da97
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/0/1.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/1/0.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/1/0.glb
new file mode 100644
index 000000000000..94c2afaf3420
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/1/0.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/1/1.glb b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/1/1.glb
new file mode 100644
index 000000000000..801e0f545651
Binary files /dev/null and b/Specs/Data/Cesium3DTiles/Terrain/Test/1/1/1/1.glb differ
diff --git a/Specs/Data/Cesium3DTiles/Terrain/Test/tileset.json b/Specs/Data/Cesium3DTiles/Terrain/Test/tileset.json
new file mode 100644
index 000000000000..03db25ee48d9
--- /dev/null
+++ b/Specs/Data/Cesium3DTiles/Terrain/Test/tileset.json
@@ -0,0 +1 @@
+{"asset":{"version":"1.1"},"geometricError":631380.3810809468,"metadata":{"class":"tilesetInfo","properties":{"description":"Entire Earth description","name":"Entire Earth"}},"root":{"boundingVolume":{"region":[-3.141592653589793,-1.5707963267948966,3.141592653589793,1.5707963267948966,-130.93199157714844,80.01030731201172]},"children":[{"boundingVolume":{"region":[-3.141592653589793,-1.5707963267948966,0.0,1.5707963267948966,-76.75321197509766,67.20038604736328]},"content":{"uri":"0/{level}/{x}/{y}.glb"},"geometricError":157845.0952702367,"implicitTiling":{"availableLevels":2,"subdivisionScheme":"QUADTREE","subtreeLevels":7,"subtrees":{"uri":"0/{level}/{x}/{y}.subtree"}}},{"boundingVolume":{"region":[0.0,-1.5707963267948966,3.141592653589793,1.5707963267948966,-130.93199157714844,80.01030731201172]},"content":{"uri":"1/{level}/{x}/{y}.glb"},"geometricError":157845.0952702367,"implicitTiling":{"availableLevels":2,"subdivisionScheme":"QUADTREE","subtreeLevels":7,"subtrees":{"uri":"1/{level}/{x}/{y}.subtree"}}}],"geometricError":315690.1905404734,"refine":"REPLACE"},"schema":{"classes":{"subtreeTile":{"properties":{"boundingSphere":{"array":true,"componentType":"FLOAT64","count":4,"semantic":"TILE_BOUNDING_SPHERE","type":"SCALAR"},"horizonOcclusionPoint":{"componentType":"FLOAT64","semantic":"TILE_HORIZON_OCCLUSION_POINT","type":"VEC3"},"maximumHeight":{"componentType":"FLOAT64","semantic":"TILE_MAXIMUM_HEIGHT","type":"SCALAR"},"minimumHeight":{"componentType":"FLOAT64","semantic":"TILE_MINIMUM_HEIGHT","type":"SCALAR"}}},"tilesetInfo":{"properties":{"description":{"semantic":"DESCRIPTION","type":"STRING"},"name":{"semantic":"NAME","type":"STRING"}}}}}}
\ No newline at end of file
diff --git a/packages/engine/Source/Core/Cesium3DTilesTerrainData.js b/packages/engine/Source/Core/Cesium3DTilesTerrainData.js
new file mode 100644
index 000000000000..3266ae253674
--- /dev/null
+++ b/packages/engine/Source/Core/Cesium3DTilesTerrainData.js
@@ -0,0 +1,936 @@
+import BoundingSphere from "./BoundingSphere.js";
+import Cartesian2 from "./Cartesian2.js";
+import Cartesian3 from "./Cartesian3.js";
+import Cesium3DTilesTerrainGeometryProcessor from "./Cesium3DTilesTerrainGeometryProcessor.js";
+import CesiumMath from "./Math.js";
+import Check from "./Check.js";
+import defined from "./defined.js";
+import DeveloperError from "./DeveloperError.js";
+import Frozen from "./Frozen.js";
+import Intersections2D from "./Intersections2D.js";
+import OrientedBoundingBox from "./OrientedBoundingBox.js";
+import Rectangle from "./Rectangle.js";
+import TaskProcessor from "./TaskProcessor.js";
+import TerrainData from "./TerrainData.js";
+import TerrainEncoding from "./TerrainEncoding.js";
+import TerrainMesh from "./TerrainMesh.js";
+
+/**
+ * Terrain data for a single tile where the terrain data is represented as a glb (binary glTF).
+ *
+ * @alias Cesium3DTilesTerrainData
+ * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
+ * @constructor
+ *
+ * @param {object} options Object with the following properties:
+ * @param {Object.} options.gltf The parsed glTF JSON.
+ * @param {number} options.minimumHeight The minimum terrain height within the tile, in meters above the ellipsoid.
+ * @param {number} options.maximumHeight The maximum terrain height within the tile, in meters above the ellipsoid.
+ * @param {BoundingSphere} options.boundingSphere A sphere bounding all of the vertices in the mesh.
+ * @param {OrientedBoundingBox} options.orientedBoundingBox An oriented bounding box containing all of the vertices in the mesh.
+ * @param {Cartesian3} options.horizonOcclusionPoint The horizon occlusion point of the mesh. If this point
+ * is below the horizon, the entire tile is assumed to be below the horizon as well.
+ * The point is expressed in ellipsoid-scaled coordinates.
+ * @param {number} options.skirtHeight The height of the skirt to add on the edges of the tile.
+ * @param {boolean} [options.requestVertexNormals=false] Indicates whether normals should be loaded.
+ * @param {boolean} [options.requestWaterMask=false] Indicates whether water mask data should be loaded.
+ * @param {Credit[]} [options.credits] Array of credits for this tile.
+ * @param {number} [options.childTileMask=15] A bit mask indicating which of this tile's four children exist.
+ * If a child's bit is set, geometry will be requested for that tile as well when it
+ * is needed. If the bit is cleared, the child tile is not requested and geometry is
+ * instead upsampled from the parent. The bit values are as follows:
+ *
+ *
Bit Position
Bit Value
Child Tile
+ *
0
1
Southwest
+ *
1
2
Southeast
+ *
2
4
Northwest
+ *
3
8
Northeast
+ *
+ * @param {Uint8Array} [options.waterMask] The buffer containing the water mask.
+ * @see TerrainData
+ * @see QuantizedMeshTerrainData
+ * @see HeightmapTerrainData
+ * @see GoogleEarthEnterpriseTerrainData
+ */
+function Cesium3DTilesTerrainData(options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ //>>includeStart('debug', pragmas.debug)
+ Check.defined("options.gltf", options.gltf);
+ Check.typeOf.number("options.minimumHeight", options.minimumHeight);
+ Check.typeOf.number("options.maximumHeight", options.maximumHeight);
+ Check.typeOf.object("options.boundingSphere", options.boundingSphere);
+ Check.typeOf.object(
+ "option.orientedBoundingBox",
+ options.orientedBoundingBox,
+ );
+ Check.typeOf.object(
+ "options.horizonOcclusionPoint",
+ options.horizonOcclusionPoint,
+ );
+ Check.typeOf.number("options.skirtHeight", options.skirtHeight);
+ //>>includeEnd('debug');
+
+ this._minimumHeight = options.minimumHeight;
+ this._maximumHeight = options.maximumHeight;
+ this._skirtHeight = options.skirtHeight;
+ this._boundingSphere = BoundingSphere.clone(
+ options.boundingSphere,
+ new BoundingSphere(),
+ );
+ this._orientedBoundingBox = OrientedBoundingBox.clone(
+ options.orientedBoundingBox,
+ new OrientedBoundingBox(),
+ );
+ this._horizonOcclusionPoint = Cartesian3.clone(
+ options.horizonOcclusionPoint,
+ new Cartesian3(),
+ );
+ this._hasVertexNormals = options.requestVertexNormals ?? false;
+ this._hasWaterMask = options.requestWaterMask ?? false;
+ this._hasWebMercatorT = true;
+ this._credits = options.credits;
+ this._childTileMask = options.childTileMask ?? 15;
+ this._gltf = options.gltf;
+
+ /**
+ * @private
+ * @type {TerrainMesh|undefined}
+ */
+ this._mesh = undefined;
+
+ this._waterMask = options.waterMask;
+}
+
+Object.defineProperties(Cesium3DTilesTerrainData.prototype, {
+ /**
+ * An array of credits for this tile.
+ * @memberof Cesium3DTilesTerrainData.prototype
+ * @type {Credit[]|undefined}
+ */
+ credits: {
+ get: function () {
+ return this._credits;
+ },
+ },
+ /**
+ * The water mask included in this terrain data, if any. A water mask is a rectangular
+ * Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land.
+ * Values in between 0 and 255 are allowed as well to smoothly blend between land and water.
+ * @memberof Cesium3DTilesTerrainData.prototype
+ * @type {Uint8Array|HTMLImageElement|HTMLCanvasElement|ImageBitmap|undefined}
+ */
+ waterMask: {
+ get: function () {
+ return this._waterMask;
+ },
+ },
+});
+
+/**
+ * Computes the terrain height at a specified longitude and latitude.
+ * @function
+ *
+ * @param {Rectangle} rectangle The rectangle covered by this terrain data.
+ * @param {number} longitude The longitude in radians.
+ * @param {number} latitude The latitude in radians.
+ * @returns {number|undefined} The terrain height at the specified position. If the position
+ * is outside the rectangle, this method will extrapolate the height, which is likely to be wildly
+ * incorrect for positions far outside the rectangle.
+ */
+Cesium3DTilesTerrainData.prototype.interpolateHeight = function (
+ rectangle,
+ longitude,
+ latitude,
+) {
+ const mesh = this._mesh;
+ if (mesh === undefined) {
+ return undefined;
+ }
+
+ const height = interpolateMeshHeight(mesh, rectangle, longitude, latitude);
+ return height;
+};
+
+/**
+ * Determines if a given child tile is available, based on the
+ * {@link TerrainData#childTileMask}. The given child tile coordinates are assumed
+ * to be one of the four children of this tile. If non-child tile coordinates are
+ * given, the availability of the southeast child tile is returned.
+ * @function
+ *
+ * @param {number} thisX The tile X coordinate of this (the parent) tile.
+ * @param {number} thisY The tile Y coordinate of this (the parent) tile.
+ * @param {number} childX The tile X coordinate of the child tile to check for availability.
+ * @param {number} childY The tile Y coordinate of the child tile to check for availability.
+ * @returns {boolean} True if the child tile is available; otherwise, false.
+ */
+Cesium3DTilesTerrainData.prototype.isChildAvailable = function (
+ thisX,
+ thisY,
+ childX,
+ childY,
+) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number("thisX", thisX);
+ Check.typeOf.number("thisY", thisY);
+ Check.typeOf.number("childX", childX);
+ Check.typeOf.number("childY", childY);
+ //>>includeEnd('debug');
+
+ let bitNumber = 2; // northwest child
+ if (childX !== thisX * 2) {
+ ++bitNumber; // east child
+ }
+ if (childY !== thisY * 2) {
+ bitNumber -= 2; // south child
+ }
+
+ return (this._childTileMask & (1 << bitNumber)) !== 0;
+};
+
+const createMeshTaskName = "createVerticesFromCesium3DTilesTerrain";
+const createMeshTaskProcessorNoThrottle = new TaskProcessor(createMeshTaskName);
+const createMeshTaskProcessorThrottle = new TaskProcessor(
+ createMeshTaskName,
+ TerrainData.maximumAsynchronousTasks,
+);
+
+/**
+ * Creates a {@link TerrainMesh} from this terrain data.
+ *
+ * @private
+ *
+ * @param {object} options Object with the following properties:
+ * @param {TilingScheme} options.tilingScheme The tiling scheme to which this tile belongs.
+ * @param {number} options.x The X coordinate of the tile for which to create the terrain data.
+ * @param {number} options.y The Y coordinate of the tile for which to create the terrain data.
+ * @param {number} options.level The level of the tile for which to create the terrain data.
+ * @param {number} [options.exaggeration=1.0] The scale used to exaggerate the terrain.
+ * @param {number} [options.exaggerationRelativeHeight=0.0] The height relative to which terrain is exaggerated.
+ * @param {boolean} [options.throttle=true] If true, indicates that this operation will need to be retried if too many asynchronous mesh creations are already in progress.
+ * @returns {Promise.|undefined} A promise for the terrain mesh, or undefined if too many
+ * asynchronous mesh creations are already in progress and the operation should
+ * be retried later.
+ */
+Cesium3DTilesTerrainData.prototype.createMesh = function (options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ //>>includeStart('debug', pragmas.debug)
+ Check.typeOf.object("options.tilingScheme", options.tilingScheme);
+ Check.typeOf.number("options.x", options.x);
+ Check.typeOf.number("options.y", options.y);
+ Check.typeOf.number("options.level", options.level);
+ //>>includeEnd('debug');
+
+ const throttle = options.throttle ?? true;
+ const createMeshTaskProcessor = throttle
+ ? createMeshTaskProcessorThrottle
+ : createMeshTaskProcessorNoThrottle;
+
+ const tilingScheme = options.tilingScheme;
+ const ellipsoid = tilingScheme.ellipsoid;
+ const x = options.x;
+ const y = options.y;
+ const level = options.level;
+ const rectangle = tilingScheme.tileXYToRectangle(
+ x,
+ y,
+ level,
+ new Rectangle(),
+ );
+
+ const gltf = this._gltf;
+ const verticesPromise = createMeshTaskProcessor.scheduleTask({
+ ellipsoid: ellipsoid,
+ rectangle: rectangle,
+ hasVertexNormals: this._hasVertexNormals,
+ hasWaterMask: this._hasWaterMask,
+ hasWebMercatorT: this._hasWebMercatorT,
+ gltf: gltf,
+ minimumHeight: this._minimumHeight,
+ maximumHeight: this._maximumHeight,
+ boundingSphere: this._boundingSphere,
+ orientedBoundingBox: this._orientedBoundingBox,
+ horizonOcclusionPoint: this._horizonOcclusionPoint,
+ skirtHeight: this._skirtHeight,
+ exaggeration: options.exaggeration,
+ exaggerationRelativeHeight: options.exaggerationRelativeHeight,
+ });
+
+ if (!defined(verticesPromise)) {
+ // Too many active requests. Postponed.
+ return undefined;
+ }
+
+ const that = this;
+ return Promise.resolve(verticesPromise).then(function (result) {
+ const taskResult = result;
+
+ // Need to re-clone and re-wrap all buffers and complex objects to put them back into their normal state
+ const encoding = TerrainEncoding.clone(
+ taskResult.encoding,
+ new TerrainEncoding(),
+ );
+ const vertices = new Float32Array(taskResult.verticesBuffer);
+ const vertexCount = vertices.length / encoding.stride;
+ const vertexCountWithoutSkirts = taskResult.vertexCountWithoutSkirts;
+ // For consistency with glTF spec, 16 bit index buffer can't contain 65535
+ const SizedIndexType = vertexCount <= 65535 ? Uint16Array : Uint32Array;
+ const indices = new SizedIndexType(taskResult.indicesBuffer);
+ const westIndices = new SizedIndexType(taskResult.westIndicesBuffer);
+ const eastIndices = new SizedIndexType(taskResult.eastIndicesBuffer);
+ const southIndices = new SizedIndexType(taskResult.southIndicesBuffer);
+ const northIndices = new SizedIndexType(taskResult.northIndicesBuffer);
+ const indexCountWithoutSkirts = taskResult.indexCountWithoutSkirts;
+ const minimumHeight = that._minimumHeight;
+ const maximumHeight = that._maximumHeight;
+ const center = Cartesian3.clone(encoding.center, new Cartesian3());
+ const boundingSphere = BoundingSphere.clone(
+ that._boundingSphere,
+ new BoundingSphere(),
+ );
+ const horizonOcclusionPoint = Cartesian3.clone(
+ that._horizonOcclusionPoint,
+ new Cartesian3(),
+ );
+ const orientedBoundingBox = OrientedBoundingBox.clone(
+ that._orientedBoundingBox,
+ new OrientedBoundingBox(),
+ );
+
+ const mesh = new TerrainMesh(
+ center,
+ vertices,
+ indices,
+ indexCountWithoutSkirts,
+ vertexCountWithoutSkirts,
+ minimumHeight,
+ maximumHeight,
+ boundingSphere,
+ horizonOcclusionPoint,
+ encoding.stride,
+ orientedBoundingBox,
+ encoding,
+ westIndices,
+ southIndices,
+ eastIndices,
+ northIndices,
+ );
+
+ that._mesh = mesh;
+
+ return Promise.resolve(mesh);
+ });
+};
+
+/**
+ * Creates a {@link TerrainMesh} from this terrain data synchronously.
+ *
+ * @private
+ *
+ * @param {object} options Object with the following properties:
+ * @param {TilingScheme} options.tilingScheme The tiling scheme to which this tile belongs.
+ * @param {number} options.x The X coordinate of the tile for which to create the terrain data.
+ * @param {number} options.y The Y coordinate of the tile for which to create the terrain data.
+ * @param {number} options.level The level of the tile for which to create the terrain data.
+ * @param {number} [options.exaggeration=1.0] The scale used to exaggerate the terrain.
+ * @param {number} [options.exaggerationRelativeHeight=0.0] The height relative to which terrain is exaggerated.
+ * @returns {Promise.} A promise for the terrain mesh.
+ */
+Cesium3DTilesTerrainData.prototype._createMeshSync = function (options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ //>>includeStart('debug', pragmas.debug)
+ Check.typeOf.object("options.tilingScheme", options.tilingScheme);
+ Check.typeOf.number("options.x", options.x);
+ Check.typeOf.number("options.y", options.y);
+ Check.typeOf.number("options.level", options.level);
+ //>>includeEnd('debug');
+
+ const tilingScheme = options.tilingScheme;
+ const ellipsoid = tilingScheme.ellipsoid;
+ const x = options.x;
+ const y = options.y;
+ const level = options.level;
+ const rectangle = tilingScheme.tileXYToRectangle(
+ x,
+ y,
+ level,
+ new Rectangle(),
+ );
+
+ const meshPromise = Cesium3DTilesTerrainGeometryProcessor.createMesh({
+ ellipsoid: ellipsoid,
+ rectangle: rectangle,
+ hasVertexNormals: this._hasVertexNormals,
+ hasWebMercatorT: this._hasWebMercatorT,
+ gltf: this._gltf,
+ minimumHeight: this._minimumHeight,
+ maximumHeight: this._maximumHeight,
+ boundingSphere: this._boundingSphere,
+ orientedBoundingBox: this._orientedBoundingBox,
+ horizonOcclusionPoint: this._horizonOcclusionPoint,
+ skirtHeight: this._skirtHeight,
+ exaggeration: options.exaggeration,
+ exaggerationRelativeHeight: options.exaggerationRelativeHeight,
+ });
+
+ const that = this;
+ return Promise.resolve(meshPromise).then(function (mesh) {
+ that._mesh = mesh;
+ return Promise.resolve(mesh);
+ });
+};
+
+/**
+ * Upsamples this terrain data for use by a descendant tile.
+ *
+ * @param {TilingScheme} tilingScheme The tiling scheme of this terrain data.
+ * @param {number} thisX The X coordinate of this tile in the tiling scheme.
+ * @param {number} thisY The Y coordinate of this tile in the tiling scheme.
+ * @param {number} thisLevel The level of this tile in the tiling scheme.
+ * @param {number} descendantX The X coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantY The Y coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantLevel The level within the tiling scheme of the descendant tile for which we are upsampling.
+ * @returns {Promise.|undefined} A promise for upsampled terrain data for the descendant tile, or undefined if createMesh has not been called yet or too many asynchronous upsample operations are in progress and the request has been deferred.
+ */
+Cesium3DTilesTerrainData.prototype.upsample = function (
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+) {
+ // mesh is not defined, so there are no UVs yet, so exit early
+ const mesh = this._mesh;
+ if (mesh === undefined) {
+ return undefined;
+ }
+
+ const isSynchronous = false;
+ const upsampledTerrainData = upsampleMesh(
+ isSynchronous,
+ mesh,
+ this._skirtHeight,
+ this._credits,
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+ );
+ return upsampledTerrainData;
+};
+
+/**
+ * Upsamples this terrain data for use by a descendant tile synchronously.
+ *
+ * @private
+ *
+ * @param {TilingScheme} tilingScheme The tiling scheme of this terrain data.
+ * @param {number} thisX The X coordinate of this tile in the tiling scheme.
+ * @param {number} thisY The Y coordinate of this tile in the tiling scheme.
+ * @param {number} thisLevel The level of this tile in the tiling scheme.
+ * @param {number} descendantX The X coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantY The Y coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantLevel The level within the tiling scheme of the descendant tile for which we are upsampling.
+ * @returns {Promise.|undefined} A promise for upsampled terrain data for the descendant tile, or undefined if createMesh has not been called yet.
+ */
+Cesium3DTilesTerrainData.prototype._upsampleSync = function (
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+) {
+ // mesh is not defined, so there are no UVs yet, so exit early
+ const mesh = this._mesh;
+ if (mesh === undefined) {
+ return undefined;
+ }
+
+ const isSynchronous = true;
+ const upsampledTerrainData = upsampleMesh(
+ isSynchronous,
+ mesh,
+ this._skirtHeight,
+ this._credits,
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+ );
+ return upsampledTerrainData;
+};
+
+/**
+ * Gets a value indicating whether or not this terrain data was created by upsampling lower resolution
+ * terrain data. If this value is false, the data was obtained from some other source, such
+ * as by downloading it from a remote server. This method should return true for instances
+ * returned from a call to {@link Cesium3DTilesTerrainData#upsample}.
+ * @function
+ *
+ * @returns {boolean} True if this instance was created by upsampling; otherwise, false.
+ */
+Cesium3DTilesTerrainData.prototype.wasCreatedByUpsampling = function () {
+ return false;
+};
+
+/**
+ * @private
+ * @constructor
+ *
+ * @param {object} options Object with the following properties:
+ * @param {TerrainMesh} options.terrainMesh The terrain mesh.
+ * @param {number} options.skirtHeight The height of the skirt to add on the edges of the tile.
+ * @param {Credit[]} [options.credits] Array of credits for this tile.
+ */
+function Cesium3DTilesUpsampleTerrainData(options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ //>>includeStart('debug', pragmas.debug)
+ Check.defined("options.terrainMesh", options.terrainMesh);
+ Check.defined("options.skirtHeight", options.skirtHeight);
+ //>>includeEnd('debug');
+
+ this._mesh = options.terrainMesh;
+ this._skirtHeight = options.skirtHeight;
+ this._credits = options.credits;
+}
+
+/**
+ * Creates a {@link TerrainMesh} from this terrain data.
+ * @function
+ *
+ * @private
+ *
+ * @param {object} options Object with the following properties:
+ * @param {TilingScheme} options.tilingScheme The tiling scheme to which this tile belongs.
+ * @param {number} options.x The X coordinate of the tile for which to create the terrain data.
+ * @param {number} options.y The Y coordinate of the tile for which to create the terrain data.
+ * @param {number} options.level The level of the tile for which to create the terrain data.
+ * @param {number} [options.exaggeration=1.0] The scale used to exaggerate the terrain.
+ * @param {number} [options.exaggerationRelativeHeight=0.0] The height relative to which terrain is exaggerated.
+ * @param {boolean} [options.throttle=true] If true, indicates that this operation will need to be retried if too many asynchronous mesh creations are already in progress.
+ * @returns {Promise.|undefined} A promise for the terrain mesh, or undefined if too many asynchronous mesh creations are already in progress and the operation should be retried later.
+ */
+Cesium3DTilesUpsampleTerrainData.prototype.createMesh = function (options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("options.tilingScheme", options.tilingScheme);
+ Check.typeOf.number("options.x", options.x);
+ Check.typeOf.number("options.y", options.y);
+ Check.typeOf.number("options.level", options.level);
+ //>>includeEnd('debug');
+
+ return Promise.resolve(this._mesh);
+};
+
+/**
+ * Upsamples this terrain data for use by a descendant tile.
+ *
+ * @param {TilingScheme} tilingScheme The tiling scheme of this terrain data.
+ * @param {number} thisX The X coordinate of this tile in the tiling scheme.
+ * @param {number} thisY The Y coordinate of this tile in the tiling scheme.
+ * @param {number} thisLevel The level of this tile in the tiling scheme.
+ * @param {number} descendantX The X coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantY The Y coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantLevel The level within the tiling scheme of the descendant tile for which we are upsampling.
+ * @returns {Promise.|undefined} A promise for upsampled terrain data for the descendant tile, or undefined if too many asynchronous upsample operations are in progress and the request has been deferred.
+ */
+Cesium3DTilesUpsampleTerrainData.prototype.upsample = function (
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+) {
+ const isSynchronous = false;
+ const upsampledTerrainData = upsampleMesh(
+ isSynchronous,
+ this._mesh,
+ this._skirtHeight,
+ this._credits,
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+ );
+ return upsampledTerrainData;
+};
+
+/**
+ * Upsamples this terrain data for use by a descendant tile synchronously.
+ *
+ * @private
+ *
+ * @param {TilingScheme} tilingScheme The tiling scheme of this terrain data.
+ * @param {number} thisX The X coordinate of this tile in the tiling scheme.
+ * @param {number} thisY The Y coordinate of this tile in the tiling scheme.
+ * @param {number} thisLevel The level of this tile in the tiling scheme.
+ * @param {number} descendantX The X coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantY The Y coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantLevel The level within the tiling scheme of the descendant tile for which we are upsampling.
+ * @returns {Promise.} A promise for upsampled terrain data for the descendant tile.
+ */
+Cesium3DTilesUpsampleTerrainData.prototype._upsampleSync = function (
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+) {
+ const isSynchronous = true;
+ return upsampleMesh(
+ isSynchronous,
+ this._mesh,
+ this._skirtHeight,
+ this._credits,
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+ );
+};
+
+/**
+ * Computes the terrain height at a specified longitude and latitude.
+ * @function
+ *
+ * @param {Rectangle} rectangle The rectangle covered by this terrain data.
+ * @param {number} longitude The longitude in radians.
+ * @param {number} latitude The latitude in radians.
+ * @returns {number} The terrain height at the specified position. If the position is outside the rectangle, this method will extrapolate the height, which is likely to be wildly incorrect for positions far outside the rectangle.
+ */
+Cesium3DTilesUpsampleTerrainData.prototype.interpolateHeight = function (
+ rectangle,
+ longitude,
+ latitude,
+) {
+ const mesh = this._mesh;
+ const height = interpolateMeshHeight(mesh, rectangle, longitude, latitude);
+ return height;
+};
+
+/**
+ * Gets a value indicating whether or not this terrain data was created by upsampling lower resolution
+ * terrain data. If this value is false, the data was obtained from some other source, such
+ * as by downloading it from a remote server. This method should return true for instances
+ * returned from a call to {@link TerrainData#upsample}.
+ * @function
+ *
+ * @returns {boolean} True if this instance was created by upsampling; otherwise, false.
+ */
+Cesium3DTilesUpsampleTerrainData.prototype.wasCreatedByUpsampling =
+ function () {
+ return true;
+ };
+
+/**
+ * Determines if a given child tile is available, based on the
+ * {@link TerrainData#childTileMask}. The given child tile coordinates are assumed
+ * to be one of the four children of this tile. If non-child tile coordinates are
+ * given, the availability of the southeast child tile is returned.
+ * @function
+ *
+ * @param {number} _thisX The tile X coordinate of this (the parent) tile.
+ * @param {number} _thisY The tile Y coordinate of this (the parent) tile.
+ * @param {number} _childX The tile X coordinate of the child tile to check for availability.
+ * @param {number} _childY The tile Y coordinate of the child tile to check for availability.
+ * @returns {boolean} True if the child tile is available; otherwise, false.
+ */
+Cesium3DTilesUpsampleTerrainData.prototype.isChildAvailable = function (
+ _thisX,
+ _thisY,
+ _childX,
+ _childY,
+) {
+ // upsample tiles are dynamic so they don't have children
+ return false;
+};
+
+Object.defineProperties(Cesium3DTilesUpsampleTerrainData.prototype, {
+ /**
+ * An array of credits for this tile.
+ * @memberof Cesium3DTilesUpsampleTerrainData.prototype
+ * @type {Credit[]|undefined}
+ */
+ credits: {
+ get: function () {
+ return this._credits;
+ },
+ },
+ /**
+ * The water mask included in this terrain data, if any. A water mask is a rectangular
+ * Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land.
+ * Values in between 0 and 255 are allowed as well to smoothly blend between land and water.
+ * @memberof Cesium3DTilesUpsampleTerrainData.prototype
+ * @type {Uint8Array|HTMLImageElement|HTMLCanvasElement|ImageBitmap|undefined}
+ */
+ waterMask: {
+ get: function () {
+ // Note: watermask not needed because there's a fallback in another file that checks for ancestor tile water mask
+ return undefined;
+ },
+ },
+});
+
+const upsampleTaskProcessor = new TaskProcessor(
+ "upsampleVerticesFromCesium3DTilesTerrain",
+ TerrainData.maximumAsynchronousTasks,
+);
+
+/**
+ * Upsamples this terrain data for use by a descendant tile.
+ * @private
+ * @param {boolean} synchronous
+ * @param {TerrainMesh} thisMesh The mesh that is being upsampled
+ * @param {number} thisSkirtHeight The mesh's skirt height
+ * @param {Credit[]|undefined} credits The credits
+ * @param {TilingScheme} tilingScheme The tiling scheme of this terrain data.
+ * @param {number} thisX The X coordinate of this tile in the tiling scheme.
+ * @param {number} thisY The Y coordinate of this tile in the tiling scheme.
+ * @param {number} thisLevel The level of this tile in the tiling scheme.
+ * @param {number} descendantX The X coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantY The Y coordinate within the tiling scheme of the descendant tile for which we are upsampling.
+ * @param {number} descendantLevel The level within the tiling scheme of the descendant tile for which we are upsampling.
+ * @returns {Promise.|undefined} A promise for upsampled terrain data for the descendant tile, or undefined if too many asynchronous upsample operations are in progress and the request has been deferred.
+ */
+function upsampleMesh(
+ synchronous,
+ thisMesh,
+ thisSkirtHeight,
+ credits,
+ tilingScheme,
+ thisX,
+ thisY,
+ thisLevel,
+ descendantX,
+ descendantY,
+ descendantLevel,
+) {
+ //>>includeStart('debug', pragmas.debug)
+ Check.typeOf.bool("synchronous", synchronous);
+ Check.typeOf.object("thisMesh", thisMesh);
+ Check.typeOf.number("thisSkirtHeight", thisSkirtHeight);
+ Check.typeOf.object("tilingScheme", tilingScheme);
+ Check.typeOf.number("thisX", thisX);
+ Check.typeOf.number("thisY", thisY);
+ Check.typeOf.number("thisLevel", thisLevel);
+ Check.typeOf.number("descendantX", descendantX);
+ Check.typeOf.number("descendantY", descendantY);
+ Check.typeOf.number("descendantLevel", descendantLevel);
+ //>>includeEnd('debug');
+
+ const levelDifference = descendantLevel - thisLevel;
+ if (levelDifference > 1) {
+ throw new DeveloperError(
+ "Upsampling through more than one level at a time is not currently supported.",
+ );
+ }
+
+ const upsampleSkirtHeight = thisSkirtHeight * 0.5;
+ const isEastChild = thisX * 2 !== descendantX;
+ const isNorthChild = thisY * 2 === descendantY;
+ const upsampleRectangle = tilingScheme.tileXYToRectangle(
+ descendantX,
+ descendantY,
+ descendantLevel,
+ new Rectangle(),
+ );
+ const ellipsoid = tilingScheme.ellipsoid;
+
+ const options = {
+ isEastChild: isEastChild,
+ isNorthChild: isNorthChild,
+ rectangle: upsampleRectangle,
+ ellipsoid: ellipsoid,
+ skirtHeight: upsampleSkirtHeight,
+ parentVertices: thisMesh.vertices,
+ parentIndices: thisMesh.indices,
+ parentVertexCountWithoutSkirts: thisMesh.vertexCountWithoutSkirts,
+ parentIndexCountWithoutSkirts: thisMesh.indexCountWithoutSkirts,
+ parentMinimumHeight: thisMesh.minimumHeight,
+ parentMaximumHeight: thisMesh.maximumHeight,
+ parentEncoding: thisMesh.encoding,
+ };
+
+ if (synchronous) {
+ const upsampledMesh =
+ Cesium3DTilesTerrainGeometryProcessor.upsampleMesh(options);
+
+ const upsampledTerrainData = new Cesium3DTilesUpsampleTerrainData({
+ terrainMesh: upsampledMesh,
+ skirtHeight: upsampleSkirtHeight,
+ credits: credits,
+ });
+
+ return Promise.resolve(upsampledTerrainData);
+ }
+
+ const upsamplePromise = upsampleTaskProcessor.scheduleTask(options);
+
+ if (upsamplePromise === undefined) {
+ // Postponed
+ return undefined;
+ }
+
+ return upsamplePromise.then(function (taskResult) {
+ // Need to re-clone and re-wrap all buffers and complex objects to put them back into their normal state
+ const encoding = TerrainEncoding.clone(
+ taskResult.encoding,
+ new TerrainEncoding(),
+ );
+ const stride = encoding.stride;
+ const vertices = new Float32Array(taskResult.verticesBuffer);
+ const vertexCount = vertices.length / stride;
+ const vertexCountWithoutSkirts = taskResult.vertexCountWithoutSkirts;
+ // For consistency with glTF spec, 16 bit index buffer can't contain 65535
+ const SizedIndexType = vertexCount <= 65535 ? Uint16Array : Uint32Array;
+ const indices = new SizedIndexType(taskResult.indicesBuffer);
+ const westIndices = new SizedIndexType(taskResult.westIndicesBuffer);
+ const eastIndices = new SizedIndexType(taskResult.eastIndicesBuffer);
+ const southIndices = new SizedIndexType(taskResult.southIndicesBuffer);
+ const northIndices = new SizedIndexType(taskResult.northIndicesBuffer);
+ const indexCountWithoutSkirts = taskResult.indexCountWithoutSkirts;
+ const minimumHeight = taskResult.minimumHeight;
+ const maximumHeight = taskResult.maximumHeight;
+ const center = Cartesian3.clone(encoding.center, new Cartesian3());
+ const boundingSphere = BoundingSphere.clone(
+ taskResult.boundingSphere,
+ new BoundingSphere(),
+ );
+ const horizonOcclusionPoint = Cartesian3.clone(
+ taskResult.horizonOcclusionPoint,
+ new Cartesian3(),
+ );
+ const orientedBoundingBox = OrientedBoundingBox.clone(
+ taskResult.orientedBoundingBox,
+ new OrientedBoundingBox(),
+ );
+
+ const upsampledMesh = new TerrainMesh(
+ center,
+ vertices,
+ indices,
+ indexCountWithoutSkirts,
+ vertexCountWithoutSkirts,
+ minimumHeight,
+ maximumHeight,
+ boundingSphere,
+ horizonOcclusionPoint,
+ stride,
+ orientedBoundingBox,
+ encoding,
+ westIndices,
+ southIndices,
+ eastIndices,
+ northIndices,
+ );
+
+ const upsampledTerrainData = new Cesium3DTilesUpsampleTerrainData({
+ terrainMesh: upsampledMesh,
+ skirtHeight: upsampleSkirtHeight,
+ credits: credits,
+ });
+
+ return Promise.resolve(upsampledTerrainData);
+ });
+}
+
+const scratchUv0 = new Cartesian2();
+const scratchUv1 = new Cartesian2();
+const scratchUv2 = new Cartesian2();
+const scratchBary = new Cartesian3();
+
+/**
+ * Computes the terrain height at a specified longitude and latitude.
+ * @private
+ * @param {TerrainMesh} mesh The terrain mesh.
+ * @param {Rectangle} rectangle The rectangle covered by this terrain data.
+ * @param {number} longitude The longitude in radians.
+ * @param {number} latitude The latitude in radians.
+ * @returns {number} The terrain height at the specified position. If the position is outside the rectangle, this method will extrapolate the height, which is likely to be wildly incorrect for positions far outside the rectangle.
+ */
+function interpolateMeshHeight(mesh, rectangle, longitude, latitude) {
+ const u = CesiumMath.clamp(
+ (longitude - rectangle.west) / rectangle.width,
+ 0.0,
+ 1.0,
+ );
+
+ const v = CesiumMath.clamp(
+ (latitude - rectangle.south) / rectangle.height,
+ 0.0,
+ 1.0,
+ );
+
+ const { vertices, encoding, indices } = mesh;
+
+ for (let i = 0; i < mesh.indexCountWithoutSkirts; i += 3) {
+ const i0 = indices[i];
+ const i1 = indices[i + 1];
+ const i2 = indices[i + 2];
+
+ const uv0 = encoding.decodeTextureCoordinates(vertices, i0, scratchUv0);
+ const uv1 = encoding.decodeTextureCoordinates(vertices, i1, scratchUv1);
+ const uv2 = encoding.decodeTextureCoordinates(vertices, i2, scratchUv2);
+
+ const minU = Math.min(uv0.x, uv1.x, uv2.x);
+ const maxU = Math.max(uv0.x, uv1.x, uv2.x);
+ const minV = Math.min(uv0.y, uv1.y, uv2.y);
+ const maxV = Math.max(uv0.y, uv1.y, uv2.y);
+
+ if (u >= minU && u <= maxU && v >= minV && v <= maxV) {
+ const barycentric = Intersections2D.computeBarycentricCoordinates(
+ u,
+ v,
+ uv0.x,
+ uv0.y,
+ uv1.x,
+ uv1.y,
+ uv2.x,
+ uv2.y,
+ scratchBary,
+ );
+ if (
+ barycentric.x >= 0.0 &&
+ barycentric.y >= 0.0 &&
+ barycentric.z >= 0.0
+ ) {
+ const h0 = encoding.decodeHeight(vertices, i0);
+ const h1 = encoding.decodeHeight(vertices, i1);
+ const h2 = encoding.decodeHeight(vertices, i2);
+ const height =
+ barycentric.x * h0 + barycentric.y * h1 + barycentric.z * h2;
+ return height;
+ }
+ }
+ }
+
+ // Position does not lie in any triangle in this mesh.
+ return 0.0;
+}
+
+export default Cesium3DTilesTerrainData;
diff --git a/packages/engine/Source/Core/Cesium3DTilesTerrainGeometryProcessor.js b/packages/engine/Source/Core/Cesium3DTilesTerrainGeometryProcessor.js
new file mode 100644
index 000000000000..e2f0334309af
--- /dev/null
+++ b/packages/engine/Source/Core/Cesium3DTilesTerrainGeometryProcessor.js
@@ -0,0 +1,1866 @@
+import { MeshoptDecoder } from "meshoptimizer";
+import AttributeCompression from "./AttributeCompression.js";
+import Axis from "../Scene/Axis.js";
+import AxisAlignedBoundingBox from "./AxisAlignedBoundingBox.js";
+import binarySearch from "./binarySearch.js";
+import BoundingSphere from "./BoundingSphere.js";
+import Cartesian2 from "./Cartesian2.js";
+import Cartesian3 from "./Cartesian3.js";
+import Cartographic from "./Cartographic.js";
+import CesiumMath from "./Math.js";
+import Check from "./Check.js";
+import ComponentDatatype from "./ComponentDatatype.js";
+import Ellipsoid from "./Ellipsoid.js";
+import EllipsoidalOccluder from "./EllipsoidalOccluder.js";
+import Frozen from "./Frozen.js";
+import Matrix4 from "./Matrix4.js";
+import OrientedBoundingBox from "./OrientedBoundingBox.js";
+import Rectangle from "./Rectangle.js";
+import TerrainEncoding from "./TerrainEncoding.js";
+import TerrainMesh from "./TerrainMesh.js";
+import TerrainProvider from "./TerrainProvider.js";
+import Transforms from "./Transforms.js";
+import WebMercatorProjection from "./WebMercatorProjection.js";
+
+/**
+ * Contains functions to create a mesh from 3D Tiles terrain data.
+ *
+ * @namespace Cesium3DTilesTerrainGeometryProcessor
+ *
+ * @private
+ */
+const Cesium3DTilesTerrainGeometryProcessor = {};
+
+/**
+ * Contains information about geometry-related vertex arrays in a glTF asset.
+ * @private
+ * @typedef GltfInfo
+ *
+ * @property {Float32Array} positions The decoded position attributes.
+ * @property {Float32Array|undefined} normals The decoded normal attributes, or undefined if the glTF has no normals.
+ * @property {Uint16Array|Uint32Array} indices The decoded indices.
+ * @property {Uint16Array|Uint32Array} edgeIndicesWest The edge indices along the West side of the tile.
+ * @property {Uint16Array|Uint32Array} edgeIndicesSouth The edge indices along the South side of the tile.
+ * @property {Uint16Array|Uint32Array} edgeIndicesEast The edge indices along the East side of the tile.
+ * @property {Uint16Array|Uint32Array} edgeIndicesNorth The edge indices along the North side of the tile.
+ */
+
+const scratchGltfInfo = {
+ positions: undefined,
+ normals: undefined,
+ indices: undefined,
+ edgeIndicesWest: undefined,
+ edgeIndicesSouth: undefined,
+ edgeIndicesEast: undefined,
+ edgeIndicesNorth: undefined,
+};
+
+const scratchCenterCartographic = new Cartographic();
+const scratchCenterCartesian = new Cartesian3();
+const scratchEnuToEcef = new Matrix4();
+const scratchEcefToEnu = new Matrix4();
+const scratchTilesetTransform = new Matrix4();
+const scratchMinimumPositionENU = new Cartesian3();
+const scratchMaximumPositionENU = new Cartesian3();
+const scratchPosLocal = new Cartesian3();
+const scratchPosEcef = new Cartesian3();
+const scratchCartographic = new Cartographic();
+const scratchUV = new Cartesian2();
+const scratchNormal = new Cartesian3();
+const scratchNormalOct = new Cartesian2();
+const scratchGeodeticSurfaceNormal = new Cartesian3();
+const scratchPosEnu = new Cartesian3();
+
+/**
+ * Compares two edge indices for sorting.
+ * @private
+ * @param {number} a The first edge index.
+ * @param {number} b The second edge index.
+ * @returns {number} A negative number if a is less than b, a positive number if a is greater than b, or zero if they are equal.
+ */
+const sortedEdgeCompare = function (a, b) {
+ return a - b;
+};
+
+/**
+ * @typedef {object} Cesium3DTilesTerrainGeometryProcessor.CreateMeshOptions
+ * @property {Ellipsoid} ellipsoid The ellipsoid.
+ * @property {Rectangle} rectangle The rectangle covered by the tile.
+ * @property {boolean} hasVertexNormals true if the tile has vertex normals.
+ * @property {boolean} hasWebMercatorT true if the tile has Web Mercator T coordinates.
+ * @property {Object.} gltf The glTF JSON of the tile.
+ * @property {number} minimumHeight The minimum height of the tile.
+ * @property {number} maximumHeight The maximum height of the tile.
+ * @property {BoundingSphere} boundingSphere The bounding sphere of the tile.
+ * @property {OrientedBoundingBox} orientedBoundingBox The oriented bounding box of the tile.
+ * @property {Cartesian3} horizonOcclusionPoint The horizon occlusion point of the tile.
+ * @property {number} skirtHeight The height of the skirts.
+ * @property {number} [exaggeration=1.0] The scale used to exaggerate the terrain.
+ * @property {number} [exaggerationRelativeHeight=0.0] The height relative to which terrain is exaggerated.
+ */
+
+/**
+ * Creates a {@link TerrainMesh} from this terrain data.
+ * @function
+ *
+ * @private
+ *
+ * @param {Cesium3DTilesTerrainGeometryProcessor.CreateMeshOptions} options An object describing options for mesh creation.
+ * @returns {Promise.} A promise to a terrain mesh.
+ */
+Cesium3DTilesTerrainGeometryProcessor.createMesh = async function (options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+ const {
+ exaggeration = 1.0,
+ exaggerationRelativeHeight = 0.0,
+ hasVertexNormals,
+ hasWebMercatorT,
+ gltf,
+ minimumHeight,
+ maximumHeight,
+ skirtHeight,
+ } = options;
+
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("options.ellipsoid", options.ellipsoid);
+ Check.typeOf.object("options.rectangle", options.rectangle);
+ Check.typeOf.bool("options.hasVertexNormals", hasVertexNormals);
+ Check.typeOf.bool("options.hasWebMercatorT", hasWebMercatorT);
+ Check.typeOf.object("options.gltf", gltf);
+ Check.typeOf.number("options.minimumHeight", minimumHeight);
+ Check.typeOf.number("options.maximumHeight", maximumHeight);
+ Check.typeOf.object("options.boundingSphere", options.boundingSphere);
+ Check.typeOf.object(
+ "options.orientedBoundingBox",
+ options.orientedBoundingBox,
+ );
+ Check.typeOf.object(
+ "options.horizonOcclusionPoint",
+ options.horizonOcclusionPoint,
+ );
+ Check.typeOf.number("options.skirtHeight", skirtHeight);
+ //>>includeEnd('debug');
+
+ const hasExaggeration = exaggeration !== 1.0;
+ const hasGeodeticSurfaceNormals = hasExaggeration;
+
+ const boundingSphere = BoundingSphere.clone(
+ options.boundingSphere,
+ new BoundingSphere(),
+ );
+ const orientedBoundingBox = OrientedBoundingBox.clone(
+ options.orientedBoundingBox,
+ new OrientedBoundingBox(),
+ );
+ const horizonOcclusionPoint = Cartesian3.clone(
+ options.horizonOcclusionPoint,
+ new Cartesian3(),
+ );
+ const ellipsoid = Ellipsoid.clone(options.ellipsoid, new Ellipsoid());
+ const rectangle = Rectangle.clone(options.rectangle, new Rectangle());
+
+ const hasMeshOptCompression =
+ gltf.extensionsRequired !== undefined &&
+ gltf.extensionsRequired.indexOf("EXT_meshopt_compression") !== -1;
+
+ const decoderPromise = hasMeshOptCompression
+ ? MeshoptDecoder.ready
+ : Promise.resolve(undefined);
+
+ await decoderPromise;
+
+ const tileMinLongitude = rectangle.west;
+ const tileMinLatitude = rectangle.south;
+ const tileMaxLatitude = rectangle.north;
+ const tileLengthLongitude = rectangle.width;
+ const tileLengthLatitude = rectangle.height;
+
+ const approximateCenterCartographic = Rectangle.center(
+ rectangle,
+ scratchCenterCartographic,
+ );
+ approximateCenterCartographic.height = 0.5 * (minimumHeight + maximumHeight);
+
+ const approximateCenterPosition = Cartographic.toCartesian(
+ approximateCenterCartographic,
+ ellipsoid,
+ scratchCenterCartesian,
+ );
+
+ const enuToEcef = Transforms.eastNorthUpToFixedFrame(
+ approximateCenterPosition,
+ ellipsoid,
+ scratchEnuToEcef,
+ );
+ const ecefToEnu = Matrix4.inverseTransformation(enuToEcef, scratchEcefToEnu);
+
+ let tilesetTransform = Matrix4.unpack(
+ gltf.nodes[0].matrix,
+ 0,
+ scratchTilesetTransform,
+ );
+
+ tilesetTransform = Matrix4.multiply(
+ Axis.Y_UP_TO_Z_UP,
+ tilesetTransform,
+ tilesetTransform,
+ );
+
+ const gltfInfo = decodeGltf(gltf, hasVertexNormals, scratchGltfInfo);
+
+ const skirtVertexCount = TerrainProvider.getSkirtVertexCount(
+ gltfInfo.edgeIndicesWest,
+ gltfInfo.edgeIndicesSouth,
+ gltfInfo.edgeIndicesEast,
+ gltfInfo.edgeIndicesNorth,
+ );
+
+ const positionsLocalWithoutSkirts = gltfInfo.positions;
+ const normalsWithoutSkirts = gltfInfo.normals;
+ const indicesWithoutSkirts = gltfInfo.indices;
+ const vertexCountWithoutSkirts = positionsLocalWithoutSkirts.length / 3;
+ const vertexCountWithSkirts = vertexCountWithoutSkirts + skirtVertexCount;
+ const indexCountWithoutSkirts = indicesWithoutSkirts.length;
+ const skirtIndexCount =
+ TerrainProvider.getSkirtIndexCountWithFilledCorners(skirtVertexCount);
+
+ // For consistency with glTF spec, 16 bit index buffer can't contain 65535
+ const SizedIndexTypeWithSkirts =
+ vertexCountWithSkirts <= 65535 ? Uint16Array : Uint32Array;
+ // Make the index buffer large enough that we can add in the skirt indices later
+ const indexBufferWithSkirts = new SizedIndexTypeWithSkirts(
+ indexCountWithoutSkirts + skirtIndexCount,
+ );
+ indexBufferWithSkirts.set(indicesWithoutSkirts);
+
+ const westIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesWest);
+ const southIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesSouth);
+ const eastIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesEast);
+ const northIndices = new SizedIndexTypeWithSkirts(gltfInfo.edgeIndicesNorth);
+
+ const sortedWestIndices = new SizedIndexTypeWithSkirts(westIndices).sort();
+ const sortedSouthIndices = new SizedIndexTypeWithSkirts(southIndices).sort();
+ const sortedEastIndices = new SizedIndexTypeWithSkirts(eastIndices).sort();
+ const sortedNorthIndices = new SizedIndexTypeWithSkirts(northIndices).sort();
+
+ const southMercatorAngle =
+ WebMercatorProjection.geodeticLatitudeToMercatorAngle(tileMinLatitude);
+ const northMercatorAngle =
+ WebMercatorProjection.geodeticLatitudeToMercatorAngle(tileMaxLatitude);
+
+ const oneOverMercatorHeight = 1.0 / (northMercatorAngle - southMercatorAngle);
+
+ // Use a terrain encoding without quantization.
+ // This is just an easier way to save intermediate state
+ let minPosEnu = Cartesian3.fromElements(
+ Number.POSITIVE_INFINITY,
+ Number.POSITIVE_INFINITY,
+ Number.POSITIVE_INFINITY,
+ scratchMinimumPositionENU,
+ );
+ let maxPosEnu = Cartesian3.fromElements(
+ Number.NEGATIVE_INFINITY,
+ Number.NEGATIVE_INFINITY,
+ Number.NEGATIVE_INFINITY,
+ scratchMaximumPositionENU,
+ );
+ const tempTerrainEncoding = new TerrainEncoding(
+ boundingSphere.center,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ hasVertexNormals,
+ hasWebMercatorT,
+ hasGeodeticSurfaceNormals,
+ exaggeration,
+ exaggerationRelativeHeight,
+ );
+
+ const tempBufferStride = tempTerrainEncoding.stride;
+ const tempBuffer = new Float32Array(vertexCountWithSkirts * tempBufferStride);
+ let tempBufferOffset = 0;
+
+ for (let i = 0; i < vertexCountWithoutSkirts; i++) {
+ const posLocal = Cartesian3.unpack(
+ positionsLocalWithoutSkirts,
+ i * 3,
+ scratchPosLocal,
+ );
+
+ const posECEF = Matrix4.multiplyByPoint(
+ tilesetTransform,
+ posLocal,
+ scratchPosEcef,
+ );
+
+ const cartographic = Cartographic.fromCartesian(
+ posECEF,
+ ellipsoid,
+ scratchCartographic,
+ );
+
+ const { longitude, latitude, height } = cartographic;
+
+ // If a vertex is an edge vertex we already know its exact UV and don't need to derive it from the position (which can have accuracy issues).
+
+ let u = (longitude - tileMinLongitude) / tileLengthLongitude;
+ let v = (latitude - tileMinLatitude) / tileLengthLatitude;
+
+ // Clamp the UVs to the valid range
+ // This should only happen when the cartesian to cartographic conversion introduces error on a point that is already very close the edge
+ u = CesiumMath.clamp(u, 0.0, 1.0);
+ v = CesiumMath.clamp(v, 0.0, 1.0);
+
+ if (binarySearch(sortedWestIndices, i, sortedEdgeCompare) >= 0) {
+ u = 0.0;
+ } else if (binarySearch(sortedEastIndices, i, sortedEdgeCompare) >= 0) {
+ u = 1.0;
+ }
+
+ if (binarySearch(sortedSouthIndices, i, sortedEdgeCompare) >= 0) {
+ v = 0.0;
+ } else if (binarySearch(sortedNorthIndices, i, sortedEdgeCompare) >= 0) {
+ v = 1.0;
+ }
+
+ const uv = Cartesian2.fromElements(u, v, scratchUV);
+
+ let normalOct;
+ if (hasVertexNormals) {
+ let normal = Cartesian3.unpack(
+ normalsWithoutSkirts,
+ i * 3,
+ scratchNormal,
+ );
+ normal = Matrix4.multiplyByPointAsVector(
+ tilesetTransform,
+ normal,
+ scratchNormal,
+ );
+
+ normal = Cartesian3.normalize(normal, scratchNormal);
+
+ normalOct = AttributeCompression.octEncode(normal, scratchNormalOct);
+ }
+
+ let webMercatorT;
+ if (hasWebMercatorT) {
+ const mercatorAngle =
+ WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude);
+ webMercatorT =
+ (mercatorAngle - southMercatorAngle) * oneOverMercatorHeight;
+ }
+
+ let geodeticSurfaceNormal;
+ if (hasGeodeticSurfaceNormals) {
+ geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal(
+ posECEF,
+ scratchGeodeticSurfaceNormal,
+ );
+ }
+
+ tempBufferOffset = tempTerrainEncoding.encode(
+ tempBuffer,
+ tempBufferOffset,
+ posECEF,
+ uv,
+ height,
+ normalOct,
+ webMercatorT,
+ geodeticSurfaceNormal,
+ );
+
+ const posEnu = Matrix4.multiplyByPoint(ecefToEnu, posECEF, scratchPosEnu);
+ minPosEnu = Cartesian3.minimumByComponent(posEnu, minPosEnu, minPosEnu);
+ maxPosEnu = Cartesian3.maximumByComponent(posEnu, maxPosEnu, maxPosEnu);
+ }
+
+ const mesh = new TerrainMesh(
+ Cartesian3.clone(tempTerrainEncoding.center, new Cartesian3()),
+ tempBuffer,
+ indexBufferWithSkirts,
+ indexCountWithoutSkirts,
+ vertexCountWithoutSkirts,
+ minimumHeight,
+ maximumHeight,
+ BoundingSphere.clone(boundingSphere, new BoundingSphere()),
+ Cartesian3.clone(horizonOcclusionPoint, new Cartesian3()),
+ tempBufferStride,
+ OrientedBoundingBox.clone(orientedBoundingBox, new OrientedBoundingBox()),
+ tempTerrainEncoding,
+ westIndices,
+ southIndices,
+ eastIndices,
+ northIndices,
+ );
+
+ addSkirtsToMesh(
+ mesh,
+ rectangle,
+ ellipsoid,
+ minPosEnu,
+ maxPosEnu,
+ enuToEcef,
+ ecefToEnu,
+ skirtHeight,
+ );
+
+ return Promise.resolve(mesh);
+};
+
+const scratchMinUV = new Cartesian2();
+const scratchMaxUV = new Cartesian2();
+const scratchPolygonIndices = new Array(6);
+const scratchUvA = new Cartesian2();
+const scratchUvB = new Cartesian2();
+const scratchUvC = new Cartesian2();
+const scratchNormalA = new Cartesian3();
+const scratchNormalB = new Cartesian3();
+const scratchNormalC = new Cartesian3();
+const scratchCenterCartographicUpsample = new Cartographic();
+const scratchCenterCartesianUpsample = new Cartesian3();
+const scratchCartographicSkirt = new Cartographic();
+const scratchCartographicUpsample = new Cartographic();
+const scratchPosEcefSkirt = new Cartesian3();
+const scratchPosEcefUpsample = new Cartesian3();
+const scratchPosEnuSkirt = new Cartesian3();
+const scratchPosEnuUpsample = new Cartesian3();
+const scratchMinimumPositionENUSkirt = new Cartesian3();
+const scratchMaximumPositionENUSkirt = new Cartesian3();
+const scratchMinimumPositionENUUpsample = new Cartesian3();
+const scratchMaximumPositionENUUpsample = new Cartesian3();
+const scratchEnuToEcefUpsample = new Matrix4();
+const scratchEcefToEnuUpsample = new Matrix4();
+const scratchUVSkirt = new Cartesian2();
+const scratchUVUpsample = new Cartesian2();
+const scratchHorizonOcclusionPoint = new Cartesian3();
+const scratchBoundingSphere = new BoundingSphere();
+const scratchOrientedBoundingBox = new OrientedBoundingBox();
+const scratchAABBEnuSkirt = new AxisAlignedBoundingBox();
+const scratchNormalUpsample = new Cartesian3();
+const scratchNormalOctSkirt = new Cartesian2();
+const scratchNormalOctUpsample = new Cartesian2();
+const scratchGeodeticSurfaceNormalSkirt = new Cartesian3();
+const scratchGeodeticSurfaceNormalUpsample = new Cartesian3();
+
+/**
+ * Decode the position attributes from a glTF object.
+ * @private
+ * @param {Object.} gltf The glTF JSON.
+ * @returns {Float32Array} The decoded positions, as a flattened array of x, y, z values.
+ */
+function decodePositions(gltf) {
+ const primitive = gltf.meshes[0].primitives[0];
+ const accessor = gltf.accessors[primitive.attributes["POSITION"]];
+ const bufferView = gltf.bufferViews[accessor.bufferView];
+ const positionCount = accessor.count;
+
+ const bufferViewMeshOpt = bufferView.extensions
+ ? bufferView.extensions["EXT_meshopt_compression"]
+ : undefined;
+
+ if (bufferViewMeshOpt === undefined) {
+ const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source;
+
+ return new Float32Array(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the start of the glb
+ (bufferView.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ positionCount * 3,
+ );
+ }
+
+ const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source;
+
+ const compressedBuffer = new Uint8Array(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the start of the glb
+ (bufferViewMeshOpt.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ bufferViewMeshOpt.byteLength,
+ );
+
+ const positionByteLength = bufferViewMeshOpt.byteStride;
+ const PositionType = positionByteLength === 4 ? Uint8Array : Uint16Array;
+ const positionsResult = new PositionType(positionCount * 4);
+ MeshoptDecoder.decodeVertexBuffer(
+ new Uint8Array(positionsResult.buffer),
+ positionCount,
+ positionByteLength,
+ compressedBuffer,
+ );
+
+ const positionStorageValueMax =
+ (1 << (positionsResult.BYTES_PER_ELEMENT * 8)) - 1;
+ const positions = new Float32Array(positionCount * 3);
+ for (let p = 0; p < positionCount; p++) {
+ // only the first 3 components are used
+ positions[p * 3 + 0] = positionsResult[p * 4 + 0] / positionStorageValueMax;
+ positions[p * 3 + 1] = positionsResult[p * 4 + 1] / positionStorageValueMax;
+ positions[p * 3 + 2] = positionsResult[p * 4 + 2] / positionStorageValueMax;
+ // fourth component is not used
+ }
+ return positions;
+}
+
+/**
+ * Decode the normal attributes from a glTF object.
+ * @private
+ * @param {Object.} gltf The glTF JSON.
+ * @returns {Float32Array} The decoded normals, as a flattened array of x, y, z values.
+ */
+function decodeNormals(gltf) {
+ const primitive = gltf.meshes[0].primitives[0];
+ const accessor = gltf.accessors[primitive.attributes["NORMAL"]];
+ const bufferView = gltf.bufferViews[accessor.bufferView];
+ const normalCount = accessor.count;
+
+ const bufferViewMeshOpt = bufferView.extensions
+ ? bufferView.extensions["EXT_meshopt_compression"]
+ : undefined;
+
+ if (bufferViewMeshOpt === undefined) {
+ const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source;
+
+ return new Float32Array(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the start of the glb
+ (bufferView.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ normalCount * 3,
+ );
+ }
+
+ const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source;
+
+ const compressedBuffer = new Uint8Array(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the start of the glb
+ (bufferViewMeshOpt.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ bufferViewMeshOpt.byteLength,
+ );
+
+ const normalByteLength = bufferViewMeshOpt.byteStride;
+ const normalsResult = new Int8Array(normalCount * normalByteLength);
+
+ MeshoptDecoder.decodeVertexBuffer(
+ new Uint8Array(normalsResult.buffer),
+ normalCount,
+ normalByteLength,
+ compressedBuffer,
+ );
+
+ const normals = new Float32Array(normalCount * 3);
+ for (let i = 0; i < normalCount; i++) {
+ // AttributeCompression.octDecodeInRange is not compatible with KHR_mesh_quantization, so do the oct decode manually
+ // The quantization puts values between -127 and +127, but clamp in case it has -128
+ // The third component is unused until normals support non-8-bit quantization
+ // The fourth component is always unused
+ let octX = Math.max(normalsResult[i * 4 + 0] / 127.0, -1.0);
+ let octY = Math.max(normalsResult[i * 4 + 1] / 127.0, -1.0);
+ const octZ = 1.0 - (Math.abs(octX) + Math.abs(octY));
+
+ if (octZ < 0.0) {
+ const oldX = octX;
+ const oldY = octY;
+ octX = (1.0 - Math.abs(oldY)) * CesiumMath.signNotZero(oldX);
+ octY = (1.0 - Math.abs(oldX)) * CesiumMath.signNotZero(oldY);
+ }
+
+ let normal = scratchNormal;
+ normal.x = octX;
+ normal.y = octY;
+ normal.z = octZ;
+ normal = Cartesian3.normalize(normal, scratchNormal);
+
+ normals[i * 3 + 0] = normal.x;
+ normals[i * 3 + 1] = normal.y;
+ normals[i * 3 + 2] = normal.z;
+ }
+ return normals;
+}
+
+/**
+ * Decode the index attributes from a glTF object.
+ * @private
+ * @param {Object.} gltf The glTF JSON.
+ * @returns {Uint16Array|Uint32Array} An array of indices.
+ */
+function decodeIndices(gltf) {
+ const primitive = gltf.meshes[0].primitives[0];
+ const accessor = gltf.accessors[primitive.indices];
+ const bufferView = gltf.bufferViews[accessor.bufferView];
+ const indexCount = accessor.count;
+
+ const SizedIndexType =
+ accessor.componentType === ComponentDatatype.UNSIGNED_SHORT
+ ? Uint16Array
+ : Uint32Array;
+
+ const bufferViewMeshOpt = bufferView.extensions
+ ? bufferView.extensions["EXT_meshopt_compression"]
+ : undefined;
+
+ if (bufferViewMeshOpt === undefined) {
+ const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source;
+ return new SizedIndexType(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the glb
+ (bufferView.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ indexCount,
+ );
+ }
+
+ const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source;
+ const compressedBuffer = new Uint8Array(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the start of the glb
+ (bufferViewMeshOpt.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ bufferViewMeshOpt.byteLength,
+ );
+
+ const indices = new SizedIndexType(indexCount);
+ MeshoptDecoder.decodeIndexBuffer(
+ new Uint8Array(indices.buffer),
+ indexCount,
+ bufferViewMeshOpt.byteStride,
+ compressedBuffer,
+ );
+ return indices;
+}
+
+/**
+ * Decode the edge index attributes from a glTF object.
+ * @private
+ * @param {Object.} gltf The glTF JSON.
+ * @param {string} name The name of the edge indices to decode.
+ * @returns {Uint16Array|Uint32Array} An array of edge indices.
+ */
+function decodeEdgeIndices(gltf, name) {
+ const primitive = gltf.meshes[0].primitives[0];
+ const accessor = gltf.accessors[primitive.extensions.CESIUM_tile_edges[name]];
+ const bufferView = gltf.bufferViews[accessor.bufferView];
+
+ const indexCount = accessor.count;
+ const SizedIndexType =
+ accessor.componentType === ComponentDatatype.UNSIGNED_SHORT
+ ? Uint16Array
+ : Uint32Array;
+
+ const bufferViewMeshOpt = bufferView.extensions
+ ? bufferView.extensions["EXT_meshopt_compression"]
+ : undefined;
+
+ if (bufferViewMeshOpt === undefined) {
+ const buffer = gltf.buffers[bufferView.buffer].extras._pipeline.source;
+
+ return new SizedIndexType(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the glb
+ (bufferView.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ indexCount,
+ );
+ }
+
+ const buffer = gltf.buffers[bufferViewMeshOpt.buffer].extras._pipeline.source;
+ const compressedBuffer = new Uint8Array(
+ buffer.buffer,
+ buffer.byteOffset + // offset from the start of the glb
+ (bufferViewMeshOpt.byteOffset ?? 0) +
+ (accessor.byteOffset ?? 0),
+ bufferViewMeshOpt.byteLength,
+ );
+
+ const indices = new SizedIndexType(indexCount);
+ const indexByteLength = bufferViewMeshOpt.byteStride;
+ MeshoptDecoder.decodeIndexSequence(
+ new Uint8Array(indices.buffer),
+ indexCount,
+ indexByteLength,
+ compressedBuffer,
+ );
+ return indices;
+}
+
+/**
+ * Decodes geometry-related vertex arrays from a glTF asset.
+ * @private
+ * @param {Object.} gltf The glTF JSON.
+ * @param {boolean} hasNormals true if the glTF has normal attributes.
+ * @param {GltfInfo} result The object to store the decoded arrays.
+ * @returns {GltfInfo} The decoded geometry info.
+ */
+function decodeGltf(gltf, hasNormals, result) {
+ result.positions = decodePositions(gltf);
+ result.normals = hasNormals ? decodeNormals(gltf) : undefined;
+ result.indices = decodeIndices(gltf);
+ result.edgeIndicesWest = decodeEdgeIndices(gltf, "left");
+ result.edgeIndicesSouth = decodeEdgeIndices(gltf, "bottom");
+ result.edgeIndicesEast = decodeEdgeIndices(gltf, "right");
+ result.edgeIndicesNorth = decodeEdgeIndices(gltf, "top");
+ return result;
+}
+
+/**
+ * @typedef {object} Cesium3DTilesTerrainGeometryProcessor.UpsampleMeshOptions
+ * @property {boolean} isEastChild true if the tile is the east child of its parent.
+ * @property {boolean} isNorthChild true if the tile is the north child of its parent.
+ * @property {Rectangle} rectangle The rectangle covered by the tile.
+ * @property {Ellipsoid} ellipsoid The ellipsoid.
+ * @property {number} skirtHeight The height of the skirts.
+ * @property {Float32Array} parentVertices The parent tile's vertex buffer.
+ * @property {Uint8Array|Uint16Array|Uint32Array} parentIndices The parent tile's index buffer.
+ * @property {number} parentVertexCountWithoutSkirts The number of vertices in the parent tile excluding skirts.
+ * @property {number} parentIndexCountWithoutSkirts The number of indices in the parent tile excluding skirts.
+ * @property {number} parentMinimumHeight The minimum height of the parent tile.
+ * @property {number} parentMaximumHeight The maximum height of the parent tile.
+ * @property {TerrainEncoding} parentEncoding The parent tile's terrain encoding.
+ */
+
+/**
+ * Upsamples a parent tile's mesh to create a higher-detail child tile's mesh.
+ *
+ * Overview: Only include triangles that are inside the UV clipping region.
+ * If a triangle is partly outside, it will be clipped at the border.
+ * The clipping function returns a polygon where each point is a barycentric coordinate of the input triangle.
+ * Most of the time the triangle will not be clipped, so the polygon will be the three barycentric coordinates of the input triangle.
+ * If the triangle is completely outside the clipping region, the polygon will have no points and will be ignored.
+ * If the triangle is clipped, the polygon will have between four and six points and needs to be triangulated.
+ * Vertex data for points that fall inside the triangle will be interpolated using the barycentric coordinates.
+ * Each vertex in the polygon is added to the new vertex list, with some special handling to avoid duplicate points between triangles.
+ *
+ * @private
+ * @param {Cesium3DTilesTerrainGeometryProcessor.UpsampleMeshOptions} options An object describing options for mesh upsampling.
+ * @returns {TerrainMesh} The upsampled terrain mesh.
+ */
+Cesium3DTilesTerrainGeometryProcessor.upsampleMesh = function (options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ const {
+ isEastChild,
+ isNorthChild,
+ parentMinimumHeight,
+ parentMaximumHeight,
+ skirtHeight,
+ } = options;
+
+ //>>includeStart('debug', pragmas.debug)
+ Check.typeOf.bool("options.isEastChild", isEastChild);
+ Check.typeOf.bool("options.isNorthChild", isNorthChild);
+ Check.typeOf.object("options.parentVertices", options.parentVertices);
+ Check.typeOf.object("options.parentIndices", options.parentIndices);
+ Check.typeOf.number(
+ "options.parentVertexCountWithoutSkirts",
+ options.parentVertexCountWithoutSkirts,
+ );
+ Check.typeOf.number(
+ "options.parentIndexCountWithoutSkirts",
+ options.parentIndexCountWithoutSkirts,
+ );
+ Check.typeOf.number("options.parentMinimumHeight", parentMinimumHeight);
+ Check.typeOf.number("options.parentMaximumHeight", parentMaximumHeight);
+ Check.typeOf.object("options.parentEncoding", options.parentEncoding);
+ Check.typeOf.object("options.rectangle", options.rectangle);
+ Check.typeOf.number("options.skirtHeight", skirtHeight);
+ Check.typeOf.object("options.ellipsoid", options.ellipsoid);
+ //>>includeEnd('debug');
+
+ const indexCount = options.parentIndexCountWithoutSkirts;
+ const indices = options.parentIndices;
+ const vertexCount = options.parentVertexCountWithoutSkirts;
+ const vertexBuffer = options.parentVertices;
+ const encoding = TerrainEncoding.clone(
+ options.parentEncoding,
+ new TerrainEncoding(),
+ );
+ const hasVertexNormals = encoding.hasVertexNormals;
+ const hasWebMercatorT = encoding.hasWebMercatorT;
+ const exaggeration = encoding.exaggeration;
+ const exaggerationRelativeHeight = encoding.exaggerationRelativeHeight;
+ const hasExaggeration = exaggeration !== 1.0;
+ const hasGeodeticSurfaceNormals = hasExaggeration;
+ const upsampleRectangle = Rectangle.clone(options.rectangle, new Rectangle());
+ const ellipsoid = Ellipsoid.clone(options.ellipsoid);
+
+ const upsampledTriIDs = [];
+ const upsampledUVs = [];
+ const upsampledBarys = [];
+ const upsampledIndices = [];
+ const upsampledWestIndices = [];
+ const upsampledSouthIndices = [];
+ const upsampledEastIndices = [];
+ const upsampledNorthIndices = [];
+
+ clipTileFromQuadrant(
+ isEastChild,
+ isNorthChild,
+ indexCount,
+ indices,
+ vertexCount,
+ vertexBuffer,
+ encoding,
+ upsampledIndices,
+ upsampledWestIndices,
+ upsampledSouthIndices,
+ upsampledEastIndices,
+ upsampledNorthIndices,
+ upsampledTriIDs,
+ upsampledBarys,
+ upsampledUVs,
+ );
+
+ // Don't know the min and max height of the upsampled positions yet,
+ // so calculate a center point from the parent's min and max height
+ const approximateCenterCartographic = Rectangle.center(
+ upsampleRectangle,
+ scratchCenterCartographicUpsample,
+ );
+ approximateCenterCartographic.height =
+ 0.5 * (parentMinimumHeight + parentMaximumHeight);
+ const approximateCenterPosition = Cartographic.toCartesian(
+ approximateCenterCartographic,
+ ellipsoid,
+ scratchCenterCartesianUpsample,
+ );
+
+ const upsampledVertexCountWithoutSkirts = upsampledTriIDs.length;
+ const upsampledTerrainEncoding = new TerrainEncoding(
+ approximateCenterPosition,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ hasVertexNormals,
+ hasWebMercatorT,
+ hasGeodeticSurfaceNormals,
+ exaggeration,
+ exaggerationRelativeHeight,
+ );
+ const upsampledVertexBufferStride = upsampledTerrainEncoding.stride;
+
+ const upsampledSkirtVertexCount = TerrainProvider.getSkirtVertexCount(
+ upsampledWestIndices,
+ upsampledSouthIndices,
+ upsampledEastIndices,
+ upsampledNorthIndices,
+ );
+ const upsampledVertexCountWithSkirts =
+ upsampledVertexCountWithoutSkirts + upsampledSkirtVertexCount;
+ const upsampledIndexCountWithoutSkirts = upsampledIndices.length;
+ const upsampledSkirtIndexCount =
+ TerrainProvider.getSkirtIndexCountWithFilledCorners(
+ upsampledSkirtVertexCount,
+ );
+ const upsampledIndexCountWithSkirts =
+ upsampledIndexCountWithoutSkirts + upsampledSkirtIndexCount;
+ // For consistency with glTF spec, 16 bit index buffer can't contain 65535
+ const SizedIndexTypeWithSkirts =
+ upsampledVertexCountWithSkirts <= 65535 ? Uint16Array : Uint32Array;
+ const upsampledIndexBuffer = new SizedIndexTypeWithSkirts(
+ upsampledIndexCountWithSkirts,
+ );
+ upsampledIndexBuffer.set(upsampledIndices);
+
+ const upsampledWestIndicesBuffer = new SizedIndexTypeWithSkirts(
+ upsampledWestIndices,
+ );
+
+ const upsampledSouthIndicesBuffer = new SizedIndexTypeWithSkirts(
+ upsampledSouthIndices,
+ );
+
+ const upsampledEastIndicesBuffer = new SizedIndexTypeWithSkirts(
+ upsampledEastIndices,
+ );
+
+ const upsampledNorthIndicesBuffer = new SizedIndexTypeWithSkirts(
+ upsampledNorthIndices,
+ );
+
+ const upsampledVertexBuffer = new Float32Array(
+ upsampledVertexCountWithSkirts * upsampledVertexBufferStride,
+ );
+ let upsampledVertexBufferOffset = 0;
+
+ const enuToEcef = Transforms.eastNorthUpToFixedFrame(
+ approximateCenterPosition,
+ ellipsoid,
+ scratchEnuToEcefUpsample,
+ );
+ const ecefToEnu = Matrix4.inverseTransformation(
+ enuToEcef,
+ scratchEcefToEnuUpsample,
+ );
+
+ const minimumLongitude = upsampleRectangle.west;
+ const maximumLongitude = upsampleRectangle.east;
+ const minimumLatitude = upsampleRectangle.south;
+ const maximumLatitude = upsampleRectangle.north;
+
+ const southMercatorAngle =
+ WebMercatorProjection.geodeticLatitudeToMercatorAngle(minimumLatitude);
+ const northMercatorAngle =
+ WebMercatorProjection.geodeticLatitudeToMercatorAngle(maximumLatitude);
+ const oneOverMercatorHeight = 1.0 / (northMercatorAngle - southMercatorAngle);
+
+ let minimumHeight = Number.POSITIVE_INFINITY;
+ let maximumHeight = Number.NEGATIVE_INFINITY;
+
+ let minPosEnu = Cartesian3.fromElements(
+ Number.POSITIVE_INFINITY,
+ Number.POSITIVE_INFINITY,
+ Number.POSITIVE_INFINITY,
+ scratchMinimumPositionENUUpsample,
+ );
+ let maxPosEnu = Cartesian3.fromElements(
+ Number.NEGATIVE_INFINITY,
+ Number.NEGATIVE_INFINITY,
+ Number.NEGATIVE_INFINITY,
+ scratchMaximumPositionENUUpsample,
+ );
+
+ for (let i = 0; i < upsampledVertexCountWithoutSkirts; i++) {
+ const triId = upsampledTriIDs[i];
+ const indexA = indices[triId * 3 + 0];
+ const indexB = indices[triId * 3 + 1];
+ const indexC = indices[triId * 3 + 2];
+
+ const uv = scratchUVUpsample;
+ uv.x = upsampledUVs[i * 2 + 0];
+ uv.y = upsampledUVs[i * 2 + 1];
+ const u = uv.x;
+ const v = uv.y;
+
+ const baryA = upsampledBarys[i * 2 + 0];
+ const baryB = upsampledBarys[i * 2 + 1];
+ const baryC = 1.0 - baryA - baryB;
+
+ const heightA = encoding.decodeHeight(vertexBuffer, indexA);
+ const heightB = encoding.decodeHeight(vertexBuffer, indexB);
+ const heightC = encoding.decodeHeight(vertexBuffer, indexC);
+
+ const height = heightA * baryA + heightB * baryB + heightC * baryC;
+ minimumHeight = Math.min(height, minimumHeight);
+ maximumHeight = Math.max(height, maximumHeight);
+
+ const lon = CesiumMath.lerp(minimumLongitude, maximumLongitude, u);
+ const lat = CesiumMath.lerp(minimumLatitude, maximumLatitude, v);
+ const carto = Cartographic.fromRadians(
+ lon,
+ lat,
+ height,
+ scratchCartographicUpsample,
+ );
+ const position = Cartographic.toCartesian(
+ carto,
+ ellipsoid,
+ scratchPosEcefUpsample,
+ );
+
+ const posEnu = Matrix4.multiplyByPoint(
+ ecefToEnu,
+ position,
+ scratchPosEnuUpsample,
+ );
+ minPosEnu = Cartesian3.minimumByComponent(posEnu, minPosEnu, minPosEnu);
+ maxPosEnu = Cartesian3.maximumByComponent(posEnu, maxPosEnu, maxPosEnu);
+
+ let normalOct;
+ if (hasVertexNormals) {
+ const normalA = encoding.decodeNormal(
+ vertexBuffer,
+ indexA,
+ scratchNormalA,
+ );
+ const normalB = encoding.decodeNormal(
+ vertexBuffer,
+ indexB,
+ scratchNormalB,
+ );
+ const normalC = encoding.decodeNormal(
+ vertexBuffer,
+ indexC,
+ scratchNormalC,
+ );
+
+ let normal = Cartesian3.fromElements(
+ normalA.x * baryA + normalB.x * baryB + normalC.x * baryC,
+ normalA.y * baryA + normalB.y * baryB + normalC.y * baryC,
+ normalA.z * baryA + normalB.z * baryB + normalC.z * baryC,
+ scratchNormalUpsample,
+ );
+ normal = Cartesian3.normalize(normal, scratchNormalUpsample);
+ normalOct = AttributeCompression.octEncode(
+ normal,
+ scratchNormalOctUpsample,
+ );
+ }
+
+ let webMercatorT;
+ if (hasWebMercatorT) {
+ const mercatorAngle =
+ WebMercatorProjection.geodeticLatitudeToMercatorAngle(lat);
+ webMercatorT =
+ (mercatorAngle - southMercatorAngle) * oneOverMercatorHeight;
+ }
+
+ let geodeticSurfaceNormal;
+ if (hasGeodeticSurfaceNormals) {
+ geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal(
+ position,
+ scratchGeodeticSurfaceNormalUpsample,
+ );
+ }
+
+ upsampledVertexBufferOffset = upsampledTerrainEncoding.encode(
+ upsampledVertexBuffer,
+ upsampledVertexBufferOffset,
+ position,
+ uv,
+ height,
+ normalOct,
+ webMercatorT,
+ geodeticSurfaceNormal,
+ );
+ }
+
+ // Now generate the more tight-fitting bounding volumes that are used for culling and other things
+ const orientedBoundingBox = OrientedBoundingBox.fromRectangle(
+ upsampleRectangle,
+ minimumHeight,
+ maximumHeight,
+ ellipsoid,
+ scratchOrientedBoundingBox,
+ );
+ const boundingSphere = BoundingSphere.fromVertices(
+ upsampledVertexBuffer,
+ upsampledTerrainEncoding.center,
+ upsampledVertexBufferStride,
+ scratchBoundingSphere,
+ );
+ const occluder = new EllipsoidalOccluder(ellipsoid);
+ const horizonOcclusionPoint =
+ occluder.computeHorizonCullingPointFromVerticesPossiblyUnderEllipsoid(
+ upsampledTerrainEncoding.center, // vector from ellipsoid center to horizon occlusion point
+ upsampledVertexBuffer,
+ upsampledVertexBufferStride,
+ upsampledTerrainEncoding.center,
+ minimumHeight,
+ scratchHorizonOcclusionPoint,
+ );
+
+ const upsampledMesh = new TerrainMesh(
+ Cartesian3.clone(upsampledTerrainEncoding.center, new Cartesian3()),
+ upsampledVertexBuffer,
+ upsampledIndexBuffer,
+ upsampledIndexCountWithoutSkirts,
+ upsampledVertexCountWithoutSkirts,
+ minimumHeight,
+ maximumHeight,
+ BoundingSphere.clone(boundingSphere),
+ Cartesian3.clone(horizonOcclusionPoint),
+ upsampledVertexBufferStride,
+ OrientedBoundingBox.clone(orientedBoundingBox),
+ upsampledTerrainEncoding,
+ upsampledWestIndicesBuffer,
+ upsampledSouthIndicesBuffer,
+ upsampledEastIndicesBuffer,
+ upsampledNorthIndicesBuffer,
+ );
+
+ addSkirtsToMesh(
+ upsampledMesh,
+ upsampleRectangle,
+ ellipsoid,
+ minPosEnu,
+ maxPosEnu,
+ enuToEcef,
+ ecefToEnu,
+ skirtHeight,
+ );
+
+ return upsampledMesh;
+};
+
+/**
+ * Helper function that adds skirts to a TerrainMesh. The mesh's vertex and index
+ * buffers are expected to be pre-allocated to fit the skirts.
+ * The mesh's vertex buffer must have quantization disabled.
+ * If the final quantization changes, a new vertex buffer will be allocated using the new quantization.
+ * Currently skirts do not affect the tile's bounding volume.
+ * @private
+ * @param {TerrainMesh} mesh
+ * @param {Rectangle} rectangle
+ * @param {Ellipsoid} ellipsoid
+ * @param {Cartesian3} enuMinimum
+ * @param {Cartesian3} enuMaximum
+ * @param {Matrix4} enuToEcef
+ * @param {Matrix4} ecefToEnu
+ * @param {number} skirtHeight
+ */
+function addSkirtsToMesh(
+ mesh,
+ rectangle,
+ ellipsoid,
+ enuMinimum,
+ enuMaximum,
+ enuToEcef,
+ ecefToEnu,
+ skirtHeight,
+) {
+ const { encoding } = mesh;
+ const vertexStride = encoding.stride;
+ const vertexBuffer = mesh.vertices;
+ const {
+ hasVertexNormals,
+ hasWebMercatorT,
+ exaggeration,
+ exaggerationRelativeHeight,
+ } = encoding;
+ const hasExaggeration = exaggeration !== 1.0;
+ const hasGeodeticSurfaceNormals = hasExaggeration;
+
+ const vertexCountWithoutSkirts = mesh.vertexCountWithoutSkirts;
+ let vertexBufferOffset = vertexCountWithoutSkirts * vertexStride;
+ const vertexCountWithSkirts = vertexBuffer.length / vertexStride;
+ const skirtVertexCount = vertexCountWithSkirts - vertexCountWithoutSkirts;
+ const indices = mesh.indices;
+ const indexCountWithoutSkirts = mesh.indexCountWithoutSkirts;
+
+ const westIndices = mesh.westIndicesSouthToNorth;
+ const southIndices = mesh.southIndicesEastToWest;
+ const eastIndices = mesh.eastIndicesNorthToSouth;
+ const northIndices = mesh.northIndicesWestToEast;
+
+ TerrainProvider.addSkirtIndicesWithFilledCorners(
+ westIndices,
+ southIndices,
+ eastIndices,
+ northIndices,
+ vertexCountWithoutSkirts,
+ indices,
+ indexCountWithoutSkirts,
+ );
+
+ const westOffset = 0;
+ const southOffset = westOffset + westIndices.length;
+ const eastOffset = southOffset + southIndices.length;
+ const northOffset = eastOffset + eastIndices.length;
+ const edges = [westIndices, southIndices, eastIndices, northIndices];
+ const edgeIndexOffset = [westOffset, southOffset, eastOffset, northOffset];
+ const edgeLongitudeSign = [-1.0, 0.0, +1.0, 0.0];
+ const edgeLatitudeSign = [0.0, -1.0, 0.0, +1.0];
+
+ const minimumPositionENUWithSkirts = Cartesian3.clone(
+ enuMinimum,
+ scratchMinimumPositionENUSkirt,
+ );
+ const maximumPositionENUWithSkirts = Cartesian3.clone(
+ enuMaximum,
+ scratchMaximumPositionENUSkirt,
+ );
+ const maximumHeight = mesh.maximumHeight;
+ const minimumHeightWithSkirts = mesh.minimumHeight - skirtHeight;
+ for (let skirtId = 0; skirtId < skirtVertexCount; skirtId++) {
+ let side = 0;
+ for (side = 0; side < 3; side++) {
+ if (skirtId < edgeIndexOffset[side + 1]) {
+ break;
+ }
+ }
+ const vertexIndex = edges[side][skirtId - edgeIndexOffset[side]];
+
+ const uv = encoding.decodeTextureCoordinates(
+ vertexBuffer,
+ vertexIndex,
+ scratchUVSkirt,
+ );
+
+ const skirtLonLatOffsetPercent = 0.0001;
+ const longitudeT =
+ uv.x + edgeLongitudeSign[side] * skirtLonLatOffsetPercent;
+ const latitudeT = uv.y + edgeLatitudeSign[side] * skirtLonLatOffsetPercent;
+
+ const longitude = CesiumMath.lerp(
+ rectangle.west,
+ rectangle.east,
+ longitudeT,
+ );
+ // Don't offset the skirt past the poles, it will screw up the cartographic -> cartesian
+ const latitude = CesiumMath.clamp(
+ CesiumMath.lerp(rectangle.south, rectangle.north, latitudeT),
+ -CesiumMath.PI_OVER_TWO,
+ +CesiumMath.PI_OVER_TWO,
+ );
+
+ const vertHeight = encoding.decodeHeight(vertexBuffer, vertexIndex);
+ const height = vertHeight - skirtHeight;
+
+ const cartographic = Cartographic.fromRadians(
+ longitude,
+ latitude,
+ height,
+ scratchCartographicSkirt,
+ );
+
+ const positionEcef = Cartographic.toCartesian(
+ cartographic,
+ ellipsoid,
+ scratchPosEcefSkirt,
+ );
+
+ let normalOct;
+ if (hasVertexNormals) {
+ normalOct = encoding.getOctEncodedNormal(
+ vertexBuffer,
+ vertexIndex,
+ scratchNormalOctSkirt,
+ );
+ }
+
+ let webMercatorT;
+ if (hasWebMercatorT) {
+ webMercatorT = encoding.decodeWebMercatorT(vertexBuffer, vertexIndex);
+ }
+
+ let geodeticSurfaceNormal;
+ if (hasGeodeticSurfaceNormals) {
+ geodeticSurfaceNormal = ellipsoid.geodeticSurfaceNormal(
+ positionEcef,
+ scratchGeodeticSurfaceNormalSkirt,
+ );
+ }
+
+ vertexBufferOffset = encoding.encode(
+ vertexBuffer,
+ vertexBufferOffset,
+ positionEcef,
+ uv,
+ height,
+ normalOct,
+ webMercatorT,
+ geodeticSurfaceNormal,
+ );
+
+ const positionENU = Matrix4.multiplyByPoint(
+ ecefToEnu,
+ positionEcef,
+ scratchPosEnuSkirt,
+ );
+ Cartesian3.minimumByComponent(
+ positionENU,
+ minimumPositionENUWithSkirts,
+ minimumPositionENUWithSkirts,
+ );
+ Cartesian3.maximumByComponent(
+ positionENU,
+ maximumPositionENUWithSkirts,
+ maximumPositionENUWithSkirts,
+ );
+ }
+
+ const aabbEnuWithSkirts = AxisAlignedBoundingBox.fromCorners(
+ minimumPositionENUWithSkirts,
+ maximumPositionENUWithSkirts,
+ scratchAABBEnuSkirt,
+ );
+
+ // Check if the final terrain encoding has a different quantization. If so,
+ // the vertices need to be re-encoded with the new quantization. Otherwise,
+ // use the vertex buffer as-is.
+ const encodingWithSkirts = new TerrainEncoding(
+ encoding.center,
+ aabbEnuWithSkirts,
+ minimumHeightWithSkirts,
+ maximumHeight,
+ enuToEcef,
+ encoding.hasVertexNormals,
+ encoding.hasWebMercatorT,
+ hasGeodeticSurfaceNormals,
+ exaggeration,
+ exaggerationRelativeHeight,
+ );
+ if (encoding.quantization !== encodingWithSkirts.quantization) {
+ const finalEncoding = encodingWithSkirts;
+ const finalVertexStride = finalEncoding.stride;
+ const finalVertexBuffer = new Float32Array(
+ vertexCountWithSkirts * finalVertexStride,
+ );
+ let finalVertexBufferOffset = 0;
+ for (let i = 0; i < vertexCountWithSkirts; i++) {
+ finalVertexBufferOffset = finalEncoding.encode(
+ finalVertexBuffer,
+ finalVertexBufferOffset,
+ encoding.decodePosition(vertexBuffer, i, scratchPosEcefSkirt),
+ encoding.decodeTextureCoordinates(vertexBuffer, i, scratchUVSkirt),
+ encoding.decodeHeight(vertexBuffer, i),
+ encoding.hasVertexNormals
+ ? encoding.getOctEncodedNormal(vertexBuffer, i, scratchNormalOctSkirt)
+ : undefined,
+ encoding.hasWebMercatorT
+ ? encoding.decodeWebMercatorT(vertexBuffer, i)
+ : undefined,
+ encoding.hasGeodeticSurfaceNormals
+ ? encoding.decodeGeodeticSurfaceNormal(
+ vertexBuffer,
+ i,
+ scratchGeodeticSurfaceNormalSkirt,
+ )
+ : undefined,
+ );
+ }
+ mesh.vertices = finalVertexBuffer;
+ mesh.stride = finalVertexStride;
+ mesh.encoding = finalEncoding;
+ }
+
+ return mesh;
+}
+
+const EDGE_ID_LEFT = 0;
+const EDGE_ID_TOP = 1;
+const EDGE_ID_RIGHT = 2;
+const EDGE_ID_BOTTOM = 3;
+const EDGE_COUNT = 4;
+
+const scratchIntersection = new Cartesian3();
+
+const scratchInBarys = [
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+];
+const scratchInPoints = [
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+];
+
+const scratchOutBarys = [
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+];
+const scratchOutPoints = [
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+];
+
+/**
+ * Check if a given point is inside the limits of a box.
+ * @private
+ * @param {Cartesian2} boxMinimum The lower left corner of the box.
+ * @param {Cartesian2} boxMaximum The upper right corner of the box.
+ * @param {number} edgeId The ID of the edge to test against.
+ * @param {Cartesian2} p The point to test.
+ * @returns {number} Positive if inside, negative if outside, zero if on the edge.
+ */
+function inside(boxMinimum, boxMaximum, edgeId, p) {
+ switch (edgeId) {
+ case EDGE_ID_LEFT:
+ return CesiumMath.sign(p.x - boxMinimum.x);
+ case EDGE_ID_RIGHT:
+ return CesiumMath.sign(boxMaximum.x - p.x);
+ case EDGE_ID_BOTTOM:
+ return CesiumMath.sign(p.y - boxMinimum.y);
+ default:
+ // EDGE_ID_TOP
+ return CesiumMath.sign(boxMaximum.y - p.y);
+ }
+}
+
+/**
+ * Compute the intersection of a line segment against one edge of a box.
+ * @private
+ * @param {Cartesian2} boxMinimum The lower left corner of the box.
+ * @param {Cartesian2} boxMaximum The upper right corner of the box.
+ * @param {number} edgeId The ID of the edge to intersect against.
+ * @param {Cartesian2} a The beginning of the line segment.
+ * @param {Cartesian2} b The end of the line segment.
+ * @param {Cartesian3} result The object into which to copy the result.
+ * @returns {Cartesian3} The intersection point in 2D coordinates and the interpolation factor t as the third component.
+ */
+function intersect(boxMinimum, boxMaximum, edgeId, a, b, result) {
+ let t, intersectX, intersectY;
+ switch (edgeId) {
+ case EDGE_ID_LEFT:
+ t = (boxMinimum.x - a.x) / (b.x - a.x);
+ intersectX = boxMinimum.x;
+ intersectY = a.y + (b.y - a.y) * t;
+ break;
+ case EDGE_ID_RIGHT:
+ t = (boxMaximum.x - a.x) / (b.x - a.x);
+ intersectX = boxMaximum.x;
+ intersectY = a.y + (b.y - a.y) * t;
+ break;
+ case EDGE_ID_BOTTOM:
+ t = (boxMinimum.y - a.y) / (b.y - a.y);
+ intersectX = a.x + (b.x - a.x) * t;
+ intersectY = boxMinimum.y;
+ break;
+ default:
+ // EDGE_ID_TOP
+ t = (boxMaximum.y - a.y) / (b.y - a.y);
+ intersectX = a.x + (b.x - a.x) * t;
+ intersectY = boxMaximum.y;
+ break;
+ }
+ return Cartesian3.fromElements(intersectX, intersectY, t, result);
+}
+
+/**
+ * Coordinates of a quadrilateral resulting from clipping a triangle against a box.
+ * @private
+ * @typedef PolygonResult
+ *
+ * @property {number} length
+ * @property {Cartesian2[]} coordinates A pre-allocated array of six 2D coordinates.
+ * @property {Cartesian3[]} barycentricCoordinates A pre-allocated array of six barycentric coordinates.
+ */
+
+/**
+ * A scratch polygon result for use in clipping.
+ * @private
+ * @type {PolygonResult}
+ */
+const scratchPolygon = {
+ length: 0,
+ coordinates: [
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ new Cartesian2(),
+ ],
+ barycentricCoordinates: [
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ new Cartesian3(),
+ ],
+};
+
+/**
+ * Clips a 2D triangle against axis-aligned edges of a box using the Sutherland-Hodgman
+ * clipping algorithm. The resulting polygon will have between 0 and 6 vertices.
+ *
+ * @private
+ * @param {number} edgeStart The first edge to clip against.
+ * @param {number} edgeCount The number of edges to clip against, starting from edgeStart.
+ * @param {Cartesian2} boxMinimum The bottom-left corner of the axis-aligned box.
+ * @param {Cartesian2} boxMaximum The top-right corner of the axis-aligned box.
+ * @param {Cartesian2} p0 The coordinates of the first vertex in the triangle, in counter-clockwise order.
+ * @param {Cartesian2} p1 The coordinates of the second vertex in the triangle, in counter-clockwise order.
+ * @param {Cartesian2} p2 The coordinates of the third vertex in the triangle, in counter-clockwise order.
+ * @param {PolygonResult} result The object into which to copy the result.
+ * @returns {PolygonResult} The polygon that results after the clip, specified as a list of coordinates in counter-clockwise order.
+ */
+function clipTriangleAgainstBoxEdgeRange(
+ edgeStart,
+ edgeCount,
+ boxMinimum,
+ boxMaximum,
+ p0,
+ p1,
+ p2,
+ result,
+) {
+ let inputLength = 0;
+ let inputPoints = scratchInPoints;
+ let inputBarys = scratchInBarys;
+
+ let outputLength = 3;
+ let outputPoints = scratchOutPoints;
+ Cartesian2.clone(p0, outputPoints[0]);
+ Cartesian2.clone(p1, outputPoints[1]);
+ Cartesian2.clone(p2, outputPoints[2]);
+
+ let outputBarys = scratchOutBarys;
+ Cartesian3.fromElements(1, 0, 0, outputBarys[0]);
+ Cartesian3.fromElements(0, 1, 0, outputBarys[1]);
+ Cartesian3.fromElements(0, 0, 1, outputBarys[2]);
+
+ // Loop over the clip window edges
+ for (let e = 0; e < edgeCount; e++) {
+ const edgeId = (edgeStart + e) % EDGE_COUNT;
+
+ // Swap the input and output arrays
+ const tempPoints = inputPoints;
+ const tempBarys = inputBarys;
+
+ inputPoints = outputPoints;
+ inputBarys = outputBarys;
+ inputLength = outputLength;
+
+ outputPoints = tempPoints;
+ outputBarys = tempBarys;
+ outputLength = 0;
+
+ // Check each polygon edge against each clip window edge
+ let prevIdx = inputLength - 1;
+ let prevPoint = inputPoints[prevIdx];
+ let prevBary = inputBarys[prevIdx];
+ let prevInside = inside(boxMinimum, boxMaximum, edgeId, prevPoint);
+
+ for (let currIdx = 0; currIdx < inputLength; currIdx++) {
+ const currPoint = inputPoints[currIdx];
+ const currBary = inputBarys[currIdx];
+ const currInside = inside(boxMinimum, boxMaximum, edgeId, currPoint);
+
+ // Check if the two points are on opposite sides of the edge.
+ // If so, there's an intersection, and a new point is created.
+ if (prevInside * currInside === -1) {
+ const intersection = intersect(
+ boxMinimum,
+ boxMaximum,
+ edgeId,
+ prevPoint,
+ currPoint,
+ scratchIntersection,
+ );
+ const { x, y, z: t } = intersection;
+ const tInv = 1.0 - t;
+
+ // Interpolate the barycentric coordinates
+ const baryA = prevBary.x * tInv + currBary.x * t;
+ const baryB = prevBary.y * tInv + currBary.y * t;
+ const baryC = prevBary.z * tInv + currBary.z * t;
+
+ Cartesian2.fromElements(x, y, outputPoints[outputLength]);
+ Cartesian3.fromElements(baryA, baryB, baryC, outputBarys[outputLength]);
+ outputLength++;
+ }
+
+ // If the second point is on or inside, add it
+ if (currInside >= 0) {
+ Cartesian2.clone(currPoint, outputPoints[outputLength]);
+ Cartesian3.clone(currBary, outputBarys[outputLength]);
+ outputLength++;
+ }
+
+ prevIdx = currIdx;
+ prevPoint = currPoint;
+ prevBary = currBary;
+ prevInside = currInside;
+ }
+
+ // All points were outside, so break early
+ if (outputLength === 0) {
+ break;
+ }
+ }
+
+ result.length = outputLength;
+ for (let i = 0; i < outputLength; i++) {
+ Cartesian2.clone(outputPoints[i], result.coordinates[i]);
+ Cartesian3.clone(outputBarys[i], result.barycentricCoordinates[i]);
+ }
+ return result;
+}
+
+/**
+ * Clips a 2D triangle against one quadrant of a box.
+ * @private
+ * @param {boolean} isEastChild true if the quadrant is on the east side of the box.
+ * @param {boolean} isNorthChild true if the quadrant is on the north side of the box.
+ * @param {Cartesian2} boxMinimum The lower left corner of the box.
+ * @param {Cartesian2} boxMaximum The upper right corner of the box.
+ * @param {Cartesian2} p0 The first vertex of the triangle.
+ * @param {Cartesian2} p1 The second vertex of the triangle.
+ * @param {Cartesian2} p2 The third vertex of the triangle.
+ * @param {PolygonResult} result The object into which to copy the result.
+ * @returns {PolygonResult} The polygon that results after the clip, specified as a list of coordinates in counter-clockwise order.
+ */
+function clipTriangleFromQuadrant(
+ isEastChild,
+ isNorthChild,
+ boxMinimum,
+ boxMaximum,
+ p0,
+ p1,
+ p2,
+ result,
+) {
+ const edgeStart = isEastChild
+ ? isNorthChild
+ ? EDGE_ID_BOTTOM
+ : EDGE_ID_LEFT
+ : isNorthChild
+ ? EDGE_ID_RIGHT
+ : EDGE_ID_TOP;
+
+ return clipTriangleAgainstBoxEdgeRange(
+ edgeStart,
+ 2,
+ boxMinimum,
+ boxMaximum,
+ p0,
+ p1,
+ p2,
+ result,
+ );
+}
+
+const lookUpTableBaryToPrim = [
+ [], // 000
+ [0], // 001
+ [1], // 010
+ [0, 1], // 011
+ [2], // 100
+ [0, 2], // 101
+ [1, 2], // 110
+ [0, 1, 2], // 111
+];
+
+/**
+ * Returns triangles that are clipped against a quadrant of a tile.
+ * @private
+ * @param {boolean} isEastChild true if the quadrant is on the east side of the tile.
+ * @param {boolean} isNorthChild true if the quadrant is on the north side of the tile.
+ * @param {number} indexCount Number of indices in the original triangle list.
+ * @param {Uint8Array|Uint16Array|Uint32Array} indices Original triangle index list.
+ * @param {number} vertexCount Number of vertices in the original vertex list.
+ * @param {Float32Array} vertices Original vertex list.
+ * @param {TerrainEncoding} vertexEncoding Encoding of the original vertices.
+ * @param {number[]} resultIndices Indices of the clipped triangles.
+ * @param {number[]} resultWestIndices Indices on the west edge.
+ * @param {number[]} resultSouthIndices Indices on the south edge.
+ * @param {number[]} resultEastIndices Indices on the east edge.
+ * @param {number[]} resultNorthIndices Indices on the north edge.
+ * @param {number[]} resultTriIds Per-vertex index to the originating triangle in indices.
+ * @param {number[]} resultBary Per-vertex barycentric coordinate corresponding to the originating triangle.
+ * @param {number[]} resultUVs Per-vertex UV within the quadrant.
+ */
+function clipTileFromQuadrant(
+ isEastChild,
+ isNorthChild,
+ indexCount,
+ indices,
+ vertexCount,
+ vertices,
+ vertexEncoding,
+ resultIndices,
+ resultWestIndices,
+ resultSouthIndices,
+ resultEastIndices,
+ resultNorthIndices,
+ resultTriIds,
+ resultBary,
+ resultUVs,
+) {
+ const upsampledVertexMap = {};
+
+ const minU = isEastChild ? 0.5 : 0.0;
+ const maxU = isEastChild ? 1.0 : 0.5;
+ const minV = isNorthChild ? 0.5 : 0.0;
+ const maxV = isNorthChild ? 1.0 : 0.5;
+
+ const minUV = scratchMinUV;
+ minUV.x = minU;
+ minUV.y = minV;
+
+ const maxUV = scratchMaxUV;
+ maxUV.x = maxU;
+ maxUV.y = maxV;
+
+ let upsampledVertexCount = 0;
+
+ // Loop over all the original triangles
+ for (let i = 0; i < indexCount; i += 3) {
+ const indexA = indices[i + 0];
+ const indexB = indices[i + 1];
+ const indexC = indices[i + 2];
+
+ const uvA = vertexEncoding.decodeTextureCoordinates(
+ vertices,
+ indexA,
+ scratchUvA,
+ );
+ const uvB = vertexEncoding.decodeTextureCoordinates(
+ vertices,
+ indexB,
+ scratchUvB,
+ );
+ const uvC = vertexEncoding.decodeTextureCoordinates(
+ vertices,
+ indexC,
+ scratchUvC,
+ );
+
+ const clippedPolygon = clipTriangleFromQuadrant(
+ isEastChild,
+ isNorthChild,
+ minUV,
+ maxUV,
+ uvA,
+ uvB,
+ uvC,
+ scratchPolygon,
+ );
+ const clippedPolygonLength = clippedPolygon.length;
+ if (clippedPolygonLength < 3) {
+ // Triangle is outside clipping window, so skip it
+ continue;
+ }
+
+ const polygonUpsampledIndices = scratchPolygonIndices;
+
+ for (let p = 0; p < clippedPolygonLength; p++) {
+ const polygonBary = clippedPolygon.barycentricCoordinates[p];
+ const bA = polygonBary.x;
+ const bB = polygonBary.y;
+ const bC = polygonBary.z;
+
+ // Convert the barycentric coords to a bitfield to find out which vertices are involved
+ const baryId =
+ Math.ceil(bA) | (Math.ceil(bB) << 1) | (Math.ceil(bC) << 2);
+ const primitiveIds = lookUpTableBaryToPrim[baryId];
+
+ let upsampledIndex;
+ let isNewVertex = false;
+
+ if (primitiveIds.length === 1) {
+ //-------------------------------------------------------
+ // Vertex: Only one barycentric coord is set, so it's on a vertex
+ //-------------------------------------------------------
+ const pointPrimitiveId = primitiveIds[0];
+ const pointIndex = indices[i + pointPrimitiveId];
+
+ // Add the vertex if it doesn't exist already
+ const pointKey = pointIndex;
+ upsampledIndex = upsampledVertexMap[pointKey];
+ if (upsampledIndex === undefined) {
+ isNewVertex = true;
+ upsampledIndex = upsampledVertexCount++;
+ upsampledVertexMap[pointKey] = upsampledIndex;
+ }
+ } else if (primitiveIds.length === 2) {
+ //-------------------------------------------------------
+ // Edge: Only two barycentric coords are set, so it's on an edge
+ //-------------------------------------------------------
+ const edgePrimitiveIdA = primitiveIds[0];
+ const edgePrimitiveIdB = primitiveIds[1];
+ const edgeIndexA = indices[i + edgePrimitiveIdA];
+ const edgeIndexB = indices[i + edgePrimitiveIdB];
+
+ // Detect if a clipped position was already added by a triangle that shares the same edge.
+ // The key is based on the two edge indices and whether it is the first or second clipped position on the edge (an edge can be clipped by a convex shape at most twice).
+ const prevBary =
+ clippedPolygon.barycentricCoordinates[
+ (p + clippedPolygonLength - 1) % clippedPolygonLength
+ ];
+ const prevBaryId =
+ Math.ceil(prevBary.x) |
+ (Math.ceil(prevBary.y) << 1) |
+ (Math.ceil(prevBary.z) << 2);
+ const sameEdge = baryId === prevBaryId;
+
+ // The winding order of the edge will consty between triangles (i.e. A -> B vs B -> A), so take the min/max to make the key consistent.
+ const minIndex = Math.min(edgeIndexA, edgeIndexB);
+ const maxIndex = Math.max(edgeIndexA, edgeIndexB);
+ const baseKey = vertexCount + 2 * (minIndex * vertexCount + maxIndex);
+
+ const firstKey = baseKey + 0;
+ const secondKey = baseKey + 1;
+ const firstEntry = upsampledVertexMap[firstKey];
+ const secondEntry = upsampledVertexMap[secondKey];
+
+ // !firstEntry && !sameEdge -> firstEntry (undefined)
+ // !secondEntry && sameEdge -> secondEntry (undefined)
+ // firstEntry && !secondEntry && !sameEdge -> firstEntry (reverse solo)
+ // firstEntry && secondEntry && !sameEdge -> secondEntry (reverse first)
+ // firstEntry && secondEntry && sameEdge -> firstEntry (reverse second)
+ const useFirst =
+ !sameEdge === (firstEntry === undefined || secondEntry === undefined);
+ upsampledIndex = useFirst ? firstEntry : secondEntry;
+
+ // Add the vertex if it doesn't already exist
+ if (upsampledIndex === undefined) {
+ isNewVertex = true;
+ upsampledIndex = upsampledVertexCount++;
+ const edgeKey = useFirst ? firstKey : secondKey;
+ upsampledVertexMap[edgeKey] = upsampledIndex;
+ }
+ } else {
+ //-------------------------------------------------------
+ // Face: All three barycentric coords are set, so it's inside the triangle
+ //-------------------------------------------------------
+ isNewVertex = true;
+ upsampledIndex = upsampledVertexCount++;
+ }
+
+ // Store the index for this point in the polygon
+ polygonUpsampledIndices[p] = upsampledIndex;
+
+ if (isNewVertex) {
+ const triId = i / 3;
+ resultTriIds.push(triId);
+ const polygonUV = clippedPolygon.coordinates[p];
+ const u = (polygonUV.x - minU) / (maxU - minU);
+ const v = (polygonUV.y - minV) / (maxV - minV);
+
+ resultUVs.push(u, v);
+ resultBary.push(bA, bB);
+
+ if (u === 0.0) {
+ resultWestIndices.push(upsampledIndex);
+ } else if (u === 1.0) {
+ resultEastIndices.push(upsampledIndex);
+ }
+ if (v === 0.0) {
+ resultSouthIndices.push(upsampledIndex);
+ } else if (v === 1.0) {
+ resultNorthIndices.push(upsampledIndex);
+ }
+ }
+ }
+
+ // Triangulate the polygon by connecting vertices in a fan shape
+ const ui0 = polygonUpsampledIndices[0];
+ let ui1 = polygonUpsampledIndices[1];
+ let ui2 = polygonUpsampledIndices[2];
+ resultIndices.push(ui0, ui1, ui2);
+ for (let j = 3; j < clippedPolygonLength; j++) {
+ ui1 = ui2;
+ ui2 = polygonUpsampledIndices[j];
+ resultIndices.push(ui0, ui1, ui2);
+ }
+ }
+
+ resultWestIndices.sort(function (a, b) {
+ return resultUVs[a * 2 + 1] - resultUVs[b * 2 + 1];
+ });
+ resultSouthIndices.sort(function (a, b) {
+ return resultUVs[b * 2 + 0] - resultUVs[a * 2 + 0];
+ });
+ resultEastIndices.sort(function (a, b) {
+ return resultUVs[b * 2 + 1] - resultUVs[a * 2 + 1];
+ });
+ resultNorthIndices.sort(function (a, b) {
+ return resultUVs[a * 2 + 0] - resultUVs[b * 2 + 0];
+ });
+}
+
+export default Cesium3DTilesTerrainGeometryProcessor;
diff --git a/packages/engine/Source/Core/Cesium3DTilesTerrainProvider.js b/packages/engine/Source/Core/Cesium3DTilesTerrainProvider.js
new file mode 100644
index 000000000000..7fc1fd3d0691
--- /dev/null
+++ b/packages/engine/Source/Core/Cesium3DTilesTerrainProvider.js
@@ -0,0 +1,1052 @@
+import BoundingSphere from "./BoundingSphere.js";
+import Cesium3DTilesTerrainData from "./Cesium3DTilesTerrainData.js";
+import CesiumMath from "./Math.js";
+import Check from "./Check.js";
+import Credit from "./Credit.js";
+import defined from "./defined.js";
+import DeveloperError from "./DeveloperError.js";
+import DoubleEndedPriorityQueue from "./DoubleEndedPriorityQueue.js";
+import Ellipsoid from "./Ellipsoid.js";
+import Event from "./Event.js";
+import Frozen from "./Frozen.js";
+import GeographicTilingScheme from "./GeographicTilingScheme.js";
+import ImplicitSubtree from "../Scene/ImplicitSubtree.js";
+import ImplicitTileCoordinates from "../Scene/ImplicitTileCoordinates.js";
+import IonResource from "./IonResource.js";
+import ImplicitTileset from "../Scene/ImplicitTileset.js";
+import loadImageFromTypedArray from "./loadImageFromTypedArray.js";
+import MetadataSchema from "../Scene/MetadataSchema.js";
+import MetadataSchemaLoader from "../Scene/MetadataSchemaLoader.js";
+import MetadataSemantic from "../Scene/MetadataSemantic.js";
+import OrientedBoundingBox from "./OrientedBoundingBox.js";
+import parseGlb from "../Scene/GltfPipeline/parseGlb.js";
+import Rectangle from "./Rectangle.js";
+import Resource from "./Resource.js";
+import ResourceCache from "../Scene/ResourceCache.js";
+import RuntimeError from "./RuntimeError.js";
+import TerrainProvider from "./TerrainProvider.js";
+
+/**
+ * @typedef {object} Cesium3DTilesTerrainProvider.ConstructorOptions
+ *
+ * Initialization options for the Cesium3DTilesTerrainProvider constructor
+ *
+ * @property {boolean} [requestVertexNormals=false] Flag that indicates if the client should request additional lighting information from the server, in the form of per vertex normals if available.
+ * @property {boolean} [requestWaterMask=false] Flag that indicates if the client should request per tile water masks from the server, if available.
+ * @property {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the WGS84 ellipsoid is used.
+ * @property {Credit|string} [credit] A credit for the data source, which is displayed on the canvas.
+ */
+
+/**
+ *
+ * To construct a Cesium3DTilesTerrainProvider, call {@link Cesium3DTilesTerrainProvider.fromIonAssetId} or {@link Cesium3DTilesTerrainProvider.fromUrl}. Do not call the constructor directly.
+ *
+ *
+ * A {@link TerrainProvider} that accesses terrain data in a 3D Tiles format.
+ *
+ * @alias Cesium3DTilesTerrainProvider
+ * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
+ * @constructor
+ *
+ * @param {Cesium3DTilesTerrainProvider.ConstructorOptions}[options] An object describing initialization options
+ *
+ * @see TerrainProvider
+ * @see Cesium3DTilesTerrainProvider.fromUrl
+ * @see Cesium3DTilesTerrainProvider.fromIonAssetId
+ *
+ * // Create GTOPO30 with vertex normals
+ * try {
+ * const viewer = new Cesium.Viewer("cesiumContainer", {
+ * terrainProvider: await Cesium.Cesium3DTilesTerrainProvider.fromIonAssetId(2732686, {
+ * requestVertexNormals: true
+ * })
+ * });
+ * } catch (error) {
+ * console.log(error);
+ * }
+ */
+function Cesium3DTilesTerrainProvider(options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ let credit = options.credit;
+ if (typeof credit === "string") {
+ credit = new Credit(credit);
+ }
+ this._credit = credit;
+ this._tileCredits = undefined;
+ this._errorEvent = new Event();
+ this._ellipsoid = options.ellipsoid ?? Ellipsoid.WGS84;
+
+ this._tilingScheme = new GeographicTilingScheme({
+ ellipsoid: this._ellipsoid,
+ });
+
+ this._subtreeCache = new ImplicitSubtreeCache({
+ provider: this,
+ });
+
+ /**
+ * @private
+ * @type {ImplicitTileset|undefined}
+ */
+ this._tileset0 = undefined;
+ /**
+ * @private
+ * @type {ImplicitTileset|undefined}
+ */
+ this._tileset1 = undefined;
+
+ this._resource = undefined;
+
+ /**
+ * Boolean flag that indicates if the client should request vertex normals from the server.
+ * @type {boolean}
+ * @default false
+ * @private
+ */
+ this._requestVertexNormals = options.requestVertexNormals ?? false;
+
+ /**
+ * Boolean flag that indicates if the client should request tile watermasks from the server.
+ * @type {boolean}
+ * @default false
+ * @private
+ */
+ this._requestWaterMask = options.requestWaterMask ?? false;
+}
+
+/**
+ * Creates a {@link TerrainProvider} that accesses terrain data in a Cesium 3D Tiles format.
+ *
+ * @param {Resource|string|Promise|Promise} url The URL of the Cesium terrain server.
+ * @param {Cesium3DTilesTerrainProvider.ConstructorOptions} [options] An object describing initialization options.
+ * @returns {Promise} A promise that resolves to the terrain provider.
+ *
+ * @example
+ * // Create terrain with normals.
+ * try {
+ * const viewer = new Cesium.Viewer("cesiumContainer", {
+ * terrainProvider: await Cesium.Cesium3DTilesTerrainProvider.fromUrl(
+ * Cesium.IonResource.fromAssetId(3956), {
+ * requestVertexNormals: true
+ * })
+ * });
+ * } catch (error) {
+ * console.log(error);
+ * }
+ */
+Cesium3DTilesTerrainProvider.fromUrl = async function (url, options) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined("url", url);
+ //>>includeEnd('debug');
+
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ url = await Promise.resolve(url);
+ const resource = Resource.createIfNeeded(url);
+
+ let tilesetJson;
+ try {
+ tilesetJson = await resource.fetchJson();
+ } catch (error) {
+ throw new RuntimeError("Could not load tileset JSON", error);
+ }
+
+ const provider = new Cesium3DTilesTerrainProvider(options);
+ // ion resources have a credits property we can use for additional attribution.
+ provider._tileCredits = resource.credits;
+ provider._resource = resource;
+
+ const childrenJson = tilesetJson["root"]["children"];
+ const child0Json = childrenJson[0];
+ const child1Json = childrenJson[1];
+
+ const metadataSchemaJson = tilesetJson["schema"];
+ const metadataSchema = MetadataSchema.fromJson(metadataSchemaJson);
+
+ provider._tileset0 = new ImplicitTileset(
+ resource,
+ child0Json,
+ metadataSchema,
+ );
+ provider._tileset1 = new ImplicitTileset(
+ resource,
+ child1Json,
+ metadataSchema,
+ );
+
+ return provider;
+};
+
+/**
+ * Creates a {@link TerrainProvider} from a Cesium ion asset ID that accesses terrain data in a Cesium 3D Tiles format
+ *
+ * @param {number} assetId The Cesium ion asset id.
+ * @param {CesiumTerrainProvider.ConstructorOptions} [options] An object describing initialization options.
+ * @returns {Promise}
+ *
+ * @example
+ * // Create GTOPO30 with vertex normals
+ * try {
+ * const viewer = new Cesium.Viewer("cesiumContainer", {
+ * terrainProvider: await Cesium.Cesium3DTilesTerrainProvider.fromIonAssetId(2732686, {
+ * requestVertexNormals: true
+ * })
+ * });
+ * } catch (error) {
+ * console.log(error);
+ * }
+ *
+ * @exception {RuntimeError} layer.json does not specify a format
+ * @exception {RuntimeError} layer.json specifies an unknown format
+ * @exception {RuntimeError} layer.json specifies an unsupported quantized-mesh version
+ * @exception {RuntimeError} layer.json does not specify a tiles property, or specifies an empty array
+ * @exception {RuntimeError} layer.json does not specify any tile URL templates
+ */
+Cesium3DTilesTerrainProvider.fromIonAssetId = async function (
+ assetId,
+ options,
+) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined("assetId", assetId);
+ //>>includeEnd('debug');
+
+ const resource = await IonResource.fromAssetId(assetId);
+ return Cesium3DTilesTerrainProvider.fromUrl(resource, options);
+};
+
+const scratchPromises = new Array(3);
+
+/**
+ * Requests the geometry for a given tile. This function should not be called before
+ * {@link Cesium3DTilesTerrainProvider#ready} returns true. The result must include terrain data and
+ * may optionally include a water mask and an indication of which child tiles are available.
+ *
+ * @param {number} x The X coordinate of the tile for which to request geometry.
+ * @param {number} y The Y coordinate of the tile for which to request geometry.
+ * @param {number} level The level of the tile for which to request geometry.
+ * @param {Request} [request] The request object. Intended for internal use only.
+ *
+ * @returns {Promise.|undefined} A promise for the requested geometry. If this method
+ * returns undefined instead of a promise, it is an indication that too many requests are already
+ * pending and the request will be retried later.
+ */
+Cesium3DTilesTerrainProvider.prototype.requestTileGeometry = async function (
+ x,
+ y,
+ level,
+ request,
+) {
+ const rootId = getRootIdFromGeographic(level, x);
+ const implicitTileset = rootId === 0 ? this._tileset0 : this._tileset1;
+
+ const tileCoord = getImplicitTileCoordinatesFromGeographicCoordinates(
+ implicitTileset,
+ level,
+ x,
+ y,
+ );
+
+ const subtreeCoord = tileCoord.getSubtreeCoordinates();
+
+ const cache = this._subtreeCache;
+ let subtree = cache.find(rootId, subtreeCoord);
+
+ const requestWaterMask = this._requestWaterMask;
+ const that = this;
+
+ let subtreePromise;
+ if (subtree === undefined) {
+ const subtreeRelative =
+ implicitTileset.subtreeUriTemplate.getDerivedResource({
+ templateValues: subtreeCoord.getTemplateValues(),
+ });
+ const subtreeResource = implicitTileset.baseResource.getDerivedResource({
+ url: subtreeRelative.url,
+ });
+ subtreePromise = subtreeResource
+ .fetchArrayBuffer()
+ .then(async function (arrayBuffer) {
+ // Check if the subtree exists again in case multiple fetches for the same subtree went out at the same time. Don't want to double-add to the cache
+ subtree = cache.find(rootId, subtreeCoord);
+ if (subtree === undefined) {
+ const bufferU8 = new Uint8Array(arrayBuffer);
+ subtree = await ImplicitSubtree.fromSubtreeJson(
+ that._resource,
+ undefined,
+ bufferU8,
+ implicitTileset,
+ subtreeCoord,
+ );
+ cache.addSubtree(rootId, subtree);
+ }
+
+ return subtree;
+ });
+ } else {
+ subtreePromise = Promise.resolve(subtree);
+ }
+
+ // Note: only one content for terrain
+ const glbRelative = implicitTileset.contentUriTemplates[0].getDerivedResource(
+ {
+ templateValues: tileCoord.getTemplateValues(),
+ },
+ );
+ const glbResource = implicitTileset.baseResource.getDerivedResource({
+ url: glbRelative.url,
+ });
+
+ // Start fetching the glb right away -- possibly even before the subtree is loaded in some cases
+ const glbPromise = glbResource.fetchArrayBuffer();
+ if (glbPromise === undefined) {
+ return undefined;
+ }
+
+ const gltfPromise = glbPromise.then((glbBuffer) =>
+ parseGlb(new Uint8Array(glbBuffer)),
+ );
+
+ const promises = scratchPromises;
+ promises[0] = subtreePromise;
+ promises[1] = gltfPromise;
+ promises[2] = requestWaterMask
+ ? gltfPromise.then((gltf) => loadWaterMask(gltf, glbResource))
+ : undefined;
+
+ try {
+ const results = await Promise.all(promises);
+ const subtree = results[0];
+ const gltf = results[1];
+ const waterMask = results[2];
+
+ const metadataView = subtree.getTileMetadataView(tileCoord);
+
+ const minimumHeight = metadataView.getPropertyBySemantic(
+ MetadataSemantic.TILE_MINIMUM_HEIGHT,
+ );
+
+ const maximumHeight = metadataView.getPropertyBySemantic(
+ MetadataSemantic.TILE_MAXIMUM_HEIGHT,
+ );
+
+ const boundingSphereArray = metadataView.getPropertyBySemantic(
+ MetadataSemantic.TILE_BOUNDING_SPHERE,
+ );
+ const boundingSphere = BoundingSphere.unpack(
+ boundingSphereArray,
+ 0,
+ new BoundingSphere(),
+ );
+
+ const horizonOcclusionPoint = metadataView.getPropertyBySemantic(
+ MetadataSemantic.TILE_HORIZON_OCCLUSION_POINT,
+ );
+
+ const tilingScheme = that._tilingScheme;
+
+ // The tiling scheme uses geographic coords, not implicit coords
+ const rectangle = tilingScheme.tileXYToRectangle(
+ x,
+ y,
+ level,
+ new Rectangle(),
+ );
+
+ const ellipsoid = that._ellipsoid;
+
+ const orientedBoundingBox = OrientedBoundingBox.fromRectangle(
+ rectangle,
+ minimumHeight,
+ maximumHeight,
+ ellipsoid,
+ new OrientedBoundingBox(),
+ );
+
+ const skirtHeight = that.getLevelMaximumGeometricError(level) * 5.0;
+
+ const hasSW = isChildAvailable(implicitTileset, subtree, tileCoord, 0, 0);
+ const hasSE = isChildAvailable(implicitTileset, subtree, tileCoord, 1, 0);
+ const hasNW = isChildAvailable(implicitTileset, subtree, tileCoord, 0, 1);
+ const hasNE = isChildAvailable(implicitTileset, subtree, tileCoord, 1, 1);
+ const childTileMask =
+ (hasSW ? 1 : 0) | (hasSE ? 2 : 0) | (hasNW ? 4 : 0) | (hasNE ? 8 : 0);
+
+ const terrainData = new Cesium3DTilesTerrainData({
+ gltf: gltf,
+ minimumHeight: minimumHeight,
+ maximumHeight: maximumHeight,
+ boundingSphere: boundingSphere,
+ orientedBoundingBox: orientedBoundingBox,
+ horizonOcclusionPoint: horizonOcclusionPoint,
+ skirtHeight: skirtHeight,
+ requestVertexNormals: that._requestVertexNormals,
+ childTileMask: childTileMask,
+ credits: that._tileCredits,
+ waterMask: waterMask,
+ });
+
+ return Promise.resolve(terrainData);
+ } catch (err) {
+ console.log(
+ `Could not load subtree: ${rootId} ${subtreeCoord.level} ${subtreeCoord.x} ${subtreeCoord.y}: ${err}`,
+ );
+
+ console.log(
+ `Could not load tile: ${rootId} ${tileCoord.level} ${tileCoord.x} ${tileCoord.y}: ${err}`,
+ );
+ return undefined;
+ }
+};
+
+/**
+ * Determines whether data for a tile is available to be loaded.
+ *
+ * @param {number} x The X coordinate of the tile for which to request geometry.
+ * @param {number} y The Y coordinate of the tile for which to request geometry.
+ * @param {number} level The level of the tile for which to request geometry.
+ * @returns {boolean|undefined} Undefined if not supported or availability is unknown, otherwise true or false.
+ */
+Cesium3DTilesTerrainProvider.prototype.getTileDataAvailable = function (
+ x,
+ y,
+ level,
+) {
+ const cache = this._subtreeCache;
+
+ const rootId = getRootIdFromGeographic(level, x);
+ const implicitTileset = rootId === 0 ? this._tileset0 : this._tileset1;
+ const tileCoord = getImplicitTileCoordinatesFromGeographicCoordinates(
+ implicitTileset,
+ level,
+ x,
+ y,
+ );
+
+ const subtreeCoord = tileCoord.getSubtreeCoordinates();
+ const subtree = cache.find(rootId, subtreeCoord);
+
+ // If the subtree is loaded, return the tile's availability
+ if (subtree !== undefined) {
+ const available = subtree.tileIsAvailableAtCoordinates(tileCoord);
+ return available;
+ }
+
+ if (subtreeCoord.isImplicitTilesetRoot()) {
+ if (tileCoord.isSubtreeRoot()) {
+ // The subtree's root tile is always available
+ return true;
+ }
+ // Don't know if the tile is available because its subtree hasn't been loaded yet
+ return undefined;
+ }
+
+ const parentSubtreeCoord = subtreeCoord.getParentSubtreeCoordinates();
+
+ // Check the parent subtree's child subtree availability to know if this subtree is available.
+ const parentSubtree = cache.find(rootId, parentSubtreeCoord);
+ if (parentSubtree !== undefined) {
+ const isChildSubtreeAvailable =
+ parentSubtree.childSubtreeIsAvailableAtCoordinates(subtreeCoord);
+
+ if (isChildSubtreeAvailable) {
+ return tileCoord.isSubtreeRoot()
+ ? true // The root tile of the subtree is always available
+ : undefined; // Don't know if the tile is available because the subtree hasn't been loaded yet
+ }
+ // Child subtree not available, so this tile isn't either
+ return false;
+ }
+
+ // The parent subtree isn't loaded either, so we don't even know if the child subtree is available
+ return undefined;
+};
+
+/**
+ * Gets the root ID from geographic tile coordinates.
+ * There are 2 root tiles in a geographic tiling scheme: one for each hemisphere.
+ * @private
+ * @param {number} level The level of the tile
+ * @param {number} x The x coordinate of the tile
+ * @returns {number} The root tile ID (0 or 1)
+ */
+function getRootIdFromGeographic(level, x) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number("level", level);
+ Check.typeOf.number("x", x);
+ //>>includeEnd('debug');
+
+ const numberOfYTilesAtLevel = 1 << level;
+ const rootId = (x / numberOfYTilesAtLevel) | 0;
+ return rootId;
+}
+
+/**
+ * Gets the implicit tile coordinates from geographic tile coordinates.
+ * @private
+ * @param {ImplicitTileset} implicitTileset
+ * @param {number} level The level of the tile
+ * @param {number} x The x coordinate of the tile
+ * @param {number} y The y coordinate of the tile
+ * @returns {ImplicitTileCoordinates} The implicit tile coordinates
+ */
+function getImplicitTileCoordinatesFromGeographicCoordinates(
+ implicitTileset,
+ level,
+ x,
+ y,
+) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("implicitTileset", implicitTileset);
+ Check.typeOf.number("level", level);
+ Check.typeOf.number("x", x);
+ Check.typeOf.number("y", y);
+ //>>includeEnd('debug');
+
+ const numberOfYTilesAtLevel = 1 << level;
+ const implicitLevel = level;
+ const implicitX = x % numberOfYTilesAtLevel;
+ const implicitY = numberOfYTilesAtLevel - y - 1;
+ const { subdivisionScheme, subtreeLevels } = implicitTileset;
+
+ return new ImplicitTileCoordinates({
+ subdivisionScheme: subdivisionScheme,
+ subtreeLevels: subtreeLevels,
+ level: implicitLevel,
+ x: implicitX,
+ y: implicitY,
+ });
+}
+
+/**
+ * Loads the water mask image texture from the glTF.
+ * @private
+ * @param {Object.} gltf The glTF JSON.
+ * @param {Resource} gltfResource The resource pointing to the glTF.
+ * @returns {Promise|undefined} A promise that resolves to the loaded image. Returns undefined if request.throttle is true and the request does not have high enough priority.
+ */
+async function loadWaterMask(gltf, gltfResource) {
+ const extension = gltf.extensions?.["EXT_structural_metadata"];
+ if (!defined(extension) || !defined(extension.propertyTextures)) {
+ return;
+ }
+
+ const schemaLoader = new MetadataSchemaLoader({
+ schema: extension.schema,
+ });
+
+ await schemaLoader.load();
+ const schema = schemaLoader.schema;
+
+ let metadataClass, waterMaskProperty;
+ if (defined(schema.classes)) {
+ for (const classId in schema.classes) {
+ if (schema.classes.hasOwnProperty(classId)) {
+ metadataClass = schema.classes[classId];
+ waterMaskProperty = metadataClass.propertiesBySemantic["WATERMASK"];
+ if (defined(waterMaskProperty)) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (!defined(waterMaskProperty)) {
+ return;
+ }
+
+ const propertyTextureData = extension.propertyTextures.find(
+ (data) => data.class === metadataClass.id,
+ );
+ if (!defined(propertyTextureData)) {
+ throw new DeveloperError(
+ `Expected a propertyTexture with a class ${metadataClass.id}`,
+ );
+ }
+
+ const textureInfo = propertyTextureData.properties[waterMaskProperty.id];
+ const texture = gltf.textures[textureInfo.index];
+ const bufferViewId = gltf.images[texture.source]?.bufferView;
+
+ const bufferViewLoader = ResourceCache.getBufferViewLoader({
+ gltf: gltf,
+ bufferViewId: bufferViewId,
+ gltfResource: gltfResource,
+ baseResource: gltfResource,
+ });
+ await bufferViewLoader.load();
+
+ const image = await loadImageFromTypedArray({
+ uint8Array: new Uint8Array(bufferViewLoader.typedArray),
+ format: "image/png",
+ flipY: false,
+ skipColorSpaceConversion: true,
+ });
+
+ return image;
+}
+
+/**
+ * Checks whether a child tile at a given tile coordinate is available in the given subtree.
+ * @private
+ * @param {ImplicitTileset} implicitTileset The implicit tileset
+ * @param {ImplicitSubtree} subtree The subtree
+ * @param {ImplicitTileCoordinates} coord The tile coordinates of the parent tile
+ * @param {number} x The x coordinate of the child tile
+ * @param {number} y The y coordinate of the child tile
+ * @returns {boolean} true if the child tile is available. false otherwise.
+ */
+function isChildAvailable(implicitTileset, subtree, coord, x, y) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("implicitTileset", implicitTileset);
+ Check.typeOf.object("subtree", subtree);
+ Check.typeOf.object("coord", coord);
+ Check.typeOf.number("x", x);
+ Check.typeOf.number("y", y);
+ //>>includeEnd('debug');
+
+ // For terrain it's required that the root tile of any available subtree is also available, so
+ // when the child tile belongs to a child subtree, we only need to check if the child subtree itself is available.
+ const isBottomOfSubtree = coord.isBottomOfSubtree();
+
+ const localLevel = 1;
+ const offset = getImplicitTileCoordinates(implicitTileset, localLevel, x, y);
+ const childCoord = coord.getDescendantCoordinates(offset);
+
+ const isAvailable = isBottomOfSubtree
+ ? subtree.childSubtreeIsAvailableAtCoordinates(childCoord)
+ : subtree.tileIsAvailableAtCoordinates(childCoord);
+
+ return isAvailable;
+}
+
+/**
+ * Gets the implicit tile coordinates from geographic tile coordinates.
+ * @private
+ * @param {ImplicitTileset} implicitTileset
+ * @param {number} level The level of the tile
+ * @param {number} x The x coordinate of the tile
+ * @param {number} y The y coordinate of the tile
+ * @returns {ImplicitTileCoordinates}
+ */
+function getImplicitTileCoordinates(implicitTileset, level, x, y) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("implicitTileset", implicitTileset);
+ Check.typeOf.number("level", level);
+ Check.typeOf.number("x", x);
+ Check.typeOf.number("y", y);
+ //>>includeEnd('debug');
+
+ const { subdivisionScheme, subtreeLevels } = implicitTileset;
+
+ return new ImplicitTileCoordinates({
+ subdivisionScheme: subdivisionScheme,
+ subtreeLevels: subtreeLevels,
+ level: level,
+ x: x,
+ y: y,
+ });
+}
+
+/**
+ * Make sure we load availability data for a tile
+ *
+ * @param {number} _x The X coordinate of the tile for which to request geometry.
+ * @param {number} _y The Y coordinate of the tile for which to request geometry.
+ * @param {number} _level The level of the tile for which to request geometry.
+ * @returns {Promise|undefined} Undefined if nothing need to be loaded or a Promise that resolves when all required tiles are loaded
+ */
+Cesium3DTilesTerrainProvider.prototype.loadTileDataAvailability = function (
+ _x,
+ _y,
+ _level,
+) {
+ return undefined;
+};
+
+/**
+ * Get the maximum geometric error allowed in a tile at a given level.
+ *
+ * @param {number} level The tile level for which to get the maximum geometric error.
+ * @returns {number} The maximum geometric error.
+ */
+Cesium3DTilesTerrainProvider.prototype.getLevelMaximumGeometricError =
+ function (level) {
+ const ellipsoid = this._ellipsoid;
+ const rootError =
+ TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(
+ ellipsoid,
+ 64,
+ 2,
+ );
+ return rootError / (1 << level);
+ };
+
+Object.defineProperties(Cesium3DTilesTerrainProvider.prototype, {
+ /**
+ * Gets an event that is raised when the terrain provider encounters an asynchronous error. By subscribing
+ * to the event, you will be notified of the error and can potentially recover from it. Event listeners
+ * are passed an instance of {@link TileProviderError}.
+ * @memberof Cesium3DTilesTerrainProvider.prototype
+ * @type {Event}
+ */
+ errorEvent: {
+ get: function () {
+ return this._errorEvent;
+ },
+ },
+
+ /**
+ * Gets the credit to display when this terrain provider is active. Typically this is used to credit
+ * the source of the terrain.
+ * @memberof Cesium3DTilesTerrainProvider.prototype
+ * @type {Credit}
+ */
+ credit: {
+ get: function () {
+ return this._credit;
+ },
+ },
+
+ /**
+ * Gets the tiling scheme used by the provider.
+ * @memberof Cesium3DTilesTerrainProvider.prototype
+ * @type {TilingScheme}
+ */
+ tilingScheme: {
+ get: function () {
+ return this._tilingScheme;
+ },
+ },
+
+ /**
+ * Gets a value indicating whether or not the provider includes a water mask. The water mask
+ * indicates which areas of the globe are water rather than land, so they can be rendered
+ * as a reflective surface with animated waves.
+ * @memberof Cesium3DTilesTerrainProvider.prototype
+ * @type {boolean}
+ */
+ hasWaterMask: {
+ get: function () {
+ return this._requestWaterMask;
+ },
+ },
+
+ /**
+ * Gets a value indicating whether or not the requested tiles include vertex normals.
+ * @memberof Cesium3DTilesTerrainProvider.prototype
+ * @type {boolean}
+ */
+ hasVertexNormals: {
+ get: function () {
+ return this._requestVertexNormals;
+ },
+ },
+
+ /**
+ * Gets an object that can be used to determine availability of terrain from this provider, such as
+ * at points and in rectangles.
+ * @memberof Cesium3DTilesTerrainProvider.prototype
+ * @type {TileAvailability|undefined}
+ */
+ availability: {
+ get: function () {
+ return this._subtreeCache;
+ },
+ },
+});
+
+/**
+ * A node in the implicit subtree cache.
+ * @private
+ * @constructor
+ * @param {number} rootId The root tile ID (0 or 1)
+ * @param {ImplicitSubtree} subtree The subtree
+ * @param {number} stamp The timestamp used for priority ordering
+ */
+function ImplicitSubtreeCacheNode(rootId, subtree, stamp) {
+ this.rootId = rootId;
+ this.subtree = subtree;
+ this.stamp = stamp;
+}
+
+/**
+ * A cache for implicit subtrees.
+ * @private
+ * @constructor
+ * @param {object} options Object with the following properties
+ * @param {Cesium3DTilesTerrainProvider} options.provider
+ * @param {number} [options.maximumSubtreeCount=0] The total number of subtrees this cache can store. If adding a new subtree would exceed this limit, the lowest priority subtrees will be removed until there is room, unless the subtree that is going to be removed is the parent of the new subtree, in which case it will not be removed and the new subtree will still be added, exceeding the memory limit.
+ */
+function ImplicitSubtreeCache(options) {
+ this._maximumSubtreeCount = options.maximumSubtreeCount ?? 0;
+ this._subtreeRequestCounter = 0;
+
+ this._queue = new DoubleEndedPriorityQueue({
+ comparator: ImplicitSubtreeCache.comparator,
+ });
+
+ this._provider = options.provider;
+}
+
+/**
+ * @param {ImplicitSubtreeCacheNode} a
+ * @param {ImplicitSubtreeCacheNode} b
+ * @returns {number}
+ */
+ImplicitSubtreeCache.comparator = function (a, b) {
+ const aCoord = a.subtree.implicitCoordinates;
+ const bCoord = b.subtree.implicitCoordinates;
+ if (aCoord.isAncestor(bCoord)) {
+ // Technically this shouldn't happen because the ancestor subtree was supposed to be added to the cache first.
+ return +1.0;
+ } else if (bCoord.isAncestor(aCoord)) {
+ return -1.0;
+ }
+ return a.stamp - b.stamp;
+};
+
+/**
+ * Adds a subtree to the cache.
+ * @param {number} rootId The root tile ID (0 or 1)
+ * @param {ImplicitSubtree} subtree The subtree
+ */
+ImplicitSubtreeCache.prototype.addSubtree = function (rootId, subtree) {
+ const cacheNode = new ImplicitSubtreeCacheNode(
+ rootId,
+ subtree,
+ this._subtreeRequestCounter,
+ );
+ this._queue.insert(cacheNode);
+
+ this._subtreeRequestCounter++;
+
+ const subtreeCoord = subtree.implicitCoordinates;
+
+ // Make sure the parent subtree exists in the cache
+ if (subtreeCoord.level > 0) {
+ const parentCoord = subtreeCoord.getParentSubtreeCoordinates();
+ const parentNode = this.find(rootId, parentCoord);
+
+ //>>includeStart('debug', pragmas.debug)
+ if (parentNode === undefined) {
+ throw new DeveloperError("parent node needs to exist");
+ }
+ //>>includeEnd('debug');
+ }
+
+ if (this._maximumSubtreeCount > 0) {
+ while (this._queue.length > this._maximumSubtreeCount) {
+ const lowestPriorityNode = this._queue.getMinimum();
+ if (lowestPriorityNode === cacheNode) {
+ // Don't remove itself
+ break;
+ }
+
+ this._queue.removeMinimum();
+ }
+ }
+};
+
+/**
+ * Finds a subtree in the cache.
+ * @param {number} rootId The root tile ID (0 or 1)
+ * @param {ImplicitTileCoordinates} subtreeCoord The coordinates of the subtree
+ * @returns {ImplicitSubtree|undefined} The subtree if found; otherwise undefined.
+ */
+ImplicitSubtreeCache.prototype.find = function (rootId, subtreeCoord) {
+ const queue = this._queue;
+ const array = queue.internalArray;
+ const { level, x, y } = subtreeCoord;
+
+ for (let i = 0; i < queue.length; i++) {
+ const other = array[i];
+ const otherRootId = other.rootId;
+ const otherCoord = other.subtree.implicitCoordinates;
+ if (
+ otherRootId === rootId &&
+ otherCoord.level === level &&
+ otherCoord.x === x &&
+ otherCoord.y === y
+ ) {
+ return other.subtree;
+ }
+ }
+ return undefined;
+};
+
+/**
+ * Determines the {@link ImplicitTileCoordinates} of the most detailed tile covering the position.
+ *
+ * @param {Cartographic} position The position for which to determine the maximum coordinates. The height component is ignored.
+ * @returns {ImplicitTileCoordinates|undefined} The coordinates of the most detailed tile covering the position.
+ * @throws {DeveloperError} If position is outside any tile according to the tiling scheme.
+ */
+
+ImplicitSubtreeCache.prototype._computeMaximumImplicitTileCoordinatesAtPosition =
+ function (position) {
+ const { longitude, latitude } = position;
+ const provider = this._provider;
+ const rootId = longitude < 0.0 ? 0 : 1;
+ const implicitTileset =
+ rootId === 0 ? provider._tileset0 : provider._tileset1;
+ const subtreeLevels = implicitTileset.subtreeLevels;
+ const rootSubtreeCoord = getImplicitTileCoordinates(
+ implicitTileset,
+ 0,
+ 0,
+ 0,
+ );
+
+ let subtree = this.find(rootId, rootSubtreeCoord);
+ if (subtree === undefined) {
+ // Nothing has been loaded yet
+ return undefined;
+ }
+
+ let subtreeCoord = subtree.implicitCoordinates;
+ let subtreeX = subtreeCoord.x;
+ let subtreeY = subtreeCoord.y;
+ let subtreeLevel = subtreeCoord.level;
+
+ const globalMinimumLongitude = -CesiumMath.PI;
+ const globalMaximumLongitude = +CesiumMath.PI;
+ const rootLongitudeStart = CesiumMath.lerp(
+ globalMinimumLongitude,
+ globalMaximumLongitude,
+ rootId / 2.0,
+ );
+ const rootLongitudeEnd = CesiumMath.lerp(
+ globalMinimumLongitude,
+ globalMaximumLongitude,
+ (rootId + 1) / 2,
+ );
+ const rootLatitudeStart = -CesiumMath.PI * 0.5;
+ const rootLatitudeEnd = +CesiumMath.PI * 0.5;
+
+ let u = 0.0;
+ let v = 0.0;
+
+ // Find the deepest available subtree
+ let childSubtreeLoaded = true;
+ while (childSubtreeLoaded) {
+ const invDim = 1.0 / (1 << subtreeLevel);
+
+ const lonLength = (rootLongitudeEnd - rootLongitudeStart) * invDim;
+ const lonMin = rootLongitudeStart + subtreeX * lonLength;
+
+ const latLength = (rootLatitudeEnd - rootLatitudeStart) * invDim;
+ const latMin = rootLatitudeStart + subtreeY * latLength;
+
+ u = (longitude - lonMin) / lonLength;
+ v = (latitude - latMin) / latLength;
+
+ const childSubtreeCoord = computeDescendantCoordinatesAtUv(
+ implicitTileset,
+ subtreeCoord,
+ u,
+ v,
+ subtreeLevels,
+ );
+
+ if (subtree.childSubtreeIsAvailableAtCoordinates(childSubtreeCoord)) {
+ const childSubtree = this.find(rootId, childSubtreeCoord);
+ if (childSubtree !== undefined) {
+ subtree = childSubtree;
+ subtreeCoord = subtree.implicitCoordinates;
+ subtreeX = subtreeCoord.x;
+ subtreeY = subtreeCoord.y;
+ subtreeLevel = subtreeCoord.level;
+ } else {
+ // Child subtree is available but has not been loaded yet
+ // Since the root node of a subtree is always available, return the level of the child subtree
+ // sampleTerrainMostDetailed will keep calling this function until all available subtrees in the chain have been loaded
+ return childSubtreeCoord;
+ }
+ } else {
+ // Child subtree is not available
+ childSubtreeLoaded = false;
+ }
+ }
+
+ // Find the deepest level in the subtree
+ let deepestTileCoord;
+ for (let localLevel = 0; localLevel < subtreeLevels; localLevel++) {
+ const childCoord = computeDescendantCoordinatesAtUv(
+ implicitTileset,
+ subtreeCoord,
+ u,
+ v,
+ localLevel,
+ );
+
+ if (subtree.tileIsAvailableAtCoordinates(childCoord)) {
+ deepestTileCoord = childCoord;
+ } else {
+ break;
+ }
+ }
+
+ return deepestTileCoord;
+ };
+
+/**
+ * Computes the descendant tile coordinates at a given (u,v) location within a subtree.
+ * @private
+ * @param {ImplicitTileset} implicitTileset
+ * @param {ImplicitTileCoordinates} subtreeCoord
+ * @param {number} u
+ * @param {number} v
+ * @param {number} levelOffset
+ * @returns {ImplicitTileCoordinates} The parent subtree coordinate
+ */
+function computeDescendantCoordinatesAtUv(
+ implicitTileset,
+ subtreeCoord,
+ u,
+ v,
+ levelOffset,
+) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("implicitTileset", implicitTileset);
+ Check.typeOf.object("subtreeCoord", subtreeCoord);
+ Check.typeOf.number("u", u);
+ Check.typeOf.number("v", v);
+ Check.typeOf.number("levelOffset", levelOffset);
+ //>>includeEnd('debug');
+
+ const dimension = 1 << levelOffset;
+ const localX = CesiumMath.clamp((u * dimension) | 0, 0, dimension - 1);
+ const localY = CesiumMath.clamp((v * dimension) | 0, 0, dimension - 1);
+ const offset = getImplicitTileCoordinates(
+ implicitTileset,
+ levelOffset,
+ localX,
+ localY,
+ );
+ return subtreeCoord.getDescendantCoordinates(offset);
+}
+
+// NOTE: ImplicitSubtreeCache implements just enough of the TileAvailability interface to support `sampleTerrain` and `sampleTerrainMostDetailed`.
+// Right now this just means implementing `computeMaximumLevelAtPosition`.
+// It's more difficult to implement the rest of the methods because doing everything in terms of ranges instead of bits is kind of awkward.
+
+/**
+ * Determines the level of the most detailed tile covering the position.
+ *
+ * @param {Cartographic} position The position for which to determine the maximum available level. The height component is ignored.
+ * @returns {number} The level of the most detailed tile covering the position.
+ * @throws {DeveloperError} If position is outside any tile according to the tiling scheme.
+ */
+ImplicitSubtreeCache.prototype.computeMaximumLevelAtPosition = function (
+ position,
+) {
+ const tileCoordinates =
+ this._computeMaximumImplicitTileCoordinatesAtPosition(position);
+ if (tileCoordinates === undefined) {
+ return 0;
+ }
+ return tileCoordinates.level;
+};
+
+export default Cesium3DTilesTerrainProvider;
diff --git a/packages/engine/Source/Core/HeightmapTerrainData.js b/packages/engine/Source/Core/HeightmapTerrainData.js
index 3f08f662dafe..6d2b5179a19b 100644
--- a/packages/engine/Source/Core/HeightmapTerrainData.js
+++ b/packages/engine/Source/Core/HeightmapTerrainData.js
@@ -94,16 +94,12 @@ import TerrainProvider from "./TerrainProvider.js";
* @see GoogleEarthEnterpriseTerrainData
*/
function HeightmapTerrainData(options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
//>>includeStart('debug', pragmas.debug);
- if (!defined(options) || !defined(options.buffer)) {
- throw new DeveloperError("options.buffer is required.");
- }
- if (!defined(options.width)) {
- throw new DeveloperError("options.width is required.");
- }
- if (!defined(options.height)) {
- throw new DeveloperError("options.height is required.");
- }
+ Check.defined("options.buffer", options.buffer);
+ Check.defined("options.width", options.width);
+ Check.defined("options.height", options.height);
//>>includeEnd('debug');
this._buffer = options.buffer;
@@ -158,7 +154,7 @@ Object.defineProperties(HeightmapTerrainData.prototype, {
* Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land.
* Values in between 0 and 255 are allowed as well to smoothly blend between land and water.
* @memberof HeightmapTerrainData.prototype
- * @type {Uint8Array|HTMLImageElement|HTMLCanvasElement|undefined}
+ * @type {Uint8Array|HTMLImageElement|HTMLCanvasElement|ImageBitmap|undefined}
*/
waterMask: {
get: function () {
@@ -645,18 +641,10 @@ HeightmapTerrainData.prototype.isChildAvailable = function (
childY,
) {
//>>includeStart('debug', pragmas.debug);
- if (!defined(thisX)) {
- throw new DeveloperError("thisX is required.");
- }
- if (!defined(thisY)) {
- throw new DeveloperError("thisY is required.");
- }
- if (!defined(childX)) {
- throw new DeveloperError("childX is required.");
- }
- if (!defined(childY)) {
- throw new DeveloperError("childY is required.");
- }
+ Check.typeOf.number("thisX", thisX);
+ Check.typeOf.number("thisY", thisY);
+ Check.typeOf.number("childX", childX);
+ Check.typeOf.number("childY", childY);
//>>includeEnd('debug');
let bitNumber = 2; // northwest child
diff --git a/packages/engine/Source/Core/QuantizedMeshTerrainData.js b/packages/engine/Source/Core/QuantizedMeshTerrainData.js
index 626dfb4e6b9c..1a95d5547625 100644
--- a/packages/engine/Source/Core/QuantizedMeshTerrainData.js
+++ b/packages/engine/Source/Core/QuantizedMeshTerrainData.js
@@ -92,52 +92,23 @@ import TerrainMesh from "./TerrainMesh.js";
* @see GoogleEarthEnterpriseTerrainData
*/
function QuantizedMeshTerrainData(options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
//>>includeStart('debug', pragmas.debug)
- if (!defined(options) || !defined(options.quantizedVertices)) {
- throw new DeveloperError("options.quantizedVertices is required.");
- }
- if (!defined(options.indices)) {
- throw new DeveloperError("options.indices is required.");
- }
- if (!defined(options.minimumHeight)) {
- throw new DeveloperError("options.minimumHeight is required.");
- }
- if (!defined(options.maximumHeight)) {
- throw new DeveloperError("options.maximumHeight is required.");
- }
- if (!defined(options.maximumHeight)) {
- throw new DeveloperError("options.maximumHeight is required.");
- }
- if (!defined(options.boundingSphere)) {
- throw new DeveloperError("options.boundingSphere is required.");
- }
- if (!defined(options.horizonOcclusionPoint)) {
- throw new DeveloperError("options.horizonOcclusionPoint is required.");
- }
- if (!defined(options.westIndices)) {
- throw new DeveloperError("options.westIndices is required.");
- }
- if (!defined(options.southIndices)) {
- throw new DeveloperError("options.southIndices is required.");
- }
- if (!defined(options.eastIndices)) {
- throw new DeveloperError("options.eastIndices is required.");
- }
- if (!defined(options.northIndices)) {
- throw new DeveloperError("options.northIndices is required.");
- }
- if (!defined(options.westSkirtHeight)) {
- throw new DeveloperError("options.westSkirtHeight is required.");
- }
- if (!defined(options.southSkirtHeight)) {
- throw new DeveloperError("options.southSkirtHeight is required.");
- }
- if (!defined(options.eastSkirtHeight)) {
- throw new DeveloperError("options.eastSkirtHeight is required.");
- }
- if (!defined(options.northSkirtHeight)) {
- throw new DeveloperError("options.northSkirtHeight is required.");
- }
+ Check.defined("options.quantizedVertices", options.quantizedVertices);
+ Check.defined("options.indices", options.indices);
+ Check.defined("options.minimumHeight", options.minimumHeight);
+ Check.defined("options.maximumHeight", options.maximumHeight);
+ Check.defined("options.boundingSphere", options.boundingSphere);
+ Check.defined("options.horizonOcclusionPoint", options.horizonOcclusionPoint);
+ Check.defined("options.westIndices", options.westIndices);
+ Check.defined("options.southIndices", options.southIndices);
+ Check.defined("options.eastIndices", options.eastIndices);
+ Check.defined("options.northIndices", options.northIndices);
+ Check.defined("options.westSkirtHeight", options.westSkirtHeight);
+ Check.defined("options.southSkirtHeight", options.southSkirtHeight);
+ Check.defined("options.eastSkirtHeight", options.eastSkirtHeight);
+ Check.defined("options.northSkirtHeight", options.northSkirtHeight);
//>>includeEnd('debug');
this._quantizedVertices = options.quantizedVertices;
@@ -729,18 +700,10 @@ QuantizedMeshTerrainData.prototype.isChildAvailable = function (
childY,
) {
//>>includeStart('debug', pragmas.debug);
- if (!defined(thisX)) {
- throw new DeveloperError("thisX is required.");
- }
- if (!defined(thisY)) {
- throw new DeveloperError("thisY is required.");
- }
- if (!defined(childX)) {
- throw new DeveloperError("childX is required.");
- }
- if (!defined(childY)) {
- throw new DeveloperError("childY is required.");
- }
+ Check.typeOf.number("thisX", thisX);
+ Check.typeOf.number("thisY", thisY);
+ Check.typeOf.number("childX", childX);
+ Check.typeOf.number("childY", childY);
//>>includeEnd('debug');
let bitNumber = 2; // northwest child
diff --git a/packages/engine/Source/Core/Resource.js b/packages/engine/Source/Core/Resource.js
index 7e4416e89acd..726553154144 100644
--- a/packages/engine/Source/Core/Resource.js
+++ b/packages/engine/Source/Core/Resource.js
@@ -2027,6 +2027,12 @@ Resource._Implementations.createImage = function (
* Wrapper for createImageBitmap
*
* @private
+ * @param {Blob} blob The image blob.
+ * @param {object} options An object containing the following properties:
+ * @param {boolean} options.flipY Whether to flip the image Y axis.
+ * @param {boolean} options.premultiplyAlpha Whether to premultiply the alpha channel.
+ * @param {boolean} options.skipColorSpaceConversion Whether to skip color space conversion.
+ * @returns {Promise} A promise that resolves to the created image bitmap.
*/
Resource.createImageBitmapFromBlob = function (blob, options) {
Check.defined("options", options);
diff --git a/packages/engine/Source/Core/TerrainData.js b/packages/engine/Source/Core/TerrainData.js
index 63ecb9a51ce3..072ce8dcde6e 100644
--- a/packages/engine/Source/Core/TerrainData.js
+++ b/packages/engine/Source/Core/TerrainData.js
@@ -29,7 +29,7 @@ Object.defineProperties(TerrainData.prototype, {
* Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land.
* Values in between 0 and 255 are allowed as well to smoothly blend between land and water.
* @memberof TerrainData.prototype
- * @type {Uint8Array|HTMLImageElement|HTMLCanvasElement|undefined}
+ * @type {Uint8Array|HTMLImageElement|HTMLCanvasElement|ImageBitmap|undefined}
*/
waterMask: {
get: DeveloperError.throwInstantiationError,
diff --git a/packages/engine/Source/Core/TerrainEncoding.js b/packages/engine/Source/Core/TerrainEncoding.js
index 019efbbfd7c2..bd3c81523b5c 100644
--- a/packages/engine/Source/Core/TerrainEncoding.js
+++ b/packages/engine/Source/Core/TerrainEncoding.js
@@ -76,32 +76,36 @@ function TerrainEncoding(
quantization = TerrainQuantization.NONE;
}
- toENU = Matrix4.inverseTransformation(fromENU, new Matrix4());
-
- const translation = Cartesian3.negate(minimum, cartesian3Scratch);
- Matrix4.multiply(
- Matrix4.fromTranslation(translation, matrix4Scratch),
- toENU,
- toENU,
+ // Scale and bias from [0,1] to [ENU min, ENU max]
+ // Also compute the inverse of the scale and bias
+ let st = Matrix4.fromScale(dimensions, matrix4Scratch);
+ st = Matrix4.setTranslation(st, minimum, st);
+
+ let invSt = Matrix4.fromScale(
+ Cartesian3.fromElements(
+ 1.0 / dimensions.x,
+ 1.0 / dimensions.y,
+ 1.0 / dimensions.z,
+ cartesian3Scratch,
+ ),
+ matrix4Scratch2,
+ );
+ invSt = Matrix4.multiplyByTranslation(
+ invSt,
+ Cartesian3.negate(minimum, cartesian3Scratch),
+ invSt,
);
- const scale = cartesian3Scratch;
- scale.x = 1.0 / dimensions.x;
- scale.y = 1.0 / dimensions.y;
- scale.z = 1.0 / dimensions.z;
- Matrix4.multiply(Matrix4.fromScale(scale, matrix4Scratch), toENU, toENU);
-
- matrix = Matrix4.clone(fromENU);
- Matrix4.setTranslation(matrix, Cartesian3.ZERO, matrix);
-
- fromENU = Matrix4.clone(fromENU, new Matrix4());
+ matrix = Matrix4.clone(fromENU, new Matrix4());
+ let rtcOffset = Matrix4.getTranslation(fromENU, cartesian3Scratch);
+ rtcOffset = Cartesian3.subtract(rtcOffset, center, cartesian3Scratch);
+ matrix = Matrix4.setTranslation(matrix, rtcOffset, matrix);
+ matrix = Matrix4.multiply(matrix, st, matrix);
- const translationMatrix = Matrix4.fromTranslation(minimum, matrix4Scratch);
- const scaleMatrix = Matrix4.fromScale(dimensions, matrix4Scratch2);
- const st = Matrix4.multiply(translationMatrix, scaleMatrix, matrix4Scratch);
+ toENU = Matrix4.inverseTransformation(fromENU, new Matrix4());
+ toENU = Matrix4.multiply(invSt, toENU, toENU);
- Matrix4.multiply(fromENU, st, fromENU);
- Matrix4.multiply(matrix, st, matrix);
+ fromENU = Matrix4.multiply(fromENU, st, new Matrix4());
}
/**
@@ -570,6 +574,23 @@ TerrainEncoding.prototype.getOctEncodedNormal = function (
return Cartesian2.fromElements(x, y, result);
};
+/**
+ * @param {Float32Array} buffer
+ * @param {number} index
+ * @param {Cartesian3} result
+ * @returns {Cartesian3}
+ */
+TerrainEncoding.prototype.decodeNormal = function (buffer, index, result) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object("buffer", buffer);
+ Check.typeOf.number("index", index);
+ Check.typeOf.object("result", result);
+ //>>includeEnd('debug');
+
+ const bufferIndex = (index = index * this.stride + this._offsetVertexNormal);
+ return AttributeCompression.octDecodeFloat(buffer[bufferIndex], result);
+};
+
/**
* Decode a geodetic surface normal from the vertex buffer.
*
diff --git a/packages/engine/Source/Core/TerrainProvider.js b/packages/engine/Source/Core/TerrainProvider.js
index 8963baa19273..910da89c63a9 100644
--- a/packages/engine/Source/Core/TerrainProvider.js
+++ b/packages/engine/Source/Core/TerrainProvider.js
@@ -236,7 +236,79 @@ TerrainProvider.getRegularGridAndSkirtIndicesAndEdgeIndices = function (
};
/**
+ * Calculates the number of skirt vertices given the edge indices.
* @private
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} westIndicesSouthToNorth Edge indices along the west side of the tile.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} southIndicesEastToWest Edge indices along the south side of the tile.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} eastIndicesNorthToSouth Edge indices along the east side of the tile.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} northIndicesWestToEast Edge indices along the north side of the tile.
+ * @returns {number} The number of skirt vertices.
+ */
+TerrainProvider.getSkirtVertexCount = function (
+ westIndicesSouthToNorth,
+ southIndicesEastToWest,
+ eastIndicesNorthToSouth,
+ northIndicesWestToEast,
+) {
+ return (
+ westIndicesSouthToNorth.length +
+ southIndicesEastToWest.length +
+ eastIndicesNorthToSouth.length +
+ northIndicesWestToEast.length
+ );
+};
+
+/**
+ * Compute the number of skirt indices given the number of skirt vertices.
+ * Consider a 3x3 grid of vertices. There will be 8 skirt vertices around the edge:
+ * - 16 edge triangles
+ * - 48 indices
+ *
+ * |\|\|
+ * |/| |/|
+ * |/| |/|
+ * |\|\|
+ *
+ * @private
+ * @param {number} skirtVertexCount
+ * @returns {number}
+ */
+TerrainProvider.getSkirtIndexCount = function (skirtVertexCount) {
+ return (skirtVertexCount - 4) * 2 * 3;
+};
+
+/**
+ * Compute the number of skirt indices given the number of skirt vertices with filled corners.
+ * Consider a 3x3 grid of vertices. There will be 8 skirt vertices around the edge:
+ * - 16 edge triangles
+ * - 4 cap triangles
+ * - 60 indices
+ *
+ * /|\|\|\
+ * |/| |/|
+ * |/| |/|
+ * \|\|\|/
+ *
+ * @private
+ * @param {number} skirtVertexCount
+ * @returns {number}
+ */
+TerrainProvider.getSkirtIndexCountWithFilledCorners = function (
+ skirtVertexCount,
+) {
+ return ((skirtVertexCount - 4) * 2 + 4) * 3;
+};
+
+/**
+ * Adds skirt indices.
+ * @private
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} westIndicesSouthToNorth The indices of the vertices on the Western edge of the tile, ordered from South to North.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} southIndicesEastToWest The indices of the vertices on the Southern edge of the tile, ordered from East to West.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} eastIndicesNorthToSouth The indices of the vertices on the Eastern edge of the tile, ordered from North to South.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} northIndicesWestToEast The indices of the vertices on the Northern edge of the tile, ordered from West to East.
+ * @param {number} vertexCount The number of vertices in the tile before adding skirt vertices.
+ * @param {Uint16Array|Uint32Array} indices The array of indices to which skirt indices are added.
+ * @param {number} offset The offset into the indices array at which to start adding skirt indices.
*/
TerrainProvider.addSkirtIndices = function (
westIndicesSouthToNorth,
@@ -272,6 +344,82 @@ TerrainProvider.addSkirtIndices = function (
addSkirtIndices(northIndicesWestToEast, vertexIndex, indices, offset);
};
+/**
+ * Adds skirt indices with filled corners.
+ * @private
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} westIndicesSouthToNorth The indices of the vertices on the Western edge of the tile, ordered from South to North.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} southIndicesEastToWest The indices of the vertices on the Southern edge of the tile, ordered from East to West.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} eastIndicesNorthToSouth The indices of the vertices on the Eastern edge of the tile, ordered from North to South.
+ * @param {number[]|Uint8Array|Uint16Array|Uint32Array} northIndicesWestToEast The indices of the vertices on the Northern edge of the tile, ordered from West to East.
+ * @param {number} vertexCount The number of vertices in the tile before adding skirt vertices.
+ * @param {Uint16Array|Uint32Array} indices The array of indices to which skirt indices are added.
+ * @param {number} offset The offset into the indices array at which to start adding skirt indices.
+ */
+TerrainProvider.addSkirtIndicesWithFilledCorners = function (
+ westIndicesSouthToNorth,
+ southIndicesEastToWest,
+ eastIndicesNorthToSouth,
+ northIndicesWestToEast,
+ vertexCount,
+ indices,
+ offset,
+) {
+ // Add skirt indices without filled corners
+ TerrainProvider.addSkirtIndices(
+ westIndicesSouthToNorth,
+ southIndicesEastToWest,
+ eastIndicesNorthToSouth,
+ northIndicesWestToEast,
+ vertexCount,
+ indices,
+ offset,
+ );
+
+ const skirtVertexCount = TerrainProvider.getSkirtVertexCount(
+ westIndicesSouthToNorth,
+ southIndicesEastToWest,
+ eastIndicesNorthToSouth,
+ northIndicesWestToEast,
+ );
+ const skirtIndexCountWithoutCaps =
+ TerrainProvider.getSkirtIndexCount(skirtVertexCount);
+
+ const cornerStartIdx = offset + skirtIndexCountWithoutCaps;
+
+ const cornerSWIndex = westIndicesSouthToNorth[0];
+ const cornerNWIndex = northIndicesWestToEast[0];
+ const cornerNEIndex = eastIndicesNorthToSouth[0];
+ const cornerSEIndex = southIndicesEastToWest[0];
+
+ // Indices based on edge order in addSkirtIndices
+ const westSouthIndex = vertexCount;
+ const westNorthIndex = westSouthIndex + westIndicesSouthToNorth.length - 1;
+ const southEastIndex = westNorthIndex + 1;
+ const southWestIndex = southEastIndex + southIndicesEastToWest.length - 1;
+ const eastNorthIndex = southWestIndex + 1;
+ const eastSouthIndex = eastNorthIndex + eastIndicesNorthToSouth.length - 1;
+ const northWestIndex = eastSouthIndex + 1;
+ const northEastIndex = northWestIndex + northIndicesWestToEast.length - 1;
+
+ // Connect the corner vertices with the skirt vertices extending from the corner
+
+ indices[cornerStartIdx + 0] = cornerSWIndex;
+ indices[cornerStartIdx + 1] = westSouthIndex;
+ indices[cornerStartIdx + 2] = southWestIndex;
+
+ indices[cornerStartIdx + 3] = cornerSEIndex;
+ indices[cornerStartIdx + 4] = southEastIndex;
+ indices[cornerStartIdx + 5] = eastSouthIndex;
+
+ indices[cornerStartIdx + 6] = cornerNEIndex;
+ indices[cornerStartIdx + 7] = eastNorthIndex;
+ indices[cornerStartIdx + 8] = northEastIndex;
+
+ indices[cornerStartIdx + 9] = cornerNWIndex;
+ indices[cornerStartIdx + 10] = northWestIndex;
+ indices[cornerStartIdx + 11] = westNorthIndex;
+};
+
function getEdgeIndices(width, height) {
const westIndicesSouthToNorth = new Array(height);
const southIndicesEastToWest = new Array(width);
diff --git a/packages/engine/Source/Core/binarySearch.js b/packages/engine/Source/Core/binarySearch.js
index 5cbb2890b96a..8619318f4226 100644
--- a/packages/engine/Source/Core/binarySearch.js
+++ b/packages/engine/Source/Core/binarySearch.js
@@ -4,7 +4,7 @@ import Check from "./Check.js";
* Finds an item in a sorted array.
*
* @function
- * @param {Array} array The sorted array to search.
+ * @param {Array|Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array} array The sorted array to search.
* @param {*} itemToFind The item to find in the array.
* @param {binarySearchComparator} comparator The function to use to compare the item to
* elements in the array.
diff --git a/packages/engine/Source/Core/loadImageFromTypedArray.js b/packages/engine/Source/Core/loadImageFromTypedArray.js
index ad843360d5ff..3db3cf7ae180 100644
--- a/packages/engine/Source/Core/loadImageFromTypedArray.js
+++ b/packages/engine/Source/Core/loadImageFromTypedArray.js
@@ -3,12 +3,18 @@ import defined from "./defined.js";
import Resource from "./Resource.js";
/**
+ * Loads an image from a typed array.
+ * @param {Object} options An object containing the following properties:
+ * @param {Uint8Array} options.uint8Array The typed array containing the image data.
+ * @param {string} options.format The MIME format of the image (e.g., "image/png").
+ * @param {Request} [options.request] The request object to use to fetch the image.
+ * @param {boolean} [options.flipY=false] Whether to flip the image vertically.
+ * @param {boolean} [options.skipColorSpaceConversion=false] Whether to skip color space conversion.
+ * @returns {Promise|undefined} A promise that resolves to the loaded image. Returns undefined if request.throttle is true and the request does not have high enough priority.
* @private
*/
function loadImageFromTypedArray(options) {
- const uint8Array = options.uint8Array;
- const format = options.format;
- const request = options.request;
+ const { uint8Array, format, request } = options;
const flipY = options.flipY ?? false;
const skipColorSpaceConversion = options.skipColorSpaceConversion ?? false;
//>>includeStart('debug', pragmas.debug);
diff --git a/packages/engine/Source/Core/mergeSort.js b/packages/engine/Source/Core/mergeSort.js
index ea60bd763811..7f84be7e6396 100644
--- a/packages/engine/Source/Core/mergeSort.js
+++ b/packages/engine/Source/Core/mergeSort.js
@@ -56,7 +56,7 @@ function sort(array, compare, userDefinedObject, start, end) {
* A stable merge sort.
*
* @function mergeSort
- * @param {Array} array The array to sort.
+ * @param {Array|Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array} array The array to sort.
* @param {mergeSortComparator} comparator The function to use to compare elements in the array.
* @param {*} [userDefinedObject] Any item to pass as the third parameter to comparator.
*
diff --git a/packages/engine/Source/Scene/GlobeSurfaceTile.js b/packages/engine/Source/Scene/GlobeSurfaceTile.js
index 039295b4a58f..05a65fbec7ef 100644
--- a/packages/engine/Source/Scene/GlobeSurfaceTile.js
+++ b/packages/engine/Source/Scene/GlobeSurfaceTile.js
@@ -929,7 +929,15 @@ function createWaterMaskTextureIfNeeded(context, surfaceTile) {
let texture;
const waterMaskLength = waterMask.length;
- if (waterMaskLength === 1) {
+ if (waterMask instanceof ImageBitmap) {
+ texture = Texture.create({
+ context: context,
+ source: waterMask,
+ sampler: waterMaskData.sampler,
+ flipY: false,
+ skipColorSpaceConversion: true,
+ });
+ } else if (waterMaskLength === 1) {
// Length 1 means the tile is entirely land or entirely water.
// A value of 0 indicates entirely land, a value of 1 indicates entirely water.
if (waterMask[0] !== 0) {
diff --git a/packages/engine/Source/Scene/GltfBufferViewLoader.js b/packages/engine/Source/Scene/GltfBufferViewLoader.js
index 659beefdac4a..5cdbf16418e6 100644
--- a/packages/engine/Source/Scene/GltfBufferViewLoader.js
+++ b/packages/engine/Source/Scene/GltfBufferViewLoader.js
@@ -125,6 +125,12 @@ Object.defineProperties(GltfBufferViewLoader.prototype, {
},
});
+/**
+ * Load the resources associated with the loader.
+ * @private
+ * @param {GltfBufferViewLoader} loader
+ * @returns {Promise}
+ */
async function loadResources(loader) {
try {
const bufferLoader = getBufferLoader(loader);
@@ -190,9 +196,18 @@ GltfBufferViewLoader.prototype.load = async function () {
return this._promise;
};
+/**
+ * Get the buffer loader for the specified buffer view loader.
+ * Attempts to retrieve from the resource cache first. If a buffer loader is
+ * not found, creates a new buffer loader and adds it to the resource cache.
+ * @private
+ * @param {GltfBufferViewLoader} bufferViewLoader The loader.
+ * @returns {BufferLoader} The buffer loader.
+ */
function getBufferLoader(bufferViewLoader) {
const resourceCache = bufferViewLoader._resourceCache;
const buffer = bufferViewLoader._buffer;
+
if (defined(buffer.uri)) {
const baseResource = bufferViewLoader._baseResource;
const resource = baseResource.getDerivedResource({
@@ -202,9 +217,12 @@ function getBufferLoader(bufferViewLoader) {
resource: resource,
});
}
+
+ const source = buffer.extras?._pipeline?.source;
return resourceCache.getEmbeddedBufferLoader({
parentResource: bufferViewLoader._gltfResource,
bufferId: bufferViewLoader._bufferId,
+ typedArray: source,
});
}
diff --git a/packages/engine/Source/Shaders/GlobeVS.glsl b/packages/engine/Source/Shaders/GlobeVS.glsl
index 53598926cf70..849b619e75c9 100644
--- a/packages/engine/Source/Shaders/GlobeVS.glsl
+++ b/packages/engine/Source/Shaders/GlobeVS.glsl
@@ -141,7 +141,7 @@ void main()
#elif defined(INCLUDE_WEB_MERCATOR_Y)
float webMercatorT = czm_decompressTextureCoordinates(compressed0.w).x;
float encodedNormal = 0.0;
-#elif defined(ENABLE_VERTEX_LIGHTING) || defined(GENERATE_POSITION_AND_NORMAL)
+#elif defined(ENABLE_VERTEX_LIGHTING) || defined(GENERATE_POSITION_AND_NORMAL) || defined(APPLY_MATERIAL)
float webMercatorT = textureCoordinates.y;
float encodedNormal = compressed0.w;
#else
diff --git a/packages/engine/Source/Workers/createVerticesFromCesium3DTilesTerrain.js b/packages/engine/Source/Workers/createVerticesFromCesium3DTilesTerrain.js
new file mode 100644
index 000000000000..010cb0566483
--- /dev/null
+++ b/packages/engine/Source/Workers/createVerticesFromCesium3DTilesTerrain.js
@@ -0,0 +1,46 @@
+import Cesium3DTilesTerrainGeometryProcessor from "../Core/Cesium3DTilesTerrainGeometryProcessor.js";
+import createTaskProcessorWorker from "./createTaskProcessorWorker.js";
+
+/**
+ * @private
+ *
+ * @param {Cesium3DTilesTerrainGeometryProcessor.CreateMeshOptions} options An object describing options for mesh creation.
+ * @param {ArrayBuffer[]} transferableObjects An array of buffers that can be transferred back to the main thread.
+ * @returns {Promise