From b450fb79325918bf87aedf816a9e1a079f14ee39 Mon Sep 17 00:00:00 2001 From: shisaiqun Date: Fri, 29 Jul 2022 15:11:22 +0800 Subject: [PATCH] GlobeSurfaceShaderSet.jsMerge branch 'projections' --- .eslintignore | 3 + Source/Core/BoundingSphere.js | 43 ++ Source/Core/GeographicProjection.js | 43 ++ .../Core/GoogleEarthEnterpriseTerrainData.js | 7 + Source/Core/GroundPolylineGeometry.js | 54 +-- Source/Core/HeightmapTerrainData.js | 41 +- Source/Core/HeightmapTessellator.js | 110 +++-- Source/Core/MapProjection.js | 77 ++++ Source/Core/QuantizedMeshTerrainData.js | 7 + Source/Core/Rectangle.js | 389 +++++++++++++++--- Source/Core/RectangleCollisionChecker.js | 37 +- Source/Core/TerrainData.js | 1 + Source/Core/TerrainEncoding.js | 74 +++- Source/Core/TerrainMesh.js | 9 +- Source/Core/WebMercatorProjection.js | 43 ++ Source/Core/sampleTerrain.js | 6 + Source/DataSources/GeometryVisualizer.js | 7 +- .../StaticGroundGeometryColorBatch.js | 14 +- .../StaticGroundGeometryPerMaterialBatch.js | 12 +- Source/Scene/Camera.js | 111 ++++- Source/Scene/FrameState.js | 8 + Source/Scene/Globe.js | 10 +- Source/Scene/GlobeSurfaceShaderSet.js | 11 +- Source/Scene/GlobeSurfaceTile.js | 3 + Source/Scene/GlobeSurfaceTileProvider.js | 42 +- Source/Scene/GroundPolylinePrimitive.js | 2 +- Source/Scene/Primitive.js | 6 +- Source/Scene/PrimitivePipeline.js | 34 +- Source/Scene/QuadtreeTile.js | 50 +++ Source/Scene/Scene.js | 31 +- Source/Scene/ScreenSpaceCameraController.js | 9 +- Source/Scene/ShadowVolumeAppearance.js | 49 +-- Source/Scene/TerrainFillMesh.js | 160 +++++-- Source/Scene/TileBoundingRegion.js | 43 +- Source/Shaders/GlobeVS.glsl | 16 + Source/Widgets/Viewer/Viewer.js | 2 +- Source/WorkersES6/combineGeometry.js | 15 +- Source/WorkersES6/createGeometry.js | 47 ++- .../createGroundPolylineGeometry.js | 7 +- ...VerticesFromGoogleEarthEnterpriseBuffer.js | 176 +++++--- .../WorkersES6/createVerticesFromHeightmap.js | 46 ++- .../createVerticesFromQuantizedTerrainMesh.js | 114 +++-- Specs/Core/BoundingSphereSpec.js | 65 ++- Specs/Core/GeographicProjectionSpec.js | 13 + .../GoogleEarthEnterpriseTerrainDataSpec.js | 25 ++ Specs/Core/GroundPolylineGeometrySpec.js | 27 +- Specs/Core/HeightmapTerrainDataSpec.js | 91 +++- Specs/Core/QuantizedMeshTerrainDataSpec.js | 80 +++- Specs/Core/RectangleCollisionCheckerSpec.js | 9 +- Specs/Core/RectangleSpec.js | 125 ++++++ Specs/Core/TerrainEncodingSpec.js | 207 ++++++++++ Specs/Core/WebMercatorProjectionSpec.js | 13 + .../StaticGroundGeometryColorBatchSpec.js | 19 +- ...taticGroundGeometryPerMaterialBatchSpec.js | 22 +- Specs/Scene/CameraSpec.js | 88 ++++ Specs/Scene/GlobeSurfaceTileSpec.js | 4 + Specs/Scene/HeightmapTessellatorSpec.js | 292 ++++++++++--- Specs/Scene/QuadtreePrimitiveSpec.js | 2 + Specs/Scene/QuadtreeTileSpec.js | 69 ++++ Specs/Scene/TerrainFillMeshSpec.js | 75 +++- Specs/createFrameState.js | 2 + 61 files changed, 2656 insertions(+), 541 deletions(-) diff --git a/.eslintignore b/.eslintignore index 2560a5e45885..8fd6085e5fd9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,10 +1,13 @@ Apps/HelloWorld.html Apps/Sandcastle/ThirdParty/** +Apps/SampleData/** Build/** Documentation/** Source/Scene/GltfPipeline/** Source/Shaders/** Source/ThirdParty/** +Source/Workers/cesiumWorkerBootstrapper.js +Specs/Data/** Source/Workers/** !Source/Workers/transferTypedArrayTest.js Specs/jasmine/** diff --git a/Source/Core/BoundingSphere.js b/Source/Core/BoundingSphere.js index 58c9f54e85d8..29162a159e86 100644 --- a/Source/Core/BoundingSphere.js +++ b/Source/Core/BoundingSphere.js @@ -247,6 +247,20 @@ BoundingSphere.fromRectangle2D = function (rectangle, projection, result) { ); }; +const INTERVAL_COUNT = 9; +const HALF_PROJECTED_POINTS_COUNT = INTERVAL_COUNT * INTERVAL_COUNT; +const PROJECTED_POINTS_COUNT = HALF_PROJECTED_POINTS_COUNT * 2; +const projectedPointsScratch = new Array(PROJECTED_POINTS_COUNT); +for ( + let projectedPointIndex = 0; + projectedPointIndex < PROJECTED_POINTS_COUNT; + projectedPointIndex++ +) { + projectedPointsScratch[projectedPointIndex] = new Cartesian3(); +} +const sampleScratch = new Cartographic(); +const cornerScratch = new Cartographic(); + /** * Computes a bounding sphere from a rectangle projected in 2D. The bounding sphere accounts for the * object's minimum and maximum heights over the rectangle. @@ -277,6 +291,35 @@ BoundingSphere.fromRectangleWithHeights2D = function ( projection = defaultValue(projection, defaultProjection); + if (!projection.isNormalCylindrical) { + const southwest = Rectangle.southwest(rectangle, cornerScratch); + const widthStep = rectangle.width / (INTERVAL_COUNT - 1); + const heightStep = rectangle.height / (INTERVAL_COUNT - 1); + const sample = sampleScratch; + let index = 0; + + // Project points in a grid over the Rectangle + for (let x = 0; x < INTERVAL_COUNT; x++) { + for (let y = 0; y < INTERVAL_COUNT; y++) { + sample.longitude = southwest.longitude + x * widthStep; + sample.latitude = southwest.latitude + y * heightStep; + sample.height = minimumHeight; + + projection.project(sample, projectedPointsScratch[index]); + + sample.height = maximumHeight; + projection.project( + sample, + projectedPointsScratch[index + HALF_PROJECTED_POINTS_COUNT] + ); + + index++; + } + } + + return BoundingSphere.fromPoints(projectedPointsScratch, result); + } + Rectangle.southwest(rectangle, fromRectangle2DSouthwest); fromRectangle2DSouthwest.height = minimumHeight; Rectangle.northeast(rectangle, fromRectangle2DNortheast); diff --git a/Source/Core/GeographicProjection.js b/Source/Core/GeographicProjection.js index f6ac972c6272..d0cc6875ee4f 100644 --- a/Source/Core/GeographicProjection.js +++ b/Source/Core/GeographicProjection.js @@ -4,6 +4,8 @@ import defaultValue from "./defaultValue.js"; import defined from "./defined.js"; import DeveloperError from "./DeveloperError.js"; import Ellipsoid from "./Ellipsoid.js"; +import MapProjectionType from "./MapProjectionType.js"; +import SerializedMapProjection from "./SerializedMapProjection.js"; /** * A simple map projection where longitude and latitude are linearly mapped to X and Y by multiplying @@ -38,8 +40,49 @@ Object.defineProperties(GeographicProjection.prototype, { return this._ellipsoid; }, }, + /** + * Gets whether or not the projection evenly maps meridians to vertical lines. + * Geographic projections are cylindrical about the equator. + * + * @memberof GeographicProjection.prototype + * + * @type {Boolean} + * @readonly + * @private + */ + isNormalCylindrical: { + get: function () { + return true; + }, + }, }); +/** + * Returns a JSON object that can be messaged to a web worker. + * + * @private + * @returns {SerializedMapProjection} A JSON object from which the MapProjection can be rebuilt. + */ +GeographicProjection.prototype.serialize = function () { + return new SerializedMapProjection( + MapProjectionType.GEOGRAPHIC, + Ellipsoid.pack(this.ellipsoid, []) + ); +}; + +/** + * Reconstructs a GeographicProjection object from the input JSON. + * + * @private + * @param {SerializedMapProjection} serializedMapProjection A JSON object from which the MapProjection can be rebuilt. + * @returns {Promise.} A Promise that resolves to a MapProjection that is ready for use, or rejects if the SerializedMapProjection is malformed. + */ +GeographicProjection.deserialize = function (serializedMapProjection) { + return Promise.resolve( + new GeographicProjection(Ellipsoid.unpack(serializedMapProjection.json)) + ); +}; + /** * Projects a set of {@link Cartographic} coordinates, in radians, to map coordinates, in meters. * X and Y are the longitude and latitude, respectively, multiplied by the maximum radius of the diff --git a/Source/Core/GoogleEarthEnterpriseTerrainData.js b/Source/Core/GoogleEarthEnterpriseTerrainData.js index cead7ff94de2..0770d413e8ad 100644 --- a/Source/Core/GoogleEarthEnterpriseTerrainData.js +++ b/Source/Core/GoogleEarthEnterpriseTerrainData.js @@ -137,6 +137,7 @@ const rectangleScratch = new Rectangle(); * @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 {SerializedMapProjection} options.serializedMapProjection Serialized map projection. * @param {Number} [options.exaggeration=1.0] The scale used to exaggerate the terrain. * @param {Number} [options.exaggerationRelativeHeight=0.0] The height from 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. @@ -152,12 +153,17 @@ GoogleEarthEnterpriseTerrainData.prototype.createMesh = function (options) { Check.typeOf.number("options.x", options.x); Check.typeOf.number("options.y", options.y); Check.typeOf.number("options.level", options.level); + Check.typeOf.object( + "options.serializedMapProjection", + options.serializedMapProjection + ); //>>includeEnd('debug'); const tilingScheme = options.tilingScheme; const x = options.x; const y = options.y; const level = options.level; + const serializedMapProjection = options.serializedMapProjection; const exaggeration = defaultValue(options.exaggeration, 1.0); const exaggerationRelativeHeight = defaultValue( options.exaggerationRelativeHeight, @@ -194,6 +200,7 @@ GoogleEarthEnterpriseTerrainData.prototype.createMesh = function (options) { includeWebMercatorT: true, negativeAltitudeExponentBias: this._negativeAltitudeExponentBias, negativeElevationThreshold: this._negativeElevationThreshold, + serializedMapProjection: serializedMapProjection, }); if (!defined(verticesPromise)) { diff --git a/Source/Core/GroundPolylineGeometry.js b/Source/Core/GroundPolylineGeometry.js index 8422a587d0ff..c7022ab11b10 100644 --- a/Source/Core/GroundPolylineGeometry.js +++ b/Source/Core/GroundPolylineGeometry.js @@ -9,7 +9,6 @@ import ComponentDatatype from "./ComponentDatatype.js"; import defaultValue from "./defaultValue.js"; import defined from "./defined.js"; import DeveloperError from "./DeveloperError.js"; -import Ellipsoid from "./Ellipsoid.js"; import EllipsoidGeodesic from "./EllipsoidGeodesic.js"; import EllipsoidRhumbLine from "./EllipsoidRhumbLine.js"; import EncodedCartesian3 from "./EncodedCartesian3.js"; @@ -22,13 +21,10 @@ import Matrix3 from "./Matrix3.js"; import Plane from "./Plane.js"; import Quaternion from "./Quaternion.js"; import Rectangle from "./Rectangle.js"; -import WebMercatorProjection from "./WebMercatorProjection.js"; - -const PROJECTIONS = [GeographicProjection, WebMercatorProjection]; -const PROJECTION_COUNT = PROJECTIONS.length; const MITER_BREAK_SMALL = Math.cos(CesiumMath.toRadians(30.0)); const MITER_BREAK_LARGE = Math.cos(CesiumMath.toRadians(150.0)); +const defaultProjection = new GeographicProjection(); // Initial heights for constructing the wall. // Keeping WALL_INITIAL_MIN_HEIGHT near the ellipsoid surface helps @@ -121,10 +117,7 @@ function GroundPolylineGeometry(options) { */ this.arcType = defaultValue(options.arcType, ArcType.GEODESIC); - this._ellipsoid = Ellipsoid.WGS84; - - // MapProjections can't be packed, so store the index to a known MapProjection. - this._projectionIndex = 0; + this._projection = defaultProjection; this._workerName = "createGroundPolylineGeometry"; // Used by GroundPolylinePrimitive to signal worker that scenemode is 3D only. @@ -141,42 +134,24 @@ Object.defineProperties(GroundPolylineGeometry.prototype, { */ packedLength: { get: function () { - return ( - 1.0 + - this._positions.length * 3 + - 1.0 + - 1.0 + - 1.0 + - Ellipsoid.packedLength + - 1.0 + - 1.0 - ); + return 1.0 + this._positions.length * 3 + 1.0 + 1.0 + 1.0 + 1.0; }, }, }); /** - * Set the GroundPolylineGeometry's projection and ellipsoid. + * Set the GroundPolylineGeometry's projection. * Used by GroundPolylinePrimitive to signal scene information to the geometry for generating 2D attributes. * * @param {GroundPolylineGeometry} groundPolylineGeometry GroundPolylinGeometry describing a polyline on terrain or 3D Tiles. * @param {Projection} mapProjection A MapProjection used for projecting cartographic coordinates to 2D. * @private */ -GroundPolylineGeometry.setProjectionAndEllipsoid = function ( +GroundPolylineGeometry.setProjection = function ( groundPolylineGeometry, mapProjection ) { - let projectionIndex = 0; - for (let i = 0; i < PROJECTION_COUNT; i++) { - if (mapProjection instanceof PROJECTIONS[i]) { - projectionIndex = i; - break; - } - } - - groundPolylineGeometry._projectionIndex = projectionIndex; - groundPolylineGeometry._ellipsoid = mapProjection.ellipsoid; + groundPolylineGeometry._projection = mapProjection; }; const cart3Scratch1 = new Cartesian3(); @@ -312,11 +287,6 @@ GroundPolylineGeometry.pack = function (value, array, startingIndex) { array[index++] = value.granularity; array[index++] = value.loop ? 1.0 : 0.0; array[index++] = value.arcType; - - Ellipsoid.pack(value._ellipsoid, array, index); - index += Ellipsoid.packedLength; - - array[index++] = value._projectionIndex; array[index++] = value._scene3DOnly ? 1.0 : 0.0; return array; @@ -347,10 +317,6 @@ GroundPolylineGeometry.unpack = function (array, startingIndex, result) { const loop = array[index++] === 1.0; const arcType = array[index++]; - const ellipsoid = Ellipsoid.unpack(array, index); - index += Ellipsoid.packedLength; - - const projectionIndex = array[index++]; const scene3DOnly = array[index++] === 1.0; if (!defined(result)) { @@ -363,8 +329,6 @@ GroundPolylineGeometry.unpack = function (array, startingIndex, result) { result.granularity = granularity; result.loop = loop; result.arcType = arcType; - result._ellipsoid = ellipsoid; - result._projectionIndex = projectionIndex; result._scene3DOnly = scene3DOnly; return result; @@ -460,10 +424,8 @@ GroundPolylineGeometry.createGeometry = function (groundPolylineGeometry) { let loop = groundPolylineGeometry.loop; const ellipsoid = groundPolylineGeometry._ellipsoid; const granularity = groundPolylineGeometry.granularity; + const projection = groundPolylineGeometry._projection; const arcType = groundPolylineGeometry.arcType; - const projection = new PROJECTIONS[groundPolylineGeometry._projectionIndex]( - ellipsoid - ); const minHeight = WALL_INITIAL_MIN_HEIGHT; const maxHeight = WALL_INITIAL_MAX_HEIGHT; @@ -838,6 +800,7 @@ function projectNormal( projectedPosition, result ) { + const ellipsoid = projection.ellipsoid; const position = Cartographic.toCartesian( cartographic, projection._ellipsoid, @@ -846,7 +809,6 @@ function projectNormal( let normalEndpoint = Cartesian3.add(position, normal, normalEndpointScratch); let flipNormal = false; - const ellipsoid = projection._ellipsoid; let normalEndpointCartographic = ellipsoid.cartesianToCartographic( normalEndpoint, endPosCartographicScratch diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js index 4d797eddbbb4..9aa03e514cc9 100644 --- a/Source/Core/HeightmapTerrainData.js +++ b/Source/Core/HeightmapTerrainData.js @@ -200,6 +200,7 @@ const createMeshTaskProcessorThrottle = new TaskProcessor( * @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 {SerializedMapProjection} options.serializedMapProjection Serialized map projection. * @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. @@ -215,12 +216,17 @@ HeightmapTerrainData.prototype.createMesh = function (options) { Check.typeOf.number("options.x", options.x); Check.typeOf.number("options.y", options.y); Check.typeOf.number("options.level", options.level); + Check.typeOf.object( + "options.serializedMapProjection", + options.serializedMapProjection + ); //>>includeEnd('debug'); const tilingScheme = options.tilingScheme; const x = options.x; const y = options.y; const level = options.level; + const serializedMapProjection = options.serializedMapProjection; const exaggeration = defaultValue(options.exaggeration, 1.0); const exaggerationRelativeHeight = defaultValue( options.exaggerationRelativeHeight, @@ -263,6 +269,7 @@ HeightmapTerrainData.prototype.createMesh = function (options) { isGeographic: tilingScheme.projection instanceof GeographicProjection, exaggeration: exaggeration, exaggerationRelativeHeight: exaggerationRelativeHeight, + serializedMapProjection: serializedMapProjection, encoding: this._encoding, }); @@ -332,6 +339,7 @@ HeightmapTerrainData.prototype._createMeshSync = function (options) { Check.typeOf.number("options.x", options.x); Check.typeOf.number("options.y", options.y); Check.typeOf.number("options.level", options.level); + Check.defined("options.mapProjection", options.mapProjection); //>>includeEnd('debug'); const tilingScheme = options.tilingScheme; @@ -361,21 +369,24 @@ HeightmapTerrainData.prototype._createMeshSync = function (options) { const thisLevelMaxError = levelZeroMaxError / (1 << level); this._skirtHeight = Math.min(thisLevelMaxError * 4.0, 1000.0); - const result = HeightmapTessellator.computeVertices({ - heightmap: this._buffer, - structure: structure, - includeWebMercatorT: true, - width: this._width, - height: this._height, - nativeRectangle: nativeRectangle, - rectangle: rectangle, - relativeToCenter: center, - ellipsoid: ellipsoid, - skirtHeight: this._skirtHeight, - isGeographic: tilingScheme.projection instanceof GeographicProjection, - exaggeration: exaggeration, - exaggerationRelativeHeight: exaggerationRelativeHeight, - }); + const result = HeightmapTessellator.computeVertices( + { + heightmap: this._buffer, + structure: structure, + includeWebMercatorT: true, + width: this._width, + height: this._height, + nativeRectangle: nativeRectangle, + rectangle: rectangle, + relativeToCenter: center, + ellipsoid: ellipsoid, + skirtHeight: this._skirtHeight, + isGeographic: tilingScheme.projection instanceof GeographicProjection, + exaggeration: exaggeration, + exaggerationRelativeHeight: exaggerationRelativeHeight, + }, + options.mapProjection + ); // Free memory received from server after mesh is created. this._buffer = undefined; diff --git a/Source/Core/HeightmapTessellator.js b/Source/Core/HeightmapTessellator.js index a1b2c48072e3..c6b61ca6ed94 100644 --- a/Source/Core/HeightmapTessellator.js +++ b/Source/Core/HeightmapTessellator.js @@ -2,9 +2,10 @@ import AxisAlignedBoundingBox from "./AxisAlignedBoundingBox.js"; import BoundingSphere from "./BoundingSphere.js"; import Cartesian2 from "./Cartesian2.js"; import Cartesian3 from "./Cartesian3.js"; +import Cartographic from "./Cartographic.js"; +import Check from "./Check.js"; import defaultValue from "./defaultValue.js"; import defined from "./defined.js"; -import DeveloperError from "./DeveloperError.js"; import Ellipsoid from "./Ellipsoid.js"; import EllipsoidalOccluder from "./EllipsoidalOccluder.js"; import CesiumMath from "./Math.js"; @@ -43,6 +44,9 @@ const matrix4Scratch = new Matrix4(); const minimumScratch = new Cartesian3(); const maximumScratch = new Cartesian3(); +const cartographicScratch = new Cartographic(); +const relativeToCenter2dScratch = new Cartesian3(); +const projectedCartesian3Scratch = new Cartesian3(); /** * Fills an array of vertices from a heightmap image. * @@ -93,7 +97,8 @@ const maximumScratch = new Cartesian3(); * @param {Boolean} [options.structure.isBigEndian=false] Indicates endianness of the elements in the buffer when the * stride property is greater than 1. If this property is false, the first element is the * low-order element. If it is true, the first element is the high-order element. - * + * @param {Boolean} [options.includeWebMercatorT=false] Indicates that the vertices should include a T coordinate to compensate for Web Mercator Latitude. + * @param {MapProjection} mapProjection MapProjection for projecting terrain positions to the target 2D coordinate system. * @example * const width = 5; * const height = 5; @@ -113,20 +118,15 @@ const maximumScratch = new Cartesian3(); * const encoding = statistics.encoding; * const position = encoding.decodePosition(statistics.vertices, index); */ -HeightmapTessellator.computeVertices = function (options) { +HeightmapTessellator.computeVertices = function (options, mapProjection) { //>>includeStart('debug', pragmas.debug); - if (!defined(options) || !defined(options.heightmap)) { - throw new DeveloperError("options.heightmap is required."); - } - if (!defined(options.width) || !defined(options.height)) { - throw new DeveloperError("options.width and options.height are required."); - } - if (!defined(options.nativeRectangle)) { - throw new DeveloperError("options.nativeRectangle is required."); - } - if (!defined(options.skirtHeight)) { - throw new DeveloperError("options.skirtHeight is required."); - } + Check.defined("options", options); + Check.defined("options.heightmap", options.heightmap); + Check.typeOf.number("options.width", options.width); + Check.typeOf.number("options.height", options.height); + Check.defined("options.nativeRectangle", options.nativeRectangle); + Check.typeOf.number("options.skirtHeight", options.skirtHeight); + Check.defined("mapProjection", mapProjection); //>>includeEnd('debug'); // This function tends to be a performance hotspot for terrain rendering, @@ -134,6 +134,8 @@ HeightmapTessellator.computeVertices = function (options) { // In particular, the functionality of Ellipsoid.cartographicToCartesian // is inlined. + const nonEquatorialCylindricalProjection = !mapProjection.isNormalCylindrical; + const cos = Math.cos; const sin = Math.sin; const sqrt = Math.sqrt; @@ -189,6 +191,30 @@ HeightmapTessellator.computeVertices = function (options) { relativeToCenter = hasRelativeToCenter ? relativeToCenter : Cartesian3.ZERO; const includeWebMercatorT = defaultValue(options.includeWebMercatorT, false); + let relativeToCenter2D; + if (nonEquatorialCylindricalProjection) { + if (hasRelativeToCenter) { + const cartographicRTC = ellipsoid.cartesianToCartographic( + relativeToCenter, + cartographicScratch + ); + const projectedRTC = mapProjection.project( + cartographicRTC, + projectedCartesian3Scratch + ); + relativeToCenter2D = Cartesian3.clone( + projectedRTC, + relativeToCenter2dScratch + ); + } else { + // eslint-disable-next-line no-unused-vars + relativeToCenter2D = Cartesian3.clone( + Cartesian3.ZERO, + relativeToCenter2dScratch + ); + } + } + const exaggeration = defaultValue(options.exaggeration, 1.0); const exaggerationRelativeHeight = defaultValue( options.exaggerationRelativeHeight, @@ -282,6 +308,10 @@ HeightmapTessellator.computeVertices = function (options) { const positions = new Array(vertexCount); const heights = new Array(vertexCount); const uvs = new Array(vertexCount); + let positions2D; + if (nonEquatorialCylindricalProjection) { + positions2D = new Array(vertexCount); + } const webMercatorTs = includeWebMercatorT ? new Array(vertexCount) : []; const geodeticSurfaceNormals = includeGeodeticSurfaceNormals ? new Array(vertexCount) @@ -460,6 +490,14 @@ HeightmapTessellator.computeVertices = function (options) { uvs[index] = new Cartesian2(u, v); heights[index] = heightSample; + if (nonEquatorialCylindricalProjection) { + const cartographic = cartographicScratch; + cartographic.height = heightSample; + cartographic.longitude = longitude; + cartographic.latitude = latitude; + positions2D[index] = mapProjection.project(cartographic); + } + if (includeWebMercatorT) { webMercatorTs[index] = webMercatorT; } @@ -504,22 +542,40 @@ HeightmapTessellator.computeVertices = function (options) { includeWebMercatorT, includeGeodeticSurfaceNormals, exaggeration, - exaggerationRelativeHeight + exaggerationRelativeHeight, + relativeToCenter2D ); const vertices = new Float32Array(vertexCount * encoding.stride); let bufferIndex = 0; - for (let j = 0; j < vertexCount; ++j) { - bufferIndex = encoding.encode( - vertices, - bufferIndex, - positions[j], - uvs[j], - heights[j], - undefined, - webMercatorTs[j], - geodeticSurfaceNormals[j] - ); + let j; + if (nonEquatorialCylindricalProjection) { + for (let j = 0; j < vertexCount; ++j) { + bufferIndex = encoding.encode( + vertices, + bufferIndex, + positions[j], + uvs[j], + heights[j], + undefined, + webMercatorTs[j], + geodeticSurfaceNormals[j], + positions2D[j] + ); + } + } else { + for (j = 0; j < vertexCount; ++j) { + bufferIndex = encoding.encode( + vertices, + bufferIndex, + positions[j], + uvs[j], + heights[j], + undefined, + webMercatorTs[j], + geodeticSurfaceNormals[j] + ); + } } return { diff --git a/Source/Core/MapProjection.js b/Source/Core/MapProjection.js index f630e69575d8..6d421a2bb06a 100644 --- a/Source/Core/MapProjection.js +++ b/Source/Core/MapProjection.js @@ -1,4 +1,7 @@ +import Cartographic from "./Cartographic.js"; +import Check from "./Check.js"; import DeveloperError from "./DeveloperError.js"; +import Rectangle from "./Rectangle.js"; /** * Defines how geodetic ellipsoid coordinates ({@link Cartographic}) project to a @@ -10,6 +13,9 @@ import DeveloperError from "./DeveloperError.js"; * * @see GeographicProjection * @see WebMercatorProjection + * @see CustomProjection + * @see Proj4Projection + * @see Matrix4Projection */ function MapProjection() { DeveloperError.throwInstantiationError(); @@ -27,6 +33,20 @@ Object.defineProperties(MapProjection.prototype, { ellipsoid: { get: DeveloperError.throwInstantiationError, }, + /** + * Gets whether or not the projection evenly maps meridians to vertical lines. + * Projections that evenly map meridians to vertical lines (such as Web Mercator and Geographic) do not need + * addition 2D vertex attributes and are more efficient to render. + * + * @memberof MapProjection.prototype + * + * @type {Boolean} + * @readonly + * @private + */ + isNormalCylindrical: { + get: DeveloperError.throwInstantiationError, + }, }); /** @@ -44,6 +64,27 @@ Object.defineProperties(MapProjection.prototype, { */ MapProjection.prototype.project = DeveloperError.throwInstantiationError; +/** + * Returns a JSON object that can be messaged to a web worker. + * + * @memberof MapProjection + * @function + * @private + * @returns {SerializedMapProjection} A JSON object from which the MapProjection can be rebuilt. + */ +MapProjection.prototype.serialize = DeveloperError.throwInstantiationError; + +/** + * Reconstructs a MapProjection object from the input JSON. + * + * @function + * @private + * + * @param {SerializedMapProjection} serializedMapProjection A JSON object from which the MapProjection can be rebuilt. + * @returns {Promise.} A Promise that resolves to a MapProjection that is ready for use, or rejects if the SerializedMapProjection is malformed. + */ +MapProjection.deserialize = DeveloperError.throwInstantiationError; + /** * Unprojects projection-specific map {@link Cartesian3} coordinates, in meters, to {@link Cartographic} * coordinates, in radians. @@ -59,4 +100,40 @@ MapProjection.prototype.project = DeveloperError.throwInstantiationError; * created and returned. */ MapProjection.prototype.unproject = DeveloperError.throwInstantiationError; + +const maxcoordRectangleScratch = new Rectangle(); +const rectangleCenterScratch = new Cartographic(); +/** + * Approximates the X/Y extents of a map projection in 2D. + * + * @function + * + * @param {MapProjection} mapProjection A map projection from cartographic coordinates to 2D space. + * @param {Cartesian2} result result parameter. + * @private + */ +MapProjection.approximateMaximumCoordinate = function (mapProjection, result) { + //>>includeStart('debug', pragmas.debug); + Check.defined("mapProjection", mapProjection); + Check.defined("result", result); + //>>includeEnd('debug'); + + const projectedExtents = Rectangle.approximateProjectedExtents( + { + cartographicRectangle: Rectangle.MAX_VALUE, + mapProjection: mapProjection, + }, + maxcoordRectangleScratch + ); + const projectedCenter = Rectangle.center( + projectedExtents, + rectangleCenterScratch + ); + + result.x = projectedCenter.longitude + projectedExtents.width * 0.5; + result.y = projectedCenter.latitude + projectedExtents.height * 0.5; + + return result; +}; + export default MapProjection; diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js index 0a88ed831cec..1e4faacbb501 100644 --- a/Source/Core/QuantizedMeshTerrainData.js +++ b/Source/Core/QuantizedMeshTerrainData.js @@ -280,6 +280,7 @@ const createMeshTaskProcessorThrottle = new TaskProcessor( * @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 {SerializedMapProjection} options.serializedMapProjection Serialized map projection. * @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. @@ -295,12 +296,17 @@ QuantizedMeshTerrainData.prototype.createMesh = function (options) { Check.typeOf.number("options.x", options.x); Check.typeOf.number("options.y", options.y); Check.typeOf.number("options.level", options.level); + Check.typeOf.object( + "options.serializedMapProjection", + options.serializedMapProjection + ); //>>includeEnd('debug'); const tilingScheme = options.tilingScheme; const x = options.x; const y = options.y; const level = options.level; + const serializedMapProjection = options.serializedMapProjection; const exaggeration = defaultValue(options.exaggeration, 1.0); const exaggerationRelativeHeight = defaultValue( options.exaggerationRelativeHeight, @@ -335,6 +341,7 @@ QuantizedMeshTerrainData.prototype.createMesh = function (options) { ellipsoid: ellipsoid, exaggeration: exaggeration, exaggerationRelativeHeight: exaggerationRelativeHeight, + serializedMapProjection: serializedMapProjection, }); if (!defined(verticesPromise)) { diff --git a/Source/Core/Rectangle.js b/Source/Core/Rectangle.js index 180350039523..be5daadd0d99 100644 --- a/Source/Core/Rectangle.js +++ b/Source/Core/Rectangle.js @@ -1,3 +1,4 @@ +import Cartesian3 from "./Cartesian3.js"; import Cartographic from "./Cartographic.js"; import Check from "./Check.js"; import defaultValue from "./defaultValue.js"; @@ -894,74 +895,366 @@ Rectangle.subsample = function (rectangle, ellipsoid, surfaceHeight, result) { return result; }; +const unprojectedScratch = new Cartographic(); +const cornerScratch = new Cartographic(); +const projectedScratch = new Cartesian3(); /** - * Computes a subsection of a rectangle from normalized coordinates in the range [0.0, 1.0]. + * Approximates a Cartographic rectangle's extents in some map projection by projecting + * points in a grid throughout the rectangle. * - * @param {Rectangle} rectangle The rectangle to subsection. - * @param {Number} westLerp The west interpolation factor in the range [0.0, 1.0]. Must be less than or equal to eastLerp. - * @param {Number} southLerp The south interpolation factor in the range [0.0, 1.0]. Must be less than or equal to northLerp. - * @param {Number} eastLerp The east interpolation factor in the range [0.0, 1.0]. Must be greater than or equal to westLerp. - * @param {Number} northLerp The north interpolation factor in the range [0.0, 1.0]. Must be greater than or equal to southLerp. - * @param {Rectangle} [result] The object onto which to store the result. - * @returns {Rectangle} The modified result parameter or a new Rectangle instance if none was provided. + * @function + * + * @param {Object} options Object with the following properties: + * @param {Rectangle} options.cartographicRectangle An input rectangle in geographic coordinates. + * @param {MapProjection} options.mapProjection A MapProjection indicating a projection from geographic coordinates. + * @param {Number} [options.steps=8] Number of points to sample along each side of the geographic Rectangle. + * @param {Rectangle} [result] Rectangle on which to store the projected extents of the input. */ -Rectangle.subsection = function ( - rectangle, - westLerp, - southLerp, - eastLerp, - northLerp, +Rectangle.approximateProjectedExtents = function (options, result) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + + //>>includeStart('debug', pragmas.debug); + Check.defined("cartographicRectangle", options.cartographicRectangle); + Check.defined("mapProjection", options.mapProjection); + //>>includeEnd('debug'); + + const cartographicRectangle = options.cartographicRectangle; + const mapProjection = options.mapProjection; + const steps = defaultValue(options.steps, 8); + + if (!defined(result)) { + result = new Rectangle(); + } + + result.west = Number.MAX_VALUE; + result.east = -Number.MAX_VALUE; + result.south = Number.MAX_VALUE; + result.north = -Number.MAX_VALUE; + + const geographicCorner = Rectangle.southwest( + cartographicRectangle, + cornerScratch + ); + const geographicWidth = cartographicRectangle.width; + const geographicHeight = cartographicRectangle.height; + + const geographicWidthStep = geographicWidth / (steps - 1); + const geographicHeightStep = geographicHeight / (steps - 1); + + const projected = projectedScratch; + const unprojected = unprojectedScratch; + + for (let longIndex = 0; longIndex < steps; longIndex++) { + for (let latIndex = 0; latIndex < steps; latIndex++) { + unprojected.longitude = + geographicCorner.longitude + geographicWidthStep * longIndex; + unprojected.latitude = + geographicCorner.latitude + geographicHeightStep * latIndex; + + mapProjection.project(unprojected, projected); + result.west = Math.min(result.west, projected.x); + result.east = Math.max(result.east, projected.x); + result.south = Math.min(result.south, projected.y); + result.north = Math.max(result.north, projected.y); + } + } + + return result; +}; + +function unprojectAndSetRectangle( + mapProjection, + projected, + unprojected, result ) { + mapProjection.unproject(projected, unprojected); + result.west = Math.min(result.west, unprojected.longitude); + result.east = Math.max(result.east, unprojected.longitude); + result.south = Math.min(result.south, unprojected.latitude); + result.north = Math.max(result.north, unprojected.latitude); +} + +const northPole = new Cartographic(0, CesiumMath.PI_OVER_TWO); +const southPole = new Cartographic(0, -CesiumMath.PI_OVER_TWO); +const projectedPoleScratch = new Cartographic(); +/** + * Approximates a projected rectangle's extents in Cartographic space by unprojecting + * points along the Rectangle's boundary, checking the poles, and guessing whether or not + * the projected rectangle crosses the IDL. + * + * Takes into account map projection boundaries. + * + * @function + * + * @param {Object} options Object with the following properties: + * @param {Rectangle} projectedRectangle An input rectangle in projected coordinates + * @param {MapProjection} mapProjection A MapProjection indicating a projection from cartographic coordiantes. + * @param {Rectangle} [result] Rectangle on which to store the projected extents of the input. + * @param {Number} [steps=16] Number of points to sample along each side of the projected Rectangle. + */ +Rectangle.approximateCartographicExtents = function (options, result) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + //>>includeStart('debug', pragmas.debug); - Check.typeOf.object("rectangle", rectangle); - Check.typeOf.number.greaterThanOrEquals("westLerp", westLerp, 0.0); - Check.typeOf.number.lessThanOrEquals("westLerp", westLerp, 1.0); - Check.typeOf.number.greaterThanOrEquals("southLerp", southLerp, 0.0); - Check.typeOf.number.lessThanOrEquals("southLerp", southLerp, 1.0); - Check.typeOf.number.greaterThanOrEquals("eastLerp", eastLerp, 0.0); - Check.typeOf.number.lessThanOrEquals("eastLerp", eastLerp, 1.0); - Check.typeOf.number.greaterThanOrEquals("northLerp", northLerp, 0.0); - Check.typeOf.number.lessThanOrEquals("northLerp", northLerp, 1.0); - - Check.typeOf.number.lessThanOrEquals("westLerp", westLerp, eastLerp); - Check.typeOf.number.lessThanOrEquals("southLerp", southLerp, northLerp); + Check.defined("projectedRectangle", options.projectedRectangle); + Check.defined("mapProjection", options.mapProjection); //>>includeEnd('debug'); + /** + * Computes a subsection of a rectangle from normalized coordinates in the range [0.0, 1.0]. + * + * @param {Rectangle} rectangle The rectangle to subsection. + * @param {Number} westLerp The west interpolation factor in the range [0.0, 1.0]. Must be less than or equal to eastLerp. + * @param {Number} southLerp The south interpolation factor in the range [0.0, 1.0]. Must be less than or equal to northLerp. + * @param {Number} eastLerp The east interpolation factor in the range [0.0, 1.0]. Must be greater than or equal to westLerp. + * @param {Number} northLerp The north interpolation factor in the range [0.0, 1.0]. Must be greater than or equal to southLerp. + * @param {Rectangle} [result] The object onto which to store the result. + * @returns {Rectangle} The modified result parameter or a new Rectangle instance if none was provided. + */ + Rectangle.subsection = function ( + rectangle, + westLerp, + southLerp, + eastLerp, + northLerp, + result + ) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("rectangle", rectangle); + Check.typeOf.number.greaterThanOrEquals("westLerp", westLerp, 0.0); + Check.typeOf.number.lessThanOrEquals("westLerp", westLerp, 1.0); + Check.typeOf.number.greaterThanOrEquals("southLerp", southLerp, 0.0); + Check.typeOf.number.lessThanOrEquals("southLerp", southLerp, 1.0); + Check.typeOf.number.greaterThanOrEquals("eastLerp", eastLerp, 0.0); + Check.typeOf.number.lessThanOrEquals("eastLerp", eastLerp, 1.0); + Check.typeOf.number.greaterThanOrEquals("northLerp", northLerp, 0.0); + Check.typeOf.number.lessThanOrEquals("northLerp", northLerp, 1.0); + + Check.typeOf.number.lessThanOrEquals("westLerp", westLerp, eastLerp); + Check.typeOf.number.lessThanOrEquals("southLerp", southLerp, northLerp); + //>>includeEnd('debug'); + + if (!defined(result)) { + result = new Rectangle(); + } + + // This function doesn't use CesiumMath.lerp because it has floating point precision problems + // when the start and end values are the same but the t changes. + + if (rectangle.west <= rectangle.east) { + const width = rectangle.east - rectangle.west; + result.west = rectangle.west + westLerp * width; + result.east = rectangle.west + eastLerp * width; + } else { + const width = CesiumMath.TWO_PI + rectangle.east - rectangle.west; + result.west = CesiumMath.negativePiToPi( + rectangle.west + westLerp * width + ); + result.east = CesiumMath.negativePiToPi( + rectangle.west + eastLerp * width + ); + } + const height = rectangle.north - rectangle.south; + result.south = rectangle.south + southLerp * height; + result.north = rectangle.south + northLerp * height; + + // Fix floating point precision problems when t = 1 + if (westLerp === 1.0) { + result.west = rectangle.east; + } + if (eastLerp === 1.0) { + result.east = rectangle.east; + } + if (southLerp === 1.0) { + result.south = rectangle.north; + } + if (northLerp === 1.0) { + result.north = rectangle.north; + } + + return result; + }; + + const projectedRectangle = options.projectedRectangle; + const mapProjection = options.mapProjection; + const steps = defaultValue(options.steps, 16); + if (!defined(result)) { result = new Rectangle(); } - // This function doesn't use CesiumMath.lerp because it has floating point precision problems - // when the start and end values are the same but the t changes. + result.west = Number.MAX_VALUE; + result.east = -Number.MAX_VALUE; + result.south = Number.MAX_VALUE; + result.north = -Number.MAX_VALUE; - if (rectangle.west <= rectangle.east) { - const width = rectangle.east - rectangle.west; - result.west = rectangle.west + westLerp * width; - result.east = rectangle.west + eastLerp * width; - } else { - const width = CesiumMath.TWO_PI + rectangle.east - rectangle.west; - result.west = CesiumMath.negativePiToPi(rectangle.west + westLerp * width); - result.east = CesiumMath.negativePiToPi(rectangle.west + eastLerp * width); + let idlCrossWest = Number.MAX_VALUE; + let idlCrossEast = -Number.MAX_VALUE; + + const projectedCorner = Rectangle.southwest( + projectedRectangle, + cornerScratch + ); + const projectedWidth = projectedRectangle.width; + const projectedHeight = projectedRectangle.height; + const projectedWidthStep = projectedWidth / (steps - 1); + const projectedHeightStep = projectedHeight / (steps - 1); + let crossedIdl = false; + let lastLongitudeTop = 0.0; + let lastLongitudeBottom = 0.0; + + const projected = projectedScratch; + const unprojected = unprojectedScratch; + for (let longIndex = 0; longIndex < steps; longIndex++) { + projected.x = projectedCorner.longitude + projectedWidthStep * longIndex; + projected.y = projectedCorner.latitude; + + unprojectAndSetRectangle(mapProjection, projected, unprojected, result); + + if ( + longIndex !== 0 && + !CesiumMath.equalsEpsilon( + Math.abs(unprojected.longitude), + CesiumMath.PI, + CesiumMath.EPSILON14 + ) + ) { + crossedIdl = + crossedIdl || + Math.abs(lastLongitudeTop - unprojected.longitude) > CesiumMath.PI; + } + lastLongitudeTop = unprojected.longitude; + + idlCrossWest = + unprojected.longitude > 0.0 + ? Math.min(idlCrossWest, unprojected.longitude) + : idlCrossWest; + idlCrossEast = + unprojected.longitude < 0.0 + ? Math.max(idlCrossEast, unprojected.longitude) + : idlCrossEast; + + projected.y = projectedCorner.latitude + projectedHeight; + + unprojectAndSetRectangle(mapProjection, projected, unprojected, result); + + if ( + longIndex !== 0 && + !CesiumMath.equalsEpsilon( + Math.abs(unprojected.longitude), + CesiumMath.PI, + CesiumMath.EPSILON14 + ) + ) { + crossedIdl = + crossedIdl || + Math.abs(lastLongitudeBottom - unprojected.longitude) > CesiumMath.PI; + } + lastLongitudeBottom = unprojected.longitude; + + idlCrossWest = + unprojected.longitude > 0.0 + ? Math.min(idlCrossWest, unprojected.longitude) + : idlCrossWest; + idlCrossEast = + unprojected.longitude < 0.0 + ? Math.max(idlCrossEast, unprojected.longitude) + : idlCrossEast; } - const height = rectangle.north - rectangle.south; - result.south = rectangle.south + southLerp * height; - result.north = rectangle.south + northLerp * height; - // Fix floating point precision problems when t = 1 - if (westLerp === 1.0) { - result.west = rectangle.east; + for (let latIndex = 0; latIndex < steps; latIndex++) { + projected.y = projectedCorner.latitude + projectedHeightStep * latIndex; + projected.x = projectedCorner.longitude; + + unprojectAndSetRectangle(mapProjection, projected, unprojected, result); + + idlCrossWest = + unprojected.longitude > 0.0 + ? Math.min(idlCrossWest, unprojected.longitude) + : idlCrossWest; + idlCrossEast = + unprojected.longitude < 0.0 + ? Math.max(idlCrossEast, unprojected.longitude) + : idlCrossEast; + + projected.x = projectedCorner.longitude + projectedWidth; + + unprojectAndSetRectangle(mapProjection, projected, unprojected, result); + + idlCrossWest = + unprojected.longitude > 0.0 + ? Math.min(idlCrossWest, unprojected.longitude) + : idlCrossWest; + idlCrossEast = + unprojected.longitude < 0.0 + ? Math.max(idlCrossEast, unprojected.longitude) + : idlCrossEast; } - if (eastLerp === 1.0) { - result.east = rectangle.east; + + // Check if either pole is in the projected rectangle + const projectionBounds = defaultValue( + mapProjection.wgs84Bounds, + Rectangle.MAX_VALUE + ); + let containsPole; + + const projectedNorthPole = mapProjection.project(northPole, projectedScratch); + const projectedNorthPoleCartographic = projectedPoleScratch; + projectedNorthPoleCartographic.longitude = projectedNorthPole.x; + projectedNorthPoleCartographic.latitude = projectedNorthPole.y; + if ( + Rectangle.contains(projectionBounds, northPole) && + Rectangle.contains(projectedRectangle, projectedNorthPoleCartographic) + ) { + result.north = CesiumMath.PI_OVER_TWO; + result.west = -CesiumMath.PI; + result.east = CesiumMath.PI; + containsPole = true; } - if (southLerp === 1.0) { - result.south = rectangle.north; + + const projectedSouthPole = mapProjection.project(southPole, projectedScratch); + const projectedSouthPoleCartographic = projectedPoleScratch; + projectedSouthPoleCartographic.longitude = projectedSouthPole.x; + projectedSouthPoleCartographic.latitude = projectedSouthPole.y; + if ( + Rectangle.contains(projectionBounds, southPole) && + Rectangle.contains(projectedRectangle, projectedSouthPoleCartographic) + ) { + result.south = -CesiumMath.PI_OVER_TWO; + result.west = -CesiumMath.PI; + result.east = CesiumMath.PI; + containsPole = true; } - if (northLerp === 1.0) { - result.north = rectangle.north; + + // Check if the rectangle crosses the IDL + if (!containsPole && crossedIdl) { + result.west = idlCrossWest; + result.east = idlCrossEast; } + // Clamp + result.west = CesiumMath.clamp( + result.west, + projectionBounds.west, + projectionBounds.east + ); + result.east = CesiumMath.clamp( + result.east, + projectionBounds.west, + projectionBounds.east + ); + result.south = CesiumMath.clamp( + result.south, + projectionBounds.south, + projectionBounds.north + ); + result.north = CesiumMath.clamp( + result.north, + projectionBounds.south, + projectionBounds.north + ); + return result; }; diff --git a/Source/Core/RectangleCollisionChecker.js b/Source/Core/RectangleCollisionChecker.js index 46a0eb6b0593..0e3ebd452f86 100644 --- a/Source/Core/RectangleCollisionChecker.js +++ b/Source/Core/RectangleCollisionChecker.js @@ -1,12 +1,14 @@ import RBush from "../ThirdParty/rbush.js"; +import Rectangle from "./Rectangle.js"; import Check from "./Check.js"; /** * Wrapper around rbush for use with Rectangle types. * @private */ -function RectangleCollisionChecker() { +function RectangleCollisionChecker(mapProjection) { this._tree = new RBush(); + this._mapProjection = mapProjection; } function RectangleWithId() { @@ -17,11 +19,25 @@ function RectangleWithId() { this.id = ""; } -RectangleWithId.fromRectangleAndId = function (id, rectangle, result) { - result.minX = rectangle.west; - result.minY = rectangle.south; - result.maxX = rectangle.east; - result.maxY = rectangle.north; +const projectedExtentsScratch = new Rectangle(); +RectangleWithId.fromRectangleAndId = function ( + id, + rectangle, + result, + mapProjection +) { + const projectedExtents = Rectangle.approximateProjectedExtents( + { + cartographicRectangle: rectangle, + mapProjection: mapProjection, + }, + projectedExtentsScratch + ); + + result.minX = projectedExtents.west; + result.minY = projectedExtents.south; + result.maxX = projectedExtents.east; + result.maxY = projectedExtents.north; result.id = id; return result; }; @@ -42,7 +58,8 @@ RectangleCollisionChecker.prototype.insert = function (id, rectangle) { const withId = RectangleWithId.fromRectangleAndId( id, rectangle, - new RectangleWithId() + new RectangleWithId(), + this._mapProjection ); this._tree.insert(withId); }; @@ -68,7 +85,8 @@ RectangleCollisionChecker.prototype.remove = function (id, rectangle) { const withId = RectangleWithId.fromRectangleAndId( id, rectangle, - removalScratch + removalScratch, + this._mapProjection ); this._tree.remove(withId, idCompare); }; @@ -88,7 +106,8 @@ RectangleCollisionChecker.prototype.collides = function (rectangle) { const withId = RectangleWithId.fromRectangleAndId( "", rectangle, - collisionScratch + collisionScratch, + this._mapProjection ); return this._tree.collides(withId); }; diff --git a/Source/Core/TerrainData.js b/Source/Core/TerrainData.js index 13e8430e03e8..093af8d5b7a2 100644 --- a/Source/Core/TerrainData.js +++ b/Source/Core/TerrainData.js @@ -76,6 +76,7 @@ TerrainData.prototype.isChildAvailable = DeveloperError.throwInstantiationError; * @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 {SerializedMapProjection} options.serializedMapProjection Serialized map projection. * @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. diff --git a/Source/Core/TerrainEncoding.js b/Source/Core/TerrainEncoding.js index a6faa36c07be..8aa2dc5e73a8 100644 --- a/Source/Core/TerrainEncoding.js +++ b/Source/Core/TerrainEncoding.js @@ -34,6 +34,7 @@ const SHIFT_LEFT_12 = Math.pow(2.0, 12.0); * @param {Boolean} [hasGeodeticSurfaceNormals=false] true if the terrain data includes geodetic surface normals; otherwise, false. * @param {Number} [exaggeration=1.0] A scalar used to exaggerate terrain. * @param {Number} [exaggerationRelativeHeight=0.0] The relative height from which terrain is exaggerated. + * @param {Cartesian3} [center2D] Center in the projected space. If defined, it is assumed that the projection requires 2D vertex attributes. * * @private */ @@ -47,7 +48,8 @@ function TerrainEncoding( hasWebMercatorT, hasGeodeticSurfaceNormals, exaggeration, - exaggerationRelativeHeight + exaggerationRelativeHeight, + center2D ) { let quantization = TerrainQuantization.NONE; let toENU; @@ -182,6 +184,18 @@ function TerrainEncoding( 0.0 ); + /** + * Center of the 2.5D projected space. + * @type {Cartesian3} + */ + this.center2D = Cartesian3.clone(center2D); + + /** + * The terrain mesh should contain projected positions for 2D space. + * @type {Boolean} + */ + this.hasPositions2D = defined(center2D); + /** * The number of components in each vertex. This value can differ with different quantizations. * @type {Number} @@ -203,7 +217,8 @@ TerrainEncoding.prototype.encode = function ( height, normalToPack, webMercatorT, - geodeticSurfaceNormal + geodeticSurfaceNormal, + position2D ) { const u = uv.x; const v = uv.y; @@ -275,6 +290,14 @@ TerrainEncoding.prototype.encode = function ( vertexBuffer[bufferIndex++] = geodeticSurfaceNormal.z; } + // Don't quantize 2D positions in custom projections + if (this.hasPositions2D) { + Cartesian3.subtract(position2D, this.center2D, cartesian3Scratch); + vertexBuffer[bufferIndex++] = cartesian3Scratch.x; + vertexBuffer[bufferIndex++] = cartesian3Scratch.y; + vertexBuffer[bufferIndex++] = cartesian3Scratch.z; + } + return bufferIndex; }; @@ -367,6 +390,18 @@ TerrainEncoding.prototype.decodePosition = function (buffer, index, result) { result.z = buffer[index + 2]; return Cartesian3.add(result, this.center, result); }; +// Only for use when encoding hasPositions2D +TerrainEncoding.prototype.decodePosition2D = function (buffer, index, result) { + if (!defined(result)) { + result = new Cartesian3(); + } + index = (index + 1) * this.stride - 3; + + result.x = buffer[index]; + result.y = buffer[index + 1]; + result.z = buffer[index + 2]; + return Cartesian3.add(result, this.center2D, result); +}; TerrainEncoding.prototype.getExaggeratedPosition = function ( buffer, @@ -456,7 +491,10 @@ TerrainEncoding.prototype.getOctEncodedNormal = function ( index, result ) { - index = index * this.stride + this._offsetVertexNormal; + index = + index * this.stride + + this._offsetVertexNormal + + (this.hasPositions2D ? -3 : 0); const temp = buffer[index] / 256.0; const x = Math.floor(temp); @@ -500,6 +538,10 @@ TerrainEncoding.prototype._calculateStrideAndOffsets = function () { vertexStride += 3; } + if (this.hasPositions2D) { + vertexStride += 3; + } + this.stride = vertexStride; }; @@ -513,12 +555,23 @@ const attributesIndicesBits12 = { compressed1: 1, geodeticSurfaceNormal: 2, }; +const attributesNoneAnd2D = { + position3DAndHeight: 0, + textureCoordAndEncodedNormals: 1, + position2D: 2, +}; +const attributesAnd2D = { + compressed0: 0, + compressed1: 1, + position2D: 2, +}; TerrainEncoding.prototype.getAttributes = function (buffer) { const datatype = ComponentDatatype.FLOAT; const sizeInBytes = ComponentDatatype.getSizeInBytes(datatype); const strideInBytes = this.stride * sizeInBytes; let offsetInBytes = 0; + const num2DComponents = this.hasPositions2D ? 3 : 0; const attributes = []; function addAttribute(index, componentsPerAttribute) { @@ -547,6 +600,9 @@ TerrainEncoding.prototype.getAttributes = function (buffer) { if (this.hasGeodeticSurfaceNormals) { addAttribute(attributesIndicesNone.geodeticSurfaceNormal, 3); } + if (this.hasPositions2D) { + addAttribute(attributesNoneAnd2D.position2D, num2DComponents); + } } else { // When there is no webMercatorT or vertex normals, the attribute only needs 3 components: x/y, z/h, u/v. // WebMercatorT and vertex normals each take up one component, so if only one of them is present the first @@ -567,12 +623,21 @@ TerrainEncoding.prototype.getAttributes = function (buffer) { if (this.hasGeodeticSurfaceNormals) { addAttribute(attributesIndicesBits12.geodeticSurfaceNormal, 3); } + if (this.hasPositions2D) { + addAttribute(attributesNoneAnd2D.position2D, num2DComponents); + } } return attributes; }; TerrainEncoding.prototype.getAttributeLocations = function () { + if (this.hasPositions2D) { + if (this.quantization === TerrainQuantization.NONE) { + return attributesIndicesNone; + } + return attributesAnd2D; + } if (this.quantization === TerrainQuantization.NONE) { return attributesIndicesNone; } @@ -599,7 +664,8 @@ TerrainEncoding.clone = function (encoding, result) { result.hasGeodeticSurfaceNormals = encoding.hasGeodeticSurfaceNormals; result.exaggeration = encoding.exaggeration; result.exaggerationRelativeHeight = encoding.exaggerationRelativeHeight; - + result.hasPositions2D = encoding.hasPositions2D; + result.center2D = Cartesian3.clone(encoding.center2D); result._calculateStrideAndOffsets(); return result; diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js index 6ea2c33e118c..842a3ecc9dbb 100644 --- a/Source/Core/TerrainMesh.js +++ b/Source/Core/TerrainMesh.js @@ -1,3 +1,5 @@ +import BoundingSphere from "./BoundingSphere.js"; +import Cartesian3 from "./Cartesian3.js"; import defaultValue from "./defaultValue.js"; /** @@ -107,7 +109,7 @@ function TerrainMesh( * A bounding sphere that completely contains the tile. * @type {BoundingSphere} */ - this.boundingSphere3D = boundingSphere3D; + this.boundingSphere3D = BoundingSphere.clone(boundingSphere3D); /** * The occludee point of the tile, represented in ellipsoid- @@ -152,5 +154,10 @@ function TerrainMesh( * @type {Number[]} */ this.northIndicesWestToEast = northIndicesWestToEast; + + /** + * Only for use with Proj4 and custom projections. + */ + this.center2D = Cartesian3.clone(encoding.center2D); } export default TerrainMesh; diff --git a/Source/Core/WebMercatorProjection.js b/Source/Core/WebMercatorProjection.js index 82e269ef95cd..4773708cd547 100644 --- a/Source/Core/WebMercatorProjection.js +++ b/Source/Core/WebMercatorProjection.js @@ -4,7 +4,9 @@ import defaultValue from "./defaultValue.js"; import defined from "./defined.js"; import DeveloperError from "./DeveloperError.js"; import Ellipsoid from "./Ellipsoid.js"; +import MapProjectionType from "./MapProjectionType.js"; import CesiumMath from "./Math.js"; +import SerializedMapProjection from "./SerializedMapProjection.js"; /** * The map projection used by Google Maps, Bing Maps, and most of ArcGIS Online, EPSG:3857. This @@ -38,8 +40,49 @@ Object.defineProperties(WebMercatorProjection.prototype, { return this._ellipsoid; }, }, + /** + * Gets whether or not the projection evenly maps meridians to vertical lines. + * The Web Mercator projection is cylindrical about the equator. + * + * @memberof WebMercatorProjection.prototype + * + * @type {Boolean} + * @readonly + * @private + */ + isNormalCylindrical: { + get: function () { + return true; + }, + }, }); +/** + * Returns a JSON object that can be messaged to a web worker. + * + * @private + * @returns {SerializedMapProjection} A JSON object from which the MapProjection can be rebuilt. + */ +WebMercatorProjection.prototype.serialize = function () { + return new SerializedMapProjection( + MapProjectionType.WEBMERCATOR, + Ellipsoid.pack(this.ellipsoid, []) + ); +}; + +/** + * Reconstructs a WebMercatorProjection object from the input JSON. + * + * @private + * @param {SerializedMapProjection} serializedMapProjection A JSON object from which the MapProjection can be rebuilt. + * @returns {Promise.} A Promise that resolves to a MapProjection that is ready for use, or rejects if the SerializedMapProjection is malformed. + */ +WebMercatorProjection.deserialize = function (serializedMapProjection) { + return Promise.resolve( + new WebMercatorProjection(Ellipsoid.unpack(serializedMapProjection.json)) + ); +}; + /** * Converts a Mercator angle, in the range -PI to PI, to a geodetic latitude * in the range -PI/2 to PI/2. diff --git a/Source/Core/sampleTerrain.js b/Source/Core/sampleTerrain.js index 97184bb870c3..9d8d81f902ab 100644 --- a/Source/Core/sampleTerrain.js +++ b/Source/Core/sampleTerrain.js @@ -1,4 +1,8 @@ import Check from "./Check.js"; +import GeographicProjection from "./GeographicProjection.js"; + +const geographicProjection = new GeographicProjection(); +const serializedGeographic = geographicProjection.serialize(); /** * Initiates a terrain height query for an array of {@link Cartographic} positions by @@ -166,6 +170,8 @@ function createInterpolateFunction(tileRequest) { // don't throttle this mesh creation because we've asked to sample these points; // so sample them! We don't care how many tiles that is! throttle: false, + // this mesh is not for rendering, so the projection for 2D doesn't matter + serializedMapProjection: serializedGeographic, }) .then(function () { // mesh has been created - so go through every position (maybe again) diff --git a/Source/DataSources/GeometryVisualizer.js b/Source/DataSources/GeometryVisualizer.js index 63a65b327ecb..39e84aa9a379 100644 --- a/Source/DataSources/GeometryVisualizer.js +++ b/Source/DataSources/GeometryVisualizer.js @@ -240,13 +240,15 @@ function GeometryVisualizer( ClassificationType.NUMBER_OF_CLASSIFICATION_TYPES; const groundColorBatches = new Array(numberOfClassificationTypes); const groundMaterialBatches = []; + const mapProjection = scene.mapProjection; if (supportsMaterialsforEntitiesOnTerrain) { for (i = 0; i < numberOfClassificationTypes; ++i) { groundMaterialBatches.push( new StaticGroundGeometryPerMaterialBatch( groundPrimitives, i, - MaterialAppearance + MaterialAppearance, + mapProjection ) ); groundColorBatches[i] = new StaticGroundGeometryColorBatch( @@ -258,7 +260,8 @@ function GeometryVisualizer( for (i = 0; i < numberOfClassificationTypes; ++i) { groundColorBatches[i] = new StaticGroundGeometryColorBatch( groundPrimitives, - i + i, + mapProjection ); } } diff --git a/Source/DataSources/StaticGroundGeometryColorBatch.js b/Source/DataSources/StaticGroundGeometryColorBatch.js index 54c5a9ee95a9..74b06bff3cc6 100644 --- a/Source/DataSources/StaticGroundGeometryColorBatch.js +++ b/Source/DataSources/StaticGroundGeometryColorBatch.js @@ -14,7 +14,7 @@ const colorScratch = new Color(); const distanceDisplayConditionScratch = new DistanceDisplayCondition(); const defaultDistanceDisplayCondition = new DistanceDisplayCondition(); -function Batch(primitives, classificationType, color, zIndex) { +function Batch(primitives, classificationType, color, zIndex, mapProjection) { this.primitives = primitives; this.zIndex = zIndex; this.classificationType = classificationType; @@ -31,7 +31,7 @@ function Batch(primitives, classificationType, color, zIndex) { this.showsUpdated = new AssociativeArray(); this.itemsToRemove = []; this.isDirty = false; - this.rectangleCollisionCheck = new RectangleCollisionChecker(); + this.rectangleCollisionCheck = new RectangleCollisionChecker(mapProjection); } Batch.prototype.overlapping = function (rectangle) { @@ -282,10 +282,15 @@ Batch.prototype.removeAllPrimitives = function () { /** * @private */ -function StaticGroundGeometryColorBatch(primitives, classificationType) { +function StaticGroundGeometryColorBatch( + primitives, + classificationType, + mapProjection +) { this._batches = []; this._primitives = primitives; this._classificationType = classificationType; + this._mapProjection = mapProjection; } StaticGroundGeometryColorBatch.prototype.add = function (time, updater) { @@ -310,7 +315,8 @@ StaticGroundGeometryColorBatch.prototype.add = function (time, updater) { this._primitives, this._classificationType, instance.attributes.color.value, - zIndex + zIndex, + this._mapProjection ); batches.push(batch); } diff --git a/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js b/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js index c162597ec714..cf8702850227 100644 --- a/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js +++ b/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js @@ -21,7 +21,8 @@ function Batch( appearanceType, materialProperty, usingSphericalTextureCoordinates, - zIndex + zIndex, + mapProjection ) { this.primitives = primitives; // scene level primitive collection this.classificationType = classificationType; @@ -44,7 +45,7 @@ function Batch( this.showsUpdated = new AssociativeArray(); this.usingSphericalTextureCoordinates = usingSphericalTextureCoordinates; this.zIndex = zIndex; - this.rectangleCollisionCheck = new RectangleCollisionChecker(); + this.rectangleCollisionCheck = new RectangleCollisionChecker(mapProjection); } Batch.prototype.onMaterialChanged = function () { @@ -314,12 +315,14 @@ Batch.prototype.destroy = function () { function StaticGroundGeometryPerMaterialBatch( primitives, classificationType, - appearanceType + appearanceType, + mapProjection ) { this._items = []; this._primitives = primitives; this._classificationType = classificationType; this._appearanceType = appearanceType; + this._mapProjection = mapProjection; } StaticGroundGeometryPerMaterialBatch.prototype.add = function (time, updater) { @@ -354,7 +357,8 @@ StaticGroundGeometryPerMaterialBatch.prototype.add = function (time, updater) { this._appearanceType, updater.fillMaterialProperty, usingSphericalTextureCoordinates, - zIndex + zIndex, + this._mapProjection ); batch.add(time, updater, geometryInstance); items.push(batch); diff --git a/Source/Scene/Camera.js b/Source/Scene/Camera.js index a6cfa7afcc00..2a39f3cf2a12 100644 --- a/Source/Scene/Camera.js +++ b/Source/Scene/Camera.js @@ -15,6 +15,7 @@ import HeadingPitchRange from "../Core/HeadingPitchRange.js"; import HeadingPitchRoll from "../Core/HeadingPitchRoll.js"; import Intersect from "../Core/Intersect.js"; import IntersectionTests from "../Core/IntersectionTests.js"; +import MapProjection from "../Core/MapProjection.js"; import CesiumMath from "../Core/Math.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; @@ -233,8 +234,9 @@ function Camera(scene) { this._modeChanged = true; const projection = scene.mapProjection; this._projection = projection; - this._maxCoord = projection.project( - new Cartographic(Math.PI, CesiumMath.PI_OVER_TWO) + this._maxCoord = MapProjection.approximateMaximumCoordinate( + projection, + new Cartesian2() ); this._max2Dfrustum = undefined; @@ -960,8 +962,12 @@ Object.defineProperties(Camera.prototype, { */ heading: { get: function () { - if (this._mode !== SceneMode.MORPHING) { - const ellipsoid = this._projection.ellipsoid; + const projection = this._projection; + if (this._mode === SceneMode.SCENE2D && !projection.isNormalCylindrical) { + updateMembers(this); + return approximateHeading2D(projection, this._position, this.up); + } else if (this._mode !== SceneMode.MORPHING) { + const ellipsoid = projection.ellipsoid; const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1); const transform = Transforms.eastNorthUpToFixedFrame( @@ -1303,10 +1309,10 @@ function setView2D(camera, position, hpr, convert) { scratchSetViewTransform1 ); camera._setTransform(Matrix4.IDENTITY); + const projection = camera._projection; if (!Cartesian3.equals(position, camera.positionWC)) { if (convert) { - const projection = camera._projection; const cartographic = projection.ellipsoid.cartesianToCartographic( position, scratchSetViewCartographic @@ -1329,21 +1335,31 @@ function setView2D(camera, position, hpr, convert) { } } + camera._setTransform(currentTransform); + if (camera._scene.mapMode2D === MapMode2D.ROTATE) { - hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; - hpr.pitch = -CesiumMath.PI_OVER_TWO; - hpr.roll = 0.0; - const rotQuat = Quaternion.fromHeadingPitchRoll( - hpr, - scratchSetViewQuaternion - ); - const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); + // If the projection is normal-cylindrical, + // assume that heading is a similar 2D vector anywhere in 2D space. + if (projection.isNormalCylindrical) { + hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; + hpr.pitch = -CesiumMath.PI_OVER_TWO; + hpr.roll = 0.0; + const rotQuat = Quaternion.fromHeadingPitchRoll( + hpr, + scratchSetViewQuaternion + ); + const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); - Matrix3.getColumn(rotMat, 2, camera.up); - Cartesian3.cross(camera.direction, camera.up, camera.right); + Matrix3.getColumn(rotMat, 2, camera.up); + Cartesian3.cross(camera.direction, camera.up, camera.right); + } else { + // Otherwise, twist camera at its new position according to new heading + const currentHeading = camera.heading; + if (currentHeading !== hpr.heading) { + camera.twistRight(hpr.heading - currentHeading); + } + } } - - camera._setTransform(currentTransform); } const scratchToHPRDirection = new Cartesian3(); @@ -1382,6 +1398,67 @@ function directionUpToHeadingPitchRoll(camera, position, orientation, result) { return result; } +const heightlessPositionScratch = new Cartesian3(); +const heightlessEndpointScratch = new Cartesian3(); +const cartographicPositionScratch = new Cartographic(); +const cartographicEndpointScratch = new Cartographic(); +const fixedFramePositionScratch = new Cartesian3(); +const fixedFrameEndpointScratch = new Cartesian3(); +const fixedFrameDirectionScratch = new Cartesian3(); +const fixedFrameToEnuScratch = new Matrix4(); +const enuDirectionScratch = new Cartesian3(); +function approximateHeading2D(projection, position, direction) { + const heightlessPosition = heightlessPositionScratch; + heightlessPosition.x = position.x; + heightlessPosition.y = position.y; + const heightlessEndpoint = heightlessEndpointScratch; + heightlessEndpoint.x = position.x + direction.x; + heightlessEndpoint.y = position.y + direction.y; + + const cartographicPosition = projection.unproject( + heightlessPosition, + cartographicPositionScratch + ); + const cartographicEndpoint = projection.unproject( + heightlessEndpoint, + cartographicEndpointScratch + ); + + const ellipsoid = projection.ellipsoid; + const fixedFramePosition = ellipsoid.cartographicToCartesian( + cartographicPosition, + fixedFramePositionScratch + ); + const fixedFrameEndpoint = ellipsoid.cartographicToCartesian( + cartographicEndpoint, + fixedFrameEndpointScratch + ); + const fixedFrameDirection = Cartesian3.subtract( + fixedFrameEndpoint, + fixedFramePosition, + fixedFrameDirectionScratch + ); + const enuToFixedFrame = Transforms.eastNorthUpToFixedFrame( + fixedFramePosition, + ellipsoid, + fixedFrameToEnuScratch + ); + const fixedFrameToEnu = Matrix4.inverse( + enuToFixedFrame, + fixedFrameToEnuScratch + ); + + const enuDirection = Matrix4.multiplyByPointAsVector( + fixedFrameToEnu, + fixedFrameDirection, + enuDirectionScratch + ); + + const heading = + Math.atan2(enuDirection.y, enuDirection.x) - CesiumMath.PI_OVER_TWO; + return CesiumMath.TWO_PI - CesiumMath.zeroToTwoPi(heading); +} + const scratchSetViewOptions = { destination: undefined, orientation: { diff --git a/Source/Scene/FrameState.js b/Source/Scene/FrameState.js index d352cfbb9f13..4d84f024760a 100644 --- a/Source/Scene/FrameState.js +++ b/Source/Scene/FrameState.js @@ -119,6 +119,14 @@ function FrameState(context, creditDisplay, jobScheduler) { */ this.mapProjection = undefined; + /** + * The serialized map projection. + * + * @type {SerializedMapProjection} + * @default undefined + */ + this.serializedMapProjection = undefined; + /** * The current camera. * diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 9220ff7a8247..d394009117b7 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -733,15 +733,15 @@ Globe.prototype.pickWorldCoordinates = function ( continue; } - let boundingVolume = surfaceTile.pickBoundingSphere; + const boundingVolume = surfaceTile.pickBoundingSphere; if (mode !== SceneMode.SCENE3D) { - surfaceTile.pickBoundingSphere = boundingVolume = BoundingSphere.fromRectangleWithHeights2D( - tile.rectangle, + const tileBoundingSphere2D = tile.getBoundingSphere2D( projection, surfaceTile.tileBoundingRegion.minimumHeight, - surfaceTile.tileBoundingRegion.maximumHeight, - boundingVolume + surfaceTile.tileBoundingRegion.maximumHeight ); + BoundingSphere.clone(tileBoundingSphere2D, boundingVolume); + surfaceTile.pickBoundingSphere = boundingVolume; Cartesian3.fromElements( boundingVolume.center.z, boundingVolume.center.x, diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 813f8296131b..1b9c06b254b8 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -116,6 +116,12 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) { quantizationDefine = "QUANTIZATION_BITS12"; } + let positions2d = 0; + let positions2dDefine = ""; + if (!frameState.mapProjection.isNormalCylindrical) { + positions2d = 1; + positions2dDefine = "POSITIONS_2D"; + } let cartographicLimitRectangleFlag = 0; let cartographicLimitRectangleDefine = ""; if (clippedByBoundaries) { @@ -161,7 +167,8 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) { (hasExaggeration << 27) | (showUndergroundColor << 28) | (translucent << 29) | - (applyDayNightAlpha << 30); + (applyDayNightAlpha << 30) | + (positions2d << 31); let currentClippingShaderState = 0; if (defined(clippingPlanes) && clippingPlanes.length > 0) { @@ -202,7 +209,7 @@ GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) { ); // Need to go before GlobeFS } - vs.defines.push(quantizationDefine); + vs.defines.push(quantizationDefine, positions2dDefine); fs.defines.push( `TEXTURE_UNITS ${numberOfDayTextures}`, cartographicLimitRectangleDefine, diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index de46d55b0b12..d9b6a50bd84c 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -766,6 +766,7 @@ const scratchCreateMeshOptions = { exaggeration: 1.0, exaggerationRelativeHeight: 0.0, throttle: true, + serializedMapProjection: undefined, }; function transform(surfaceTile, frameState, terrainProvider, x, y, level) { @@ -780,6 +781,8 @@ function transform(surfaceTile, frameState, terrainProvider, x, y, level) { createMeshOptions.exaggerationRelativeHeight = frameState.terrainExaggerationRelativeHeight; createMeshOptions.throttle = true; + createMeshOptions.serializedMapProjection = + frameState.serializedMapProjection; const terrainData = surfaceTile.terrainData; const meshPromise = terrainData.createMesh(createMeshOptions); diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 544d28700da0..7bfd28b0fcf3 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -753,13 +753,13 @@ GlobeSurfaceTileProvider.prototype.computeTileVisibility = function ( if (frameState.mode !== SceneMode.SCENE3D) { boundingVolume = boundingSphereScratch; - BoundingSphere.fromRectangleWithHeights2D( - tile.rectangle, + const tileBoundingSphere2D = tile.getBoundingSphere2D( frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume ); + BoundingSphere.clone(tileBoundingSphere2D, boundingVolume); Cartesian3.fromElements( boundingVolume.center.z, boundingVolume.center.x, @@ -1628,6 +1628,9 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) { u_center3D: function () { return this.properties.center3D; }, + u_center2D: function () { + return this.properties.center2D; + }, u_terrainExaggerationAndRelativeHeight: function () { return this.properties.terrainExaggerationAndRelativeHeight; }, @@ -1804,6 +1807,7 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) { hsbShift: new Cartesian3(), center3D: undefined, + center2D: undefined, rtc: new Cartesian3(), modifiedModelView: new Matrix4(), tileRectangle: new Cartesian4(), @@ -2191,14 +2195,9 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { if (frameState.mode !== SceneMode.SCENE3D) { const projection = frameState.mapProjection; - const southwest = projection.project( - Rectangle.southwest(tile.rectangle), - southwestScratch - ); - const northeast = projection.project( - Rectangle.northeast(tile.rectangle), - northeastScratch - ); + const southwest = southwestScratch; + const northeast = northeastScratch; + tile.getProjectedCorners(projection, southwest, northeast); tileRectangle.x = southwest.x; tileRectangle.y = southwest.y; @@ -2207,10 +2206,18 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { // In 2D and Columbus View, use the center of the tile for RTC rendering. if (frameState.mode !== SceneMode.MORPHING) { + // If using a custom projection, project the existing tile center instead. rtc = rtcScratch; - rtc.x = 0.0; - rtc.y = (tileRectangle.z + tileRectangle.x) * 0.5; - rtc.z = (tileRectangle.w + tileRectangle.y) * 0.5; + if (projection.isNormalCylindrical) { + rtc.x = 0.0; + rtc.y = (tileRectangle.z + tileRectangle.x) * 0.5; + rtc.z = (tileRectangle.w + tileRectangle.y) * 0.5; + } else { + rtc.x = mesh.center2D.z; + rtc.y = mesh.center2D.x; + rtc.z = mesh.center2D.y; + } + tileRectangle.x -= rtc.y; tileRectangle.y -= rtc.z; tileRectangle.z -= rtc.y; @@ -2428,6 +2435,7 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { ); } + uniformMapProperties.center2D = mesh.center2D; uniformMapProperties.terrainExaggerationAndRelativeHeight.x = exaggeration; uniformMapProperties.terrainExaggerationAndRelativeHeight.y = exaggerationRelativeHeight; @@ -2771,13 +2779,13 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { const orientedBoundingBox = command.orientedBoundingBox; if (frameState.mode !== SceneMode.SCENE3D) { - BoundingSphere.fromRectangleWithHeights2D( - tile.rectangle, + const tileBoundingRegion = surfaceTile.tileBoundingRegion; + const tileBoundingSphere2D = tile.getBoundingSphere2D( frameState.mapProjection, tileBoundingRegion.minimumHeight, - tileBoundingRegion.maximumHeight, - boundingVolume + tileBoundingRegion.maximumHeight ); + BoundingSphere.clone(tileBoundingSphere2D, boundingVolume); Cartesian3.fromElements( boundingVolume.center.z, boundingVolume.center.x, diff --git a/Source/Scene/GroundPolylinePrimitive.js b/Source/Scene/GroundPolylinePrimitive.js index f6fe97e1827b..edca07984035 100644 --- a/Source/Scene/GroundPolylinePrimitive.js +++ b/Source/Scene/GroundPolylinePrimitive.js @@ -749,7 +749,7 @@ GroundPolylinePrimitive.prototype.update = function (frameState) { // Update each geometry for framestate.scene3DOnly = true and projection geometryInstance.geometry._scene3DOnly = frameState.scene3DOnly; - GroundPolylineGeometry.setProjectionAndEllipsoid( + GroundPolylineGeometry.setProjection( geometryInstance.geometry, frameState.mapProjection ); diff --git a/Source/Scene/Primitive.js b/Source/Scene/Primitive.js index 283fa801bedd..6731573467a9 100644 --- a/Source/Scene/Primitive.js +++ b/Source/Scene/Primitive.js @@ -1243,6 +1243,7 @@ function loadAsynchronous(primitive, frameState) { createGeometryTaskProcessors[i].scheduleTask( { subTasks: subTasks[i], + serializedMapProjection: frameState.serializedMapProjection, }, subTaskTransferableObjects ) @@ -1266,21 +1267,20 @@ function loadAsynchronous(primitive, frameState) { : [primitive.geometryInstances]; const scene3DOnly = frameState.scene3DOnly; - const projection = frameState.mapProjection; const promise = combineGeometryTaskProcessor.scheduleTask( PrimitivePipeline.packCombineGeometryParameters( { createGeometryResults: primitive._createGeometryResults, instances: instances, - ellipsoid: projection.ellipsoid, - projection: projection, + ellipsoid: frameState.mapProjection.ellipsoid, elementIndexUintSupported: frameState.context.elementIndexUint, scene3DOnly: scene3DOnly, vertexCacheOptimize: primitive.vertexCacheOptimize, compressVertices: primitive.compressVertices, modelMatrix: primitive.modelMatrix, createPickOffsets: primitive._createPickOffsets, + serializedMapProjection: frameState.serializedMapProjection, }, transferableObjects ), diff --git a/Source/Scene/PrimitivePipeline.js b/Source/Scene/PrimitivePipeline.js index fa1312ee8406..0730e4546da0 100644 --- a/Source/Scene/PrimitivePipeline.js +++ b/Source/Scene/PrimitivePipeline.js @@ -2,9 +2,9 @@ import BoundingSphere from "../Core/BoundingSphere.js"; import ComponentDatatype from "../Core/ComponentDatatype.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; +import deserializeMapProjection from "../Core/deserializeMapProjection.js"; import DeveloperError from "../Core/DeveloperError.js"; import Ellipsoid from "../Core/Ellipsoid.js"; -import GeographicProjection from "../Core/GeographicProjection.js"; import Geometry from "../Core/Geometry.js"; import GeometryAttribute from "../Core/GeometryAttribute.js"; import GeometryAttributes from "../Core/GeometryAttributes.js"; @@ -12,7 +12,6 @@ import GeometryPipeline from "../Core/GeometryPipeline.js"; import IndexDatatype from "../Core/IndexDatatype.js"; import Matrix4 from "../Core/Matrix4.js"; import OffsetGeometryInstanceAttribute from "../Core/OffsetGeometryInstanceAttribute.js"; -import WebMercatorProjection from "../Core/WebMercatorProjection.js"; function transformToWorldCoordinates( instances, @@ -713,13 +712,13 @@ PrimitivePipeline.packCombineGeometryParameters = function ( transferableObjects ), ellipsoid: parameters.ellipsoid, - isGeographic: parameters.projection instanceof GeographicProjection, elementIndexUintSupported: parameters.elementIndexUintSupported, scene3DOnly: parameters.scene3DOnly, vertexCacheOptimize: parameters.vertexCacheOptimize, compressVertices: parameters.compressVertices, modelMatrix: parameters.modelMatrix, createPickOffsets: parameters.createPickOffsets, + serializedMapProjection: parameters.serializedMapProjection, }; }; @@ -752,21 +751,22 @@ PrimitivePipeline.unpackCombineGeometryParameters = function ( } const ellipsoid = Ellipsoid.clone(packedParameters.ellipsoid); - const projection = packedParameters.isGeographic - ? new GeographicProjection(ellipsoid) - : new WebMercatorProjection(ellipsoid); - return { - instances: instances, - ellipsoid: ellipsoid, - projection: projection, - elementIndexUintSupported: packedParameters.elementIndexUintSupported, - scene3DOnly: packedParameters.scene3DOnly, - vertexCacheOptimize: packedParameters.vertexCacheOptimize, - compressVertices: packedParameters.compressVertices, - modelMatrix: Matrix4.clone(packedParameters.modelMatrix), - createPickOffsets: packedParameters.createPickOffsets, - }; + return deserializeMapProjection( + packedParameters.serializedMapProjection + ).then(function (projection) { + return { + instances: instances, + ellipsoid: ellipsoid, + projection: projection, + elementIndexUintSupported: packedParameters.elementIndexUintSupported, + scene3DOnly: packedParameters.scene3DOnly, + vertexCacheOptimize: packedParameters.vertexCacheOptimize, + compressVertices: packedParameters.compressVertices, + modelMatrix: Matrix4.clone(packedParameters.modelMatrix), + createPickOffsets: packedParameters.createPickOffsets, + }; + }); }; function packBoundingSpheres(boundingSpheres) { diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 19f5b5abc480..d01290c7826b 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -1,3 +1,5 @@ +import BoundingSphere from "../Core/BoundingSphere.js"; +import Cartesian3 from "../Core/Cartesian3.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import Rectangle from "../Core/Rectangle.js"; @@ -74,6 +76,11 @@ function QuadtreeTile(options) { this._lastSelectionResultFrame = undefined; this._loadedCallbacks = {}; + // For caching the result when MapProjections are expensive + this._boundingSphere2D = undefined; + this._southwestProjected = undefined; + this._northeastProjected = undefined; + /** * Gets or sets the current state of the tile in the tile load pipeline. * @type {QuadtreeTileLoadState} @@ -404,6 +411,49 @@ Object.defineProperties(QuadtreeTile.prototype, { }, }); +QuadtreeTile.prototype.getBoundingSphere2D = function ( + mapProjection, + minimumHeight, + maximumHeight +) { + let boundingSphere2D = this._boundingSphere2D; + if (!defined(boundingSphere2D)) { + boundingSphere2D = BoundingSphere.fromRectangleWithHeights2D( + this.rectangle, + mapProjection, + minimumHeight, + maximumHeight + ); + this._boundingSphere2D = boundingSphere2D; + } + return boundingSphere2D; +}; + +QuadtreeTile.prototype.getProjectedCorners = function ( + mapProjection, + southwestResult, + northeastResult +) { + let projectedSouthwestCorner = this._southwestProjected; + let projectedNortheastCorner = this._northeastProjected; + + if (!defined(projectedSouthwestCorner)) { + const rectangle = this.rectangle; + projectedSouthwestCorner = mapProjection.project( + Rectangle.southwest(rectangle) + ); + projectedNortheastCorner = mapProjection.project( + Rectangle.northeast(rectangle) + ); + + this._southwestProjected = projectedSouthwestCorner; + this._northeastProjected = projectedNortheastCorner; + } + + Cartesian3.clone(projectedSouthwestCorner, southwestResult); + Cartesian3.clone(projectedNortheastCorner, northeastResult); +}; + QuadtreeTile.prototype.findLevelZeroTile = function (levelZeroTiles, x, y) { const xTiles = this.tilingScheme.getNumberOfXTilesAtLevel(0); if (x < 0) { diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index 33eeecae97dc..1ae68a716dbe 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -1,8 +1,8 @@ import BoundingRectangle from "../Core/BoundingRectangle.js"; import BoundingSphere from "../Core/BoundingSphere.js"; import BoxGeometry from "../Core/BoxGeometry.js"; +import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; -import Cartographic from "../Core/Cartographic.js"; import clone from "../Core/clone.js"; import Color from "../Core/Color.js"; import ColorGeometryInstanceAttribute from "../Core/ColorGeometryInstanceAttribute.js"; @@ -19,6 +19,7 @@ import GeometryInstance from "../Core/GeometryInstance.js"; import GeometryPipeline from "../Core/GeometryPipeline.js"; import Intersect from "../Core/Intersect.js"; import JulianDate from "../Core/JulianDate.js"; +import MapProjection from "../Core/MapProjection.js"; import CesiumMath from "../Core/Math.js"; import Matrix4 from "../Core/Matrix4.js"; import mergeSort from "../Core/mergeSort.js"; @@ -131,7 +132,7 @@ const requestRenderAfterFrame = function (scene) { * @param {Boolean} [options.orderIndependentTranslucency=true] If true and the configuration supports it, use order independent translucency. * @param {Boolean} [options.scene3DOnly=false] If true, optimizes memory use and performance for 3D mode but disables the ability to use 2D or Columbus View. * @param {Boolean} [options.shadows=false] Determines if shadows are cast by light sources. - * @param {MapMode2D} [options.mapMode2D=MapMode2D.INFINITE_SCROLL] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction. + * @param {MapMode2D} [options.mapMode2D] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction. MapMode2D.INFINITE_SCROLL is default for scenes using {@link GeographicProjection} or {@link WebMercatorProjection}. * @param {Boolean} [options.requestRenderMode=false] If true, rendering a frame will only occur when needed as determined by changes within the scene. Enabling improves performance of the application, but requires using {@link Scene#requestRender} to render a new frame explicitly in this mode. This will be necessary in many cases after making changes to the scene in other parts of the API. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @param {Number} [options.maximumRenderTimeChange=0.0] If requestRenderMode is true, this value defines the maximum change in simulation time allowed before a render is requested. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @param {Number} [depthPlaneEllipsoidOffset=0.0] Adjust the DepthPlane to address rendering artefacts below ellipsoid zero elevation. @@ -353,9 +354,15 @@ function Scene(options) { this._mode = SceneMode.SCENE3D; - this._mapProjection = defined(options.mapProjection) + const mapProjection = defined(options.mapProjection) ? options.mapProjection : new GeographicProjection(); + this._mapProjection = mapProjection; + this._serializedMapProjection = mapProjection.serialize(); + this._maxCoord2D = MapProjection.approximateMaximumCoordinate( + mapProjection, + new Cartesian2() + ); /** * The current morph transition time between 2D/Columbus View and 3D, @@ -606,7 +613,12 @@ function Scene(options) { this._screenSpaceCameraController = new ScreenSpaceCameraController(this); this._cameraUnderground = false; - this._mapMode2D = defaultValue(options.mapMode2D, MapMode2D.INFINITE_SCROLL); + this._mapMode2D = defaultValue( + options.mapMode2D, + mapProjection.isNormalCylindrical + ? MapMode2D.INFINITE_SCROLL + : MapMode2D.ROTATE + ); // Keeps track of the state of a frame. FrameState is the state across // the primitives of the scene. This state is for internally keeping track @@ -1900,6 +1912,7 @@ Scene.prototype.updateFrameState = function () { frameState.mode = this._mode; frameState.morphTime = this.morphTime; frameState.mapProjection = this.mapProjection; + frameState.serializedMapProjection = this._serializedMapProjection; frameState.camera = camera; frameState.cullingVolume = camera.frustum.computeCullingVolume( camera.positionWC, @@ -2944,11 +2957,7 @@ function executeWebVRCommands(scene, passState, backgroundColor) { Camera.clone(savedCamera, camera); } -const scratch2DViewportCartographic = new Cartographic( - Math.PI, - CesiumMath.PI_OVER_TWO -); -const scratch2DViewportMaxCoord = new Cartesian3(); +const scratch2DViewportMaxCoord = new Cartesian2(); const scratch2DViewportSavedPosition = new Cartesian3(); const scratch2DViewportTransform = new Matrix4(); const scratch2DViewportCameraTransform = new Matrix4(); @@ -2965,11 +2974,9 @@ function execute2DViewportCommands(scene, passState) { const viewport = BoundingRectangle.clone(originalViewport, scratch2DViewport); passState.viewport = viewport; - const maxCartographic = scratch2DViewportCartographic; const maxCoord = scratch2DViewportMaxCoord; - const projection = scene.mapProjection; - projection.project(maxCartographic, maxCoord); + Cartesian2.clone(scene._maxCoord2D, maxCoord); const position = Cartesian3.clone( camera.position, diff --git a/Source/Scene/ScreenSpaceCameraController.js b/Source/Scene/ScreenSpaceCameraController.js index 0763865ddec3..750e725cae77 100644 --- a/Source/Scene/ScreenSpaceCameraController.js +++ b/Source/Scene/ScreenSpaceCameraController.js @@ -10,6 +10,7 @@ import Ellipsoid from "../Core/Ellipsoid.js"; import HeadingPitchRoll from "../Core/HeadingPitchRoll.js"; import IntersectionTests from "../Core/IntersectionTests.js"; import KeyboardEventModifier from "../Core/KeyboardEventModifier.js"; +import MapProjection from "../Core/MapProjection.js"; import CesiumMath from "../Core/Math.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; @@ -296,8 +297,9 @@ function ScreenSpaceCameraController(scene) { this._cameraUnderground = false; const projection = scene.mapProjection; - this._maxCoord = projection.project( - new Cartographic(Math.PI, CesiumMath.PI_OVER_TWO) + this._maxCoord = MapProjection.approximateMaximumCoordinate( + projection, + new Cartesian2() ); // Constants, Make any of these public? @@ -683,6 +685,9 @@ function handleZoom( object._zoomWorldPosition ); } + + // Set heading again - in some projections, this may change due to camera.move + orientation.heading = camera.heading; } } else if (mode === SceneMode.SCENE3D) { const cameraPositionNormal = Cartesian3.normalize( diff --git a/Source/Scene/ShadowVolumeAppearance.js b/Source/Scene/ShadowVolumeAppearance.js index 169bf41b2c90..21eddc700a89 100644 --- a/Source/Scene/ShadowVolumeAppearance.js +++ b/Source/Scene/ShadowVolumeAppearance.js @@ -478,51 +478,51 @@ function addTextureCoordinateRotationAttributes( }); } -const cartographicScratch = new Cartographic(); const cornerScratch = new Cartesian3(); -const northWestScratch = new Cartesian3(); -const southEastScratch = new Cartesian3(); +const topLeftScratch = new Cartographic(); +const bottomRightScratch = new Cartographic(); +const projectedExtentsScratch = new Rectangle(); const highLowScratch = { high: 0.0, low: 0.0 }; function add2DTextureCoordinateAttributes(rectangle, projection, attributes) { // Compute corner positions in double precision - const carto = cartographicScratch; - carto.height = 0.0; - - carto.longitude = rectangle.west; - carto.latitude = rectangle.south; - - const southWestCorner = projection.project(carto, cornerScratch); - - carto.latitude = rectangle.north; - const northWest = projection.project(carto, northWestScratch); + const projectedExtents = Rectangle.approximateProjectedExtents( + { + cartographicRectangle: rectangle, + mapProjection: projection, + }, + projectedExtentsScratch + ); - carto.longitude = rectangle.east; - carto.latitude = rectangle.south; - const southEast = projection.project(carto, southEastScratch); + const bottomLeftCorner = Rectangle.southwest(projectedExtents, cornerScratch); + const topLeft = Rectangle.northwest(projectedExtents, topLeftScratch); + const bottomRight = Rectangle.southeast(projectedExtents, bottomRightScratch); // Since these positions are all in the 2D plane, there's a lot of zeros // and a lot of repetition. So we only need to encode 4 values. // Encode: - // x: x value for southWestCorner - // y: y value for southWestCorner - // z: y value for northWest - // w: x value for southEast + // x: x value for bottomLeftCorner + // y: y value for bottomLeftCorner + // z: y value for topLeft + // w: x value for bottomRight const valuesHigh = [0, 0, 0, 0]; const valuesLow = [0, 0, 0, 0]; - let encoded = EncodedCartesian3.encode(southWestCorner.x, highLowScratch); + let encoded = EncodedCartesian3.encode( + bottomLeftCorner.longitude, + highLowScratch + ); valuesHigh[0] = encoded.high; valuesLow[0] = encoded.low; - encoded = EncodedCartesian3.encode(southWestCorner.y, highLowScratch); + encoded = EncodedCartesian3.encode(bottomLeftCorner.latitude, highLowScratch); valuesHigh[1] = encoded.high; valuesLow[1] = encoded.low; - encoded = EncodedCartesian3.encode(northWest.y, highLowScratch); + encoded = EncodedCartesian3.encode(topLeft.latitude, highLowScratch); valuesHigh[2] = encoded.high; valuesLow[2] = encoded.low; - encoded = EncodedCartesian3.encode(southEast.x, highLowScratch); + encoded = EncodedCartesian3.encode(bottomRight.longitude, highLowScratch); valuesHigh[3] = encoded.high; valuesLow[3] = encoded.low; @@ -747,6 +747,7 @@ ShadowVolumeAppearance.getPlanarTextureCoordinateAttributes = function ( return attributes; }; +const cartographicScratch = new Cartographic(); const spherePointScratch = new Cartesian3(); function latLongToSpherical(latitude, longitude, ellipsoid, result) { const cartographic = cartographicScratch; diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js index 20a667b285e2..c38fb19614ed 100644 --- a/Source/Scene/TerrainFillMesh.js +++ b/Source/Scene/TerrainFillMesh.js @@ -747,6 +747,7 @@ function propagateEdge( const cartographicScratch = new Cartographic(); const centerCartographicScratch = new Cartographic(); const cartesianScratch = new Cartesian3(); +const projectedCartesianScratch = new Cartesian3(); const normalScratch = new Cartesian3(); const octEncodedNormalScratch = new Cartesian2(); const uvScratch2 = new Cartesian2(); @@ -810,6 +811,9 @@ const nwVertexScratch = new HeightAndNormal(); const neVertexScratch = new HeightAndNormal(); const heightmapBuffer = typeof Uint8Array !== "undefined" ? new Uint8Array(9 * 9) : undefined; +const relativeToCenter2dScratch = new Cartesian3(); +const obbCenterCartographicScratch = new Cartographic(); +const obbCenter2dScratch = new Cartographic(); const scratchCreateMeshSyncOptions = { tilingScheme: undefined, @@ -818,6 +822,7 @@ const scratchCreateMeshSyncOptions = { level: 0, exaggeration: 1.0, exaggerationRelativeHeight: 0.0, + mapProjection: undefined, }; function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { GlobeSurfaceTile.initialize( @@ -976,6 +981,8 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { // at levels 1, 2, and 3. maxTileWidth *= 1.5; + const mapProjection = frameState.mapProjection; + if ( rectangle.width > maxTileWidth && maximumHeight - minimumHeight <= geometricError @@ -996,13 +1003,14 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { createMeshSyncOptions.x = tile.x; createMeshSyncOptions.y = tile.y; createMeshSyncOptions.level = tile.level; + createMeshSyncOptions.mapProjection = mapProjection; createMeshSyncOptions.exaggeration = exaggeration; createMeshSyncOptions.exaggerationRelativeHeight = exaggerationRelativeHeight; fill.mesh = terrainData._createMeshSync(createMeshSyncOptions); } else { const hasGeodeticSurfaceNormals = hasExaggeration; - const centerCartographic = Rectangle.center( + let centerCartographic = Rectangle.center( rectangle, centerCartographicScratch ); @@ -1011,6 +1019,19 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { centerCartographic, scratchCenter ); + const hasCustomProjection = !mapProjection.isNormalCylindrical; + + let center2D; + if (hasCustomProjection) { + centerCartographic = centerCartographicScratch; + centerCartographic.longitude = (rectangle.east + rectangle.west) * 0.5; + centerCartographic.latitude = (rectangle.north + rectangle.south) * 0.5; + centerCartographic.height = middleHeight; + center2D = mapProjection.project( + centerCartographic, + relativeToCenter2dScratch + ); + } const encoding = new TerrainEncoding( center, undefined, @@ -1021,7 +1042,8 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { true, hasGeodeticSurfaceNormals, exaggeration, - exaggerationRelativeHeight + exaggerationRelativeHeight, + center2D ); // At _most_, we have vertices for the 4 corners, plus 1 center, plus every adjacent edge vertex. @@ -1071,7 +1093,8 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { nwCorner.height, nwCorner.encodedNormal, 1.0, - heightRange + heightRange, + mapProjection ); nextIndex = addEdge( fill, @@ -1096,7 +1119,8 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { swCorner.height, swCorner.encodedNormal, 0.0, - heightRange + heightRange, + mapProjection ); nextIndex = addEdge( fill, @@ -1121,7 +1145,8 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { seCorner.height, seCorner.encodedNormal, 0.0, - heightRange + heightRange, + mapProjection ); nextIndex = addEdge( fill, @@ -1146,7 +1171,8 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { neCorner.height, neCorner.encodedNormal, 1.0, - heightRange + heightRange, + mapProjection ); nextIndex = addEdge( fill, @@ -1195,16 +1221,38 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { ); const centerIndex = nextIndex; - encoding.encode( - typedArray, - nextIndex * stride, - obb.center, - Cartesian2.fromElements(0.5, 0.5, uvScratch), - middleHeight, - centerEncodedNormal, - centerWebMercatorT, - geodeticSurfaceNormal - ); + const obbCenter = obb.center; + if (hasCustomProjection) { + const obbCenterCartographic = ellipsoid.cartesianToCartographic( + obbCenter, + obbCenterCartographicScratch + ); + const obbCenter2D = mapProjection.project( + obbCenterCartographic, + obbCenter2dScratch + ); + encoding.encode( + typedArray, + nextIndex * stride, + obbCenter, + Cartesian2.fromElements(0.5, 0.5, uvScratch), + middleHeight, + centerEncodedNormal, + centerWebMercatorT, + geodeticSurfaceNormal, + obbCenter2D + ); + } else { + encoding.encode( + typedArray, + nextIndex * stride, + obbCenter, + Cartesian2.fromElements(0.5, 0.5, uvScratch), + middleHeight, + centerEncodedNormal, + centerWebMercatorT + ); + } ++nextIndex; const vertexCount = nextIndex; @@ -1345,7 +1393,8 @@ function addVertexWithComputedPosition( height, encodedNormal, webMercatorT, - heightRange + heightRange, + mapProjection ) { const cartographic = cartographicScratch; cartographic.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u); @@ -1368,16 +1417,34 @@ function addVertexWithComputedPosition( uv.x = u; uv.y = v; - encoding.encode( - buffer, - index * encoding.stride, - position, - uv, - height, - encodedNormal, - webMercatorT, - geodeticSurfaceNormal - ); + if (encoding.hasPositions2D) { + const position2D = mapProjection.project( + cartographic, + projectedCartesianScratch + ); + encoding.encode( + buffer, + index * encoding.stride, + position, + uv, + height, + encodedNormal, + webMercatorT, + geodeticSurfaceNormal, + position2D + ); + } else { + encoding.encode( + buffer, + index * encoding.stride, + position, + uv, + height, + encodedNormal, + webMercatorT, + geodeticSurfaceNormal + ); + } heightRange.minimumHeight = Math.min(heightRange.minimumHeight, height); heightRange.maximumHeight = Math.max(heightRange.maximumHeight, height); @@ -1948,16 +2015,35 @@ function addEdgeMesh( ); } - encoding.encode( - typedArray, - nextIndex * targetStride, - position, - uv, - height, - normal, - webMercatorT, - geodeticSurfaceNormal - ); + if (encoding.hasPositions2D) { + const position2D = sourceEncoding.decodePosition2D( + sourceVertices, + index, + projectedCartesianScratch + ); + encoding.encode( + typedArray, + nextIndex * targetStride, + position, + uv, + height, + normal, + webMercatorT, + geodeticSurfaceNormal, + position2D + ); + } else { + encoding.encode( + typedArray, + nextIndex * targetStride, + position, + uv, + height, + normal, + webMercatorT, + geodeticSurfaceNormal + ); + } heightRange.minimumHeight = Math.min(heightRange.minimumHeight, height); heightRange.maximumHeight = Math.max(heightRange.maximumHeight, height); diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js index e43a8b4922c8..0e3adb7252db 100644 --- a/Source/Scene/TileBoundingRegion.js +++ b/Source/Scene/TileBoundingRegion.js @@ -99,6 +99,9 @@ function TileBoundingRegion(options) { */ this.northNormal = new Cartesian3(); + this._projectedSouthWestCornerCartesian = undefined; + this._projectedNorthEastCornerCartesian = undefined; + const ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); computeBox(this, options.rectangle, ellipsoid); @@ -153,6 +156,31 @@ TileBoundingRegion.prototype.computeBoundingVolumes = function (ellipsoid) { ); }; +function getProjectedCorners( + tile, + mapProjection, + southwestResult, + northeastResult +) { + let projectedSouthwestCorner = tile._projectedSouthWestCornerCartesian; + let projectedNortheastCorner = tile._projectedNorthEastCornerCartesian; + + if (!defined(projectedSouthwestCorner)) { + projectedSouthwestCorner = mapProjection.project( + Rectangle.southwest(tile.rectangle) + ); + projectedNortheastCorner = mapProjection.project( + Rectangle.northeast(tile.rectangle) + ); + + tile._projectedSouthWestCornerCartesian = projectedSouthwestCorner; + tile._projectedNorthEastCornerCartesian = projectedNortheastCorner; + } + + Cartesian3.clone(projectedSouthwestCorner, southwestResult); + Cartesian3.clone(projectedNortheastCorner, northeastResult); +} + const cartesian3Scratch = new Cartesian3(); const cartesian3Scratch2 = new Cartesian3(); const cartesian3Scratch3 = new Cartesian3(); @@ -325,17 +353,18 @@ function distanceToCameraRegion(tileBB, frameState) { let northNormal = tileBB.northNormal; if (frameState.mode !== SceneMode.SCENE3D) { - southwestCornerCartesian = frameState.mapProjection.project( - Rectangle.southwest(tileBB.rectangle), - southwestCornerScratch + southwestCornerCartesian = southwestCornerScratch; + northeastCornerCartesian = northeastCornerScratch; + getProjectedCorners( + tileBB, + frameState.mapProjection, + southwestCornerCartesian, + northeastCornerCartesian ); + southwestCornerCartesian.z = southwestCornerCartesian.y; southwestCornerCartesian.y = southwestCornerCartesian.x; southwestCornerCartesian.x = 0.0; - northeastCornerCartesian = frameState.mapProjection.project( - Rectangle.northeast(tileBB.rectangle), - northeastCornerScratch - ); northeastCornerCartesian.z = northeastCornerCartesian.y; northeastCornerCartesian.y = northeastCornerCartesian.x; northeastCornerCartesian.x = 0.0; diff --git a/Source/Shaders/GlobeVS.glsl b/Source/Shaders/GlobeVS.glsl index 2bdda38f5a28..845cee69cfb7 100644 --- a/Source/Shaders/GlobeVS.glsl +++ b/Source/Shaders/GlobeVS.glsl @@ -6,6 +6,12 @@ attribute vec4 position3DAndHeight; attribute vec4 textureCoordAndEncodedNormals; #endif +#ifdef POSITIONS_2D +attribute vec3 position2D; + +uniform vec3 u_center2D; +#endif + #ifdef GEODETIC_SURFACE_NORMALS attribute vec3 geodeticSurfaceNormal; #endif @@ -85,9 +91,13 @@ float get2DGeographicYPositionFraction(vec2 textureCoordinates) vec4 getPositionPlanarEarth(vec3 position, float height, vec2 textureCoordinates) { +#ifdef POSITIONS_2D + return u_modifiedModelViewProjection * vec4(position2D.zxy, 1.0); +#else float yPositionFraction = get2DYPositionFraction(textureCoordinates); vec4 rtcPosition2D = vec4(height, mix(u_tileRectangle.st, u_tileRectangle.pq, vec2(textureCoordinates.x, yPositionFraction)), 1.0); return u_modifiedModelViewProjection * rtcPosition2D; +#endif } vec4 getPosition2DMode(vec3 position, float height, vec2 textureCoordinates) @@ -106,7 +116,13 @@ vec4 getPositionMorphingMode(vec3 position, float height, vec2 textureCoordinate // This is unlikely to be noticeable, though. vec3 position3DWC = position + u_center3D; float yPositionFraction = get2DYPositionFraction(textureCoordinates); + +#ifdef POSITIONS_2D + vec4 position2DWC = vec4(position2D.zxy + u_center2D.zxy, 1.0); +#else vec4 position2DWC = vec4(height, mix(u_tileRectangle.st, u_tileRectangle.pq, vec2(textureCoordinates.x, yPositionFraction)), 1.0); +#endif + vec4 morphPosition = czm_columbusViewMorph(position2DWC, vec4(position3DWC, 1.0), czm_morphTime); return czm_modelViewProjection * morphPosition; } diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index d5d011f855d5..45ebfd583d1b 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -332,7 +332,7 @@ function enableVRUI(viewer, enabled) { * the instance is assumed to be owned by the caller and will not be destroyed when the viewer is destroyed. * @property {Boolean} [shadows=false] Determines if shadows are cast by light sources. * @property {ShadowMode} [terrainShadows=ShadowMode.RECEIVE_ONLY] Determines if the terrain casts or receives shadows from light sources. - * @property {MapMode2D} [mapMode2D=MapMode2D.INFINITE_SCROLL] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction. + * @param {MapMode2D} [options.mapMode2D] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction. MapMode2D.INFINITE_SCROLL is default for scenes using {@link GeographicProjection} or {@link WebMercatorProjection}. * @property {Boolean} [projectionPicker=false] If set to true, the ProjectionPicker widget will be created. * @property {Boolean} [requestRenderMode=false] If true, rendering a frame will only occur when needed as determined by changes within the scene. Enabling reduces the CPU/GPU usage of your application and uses less battery on mobile, but requires using {@link Scene#requestRender} to render a new frame explicitly in this mode. This will be necessary in many cases after making changes to the scene in other parts of the API. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @property {Number} [maximumRenderTimeChange=0.0] If requestRenderMode is true, this value defines the maximum change in simulation time allowed before a render is requested. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. diff --git a/Source/WorkersES6/combineGeometry.js b/Source/WorkersES6/combineGeometry.js index 6ce02cf5c665..b41a5a6f62c0 100644 --- a/Source/WorkersES6/combineGeometry.js +++ b/Source/WorkersES6/combineGeometry.js @@ -2,13 +2,14 @@ import PrimitivePipeline from "../Scene/PrimitivePipeline.js"; import createTaskProcessorWorker from "./createTaskProcessorWorker.js"; function combineGeometry(packedParameters, transferableObjects) { - const parameters = PrimitivePipeline.unpackCombineGeometryParameters( + return PrimitivePipeline.unpackCombineGeometryParameters( packedParameters - ); - const results = PrimitivePipeline.combineGeometry(parameters); - return PrimitivePipeline.packCombineGeometryResults( - results, - transferableObjects - ); + ).then(function (parameters) { + const results = PrimitivePipeline.combineGeometry(parameters); + return PrimitivePipeline.packCombineGeometryResults( + results, + transferableObjects + ); + }); } export default createTaskProcessorWorker(combineGeometry); diff --git a/Source/WorkersES6/createGeometry.js b/Source/WorkersES6/createGeometry.js index b45d6a26736b..07a1bcb2b0a5 100644 --- a/Source/WorkersES6/createGeometry.js +++ b/Source/WorkersES6/createGeometry.js @@ -1,5 +1,6 @@ /* global require */ import defined from "../Core/defined.js"; +import deserializeMapProjection from "../Core/deserializeMapProjection.js"; import PrimitivePipeline from "../Scene/PrimitivePipeline.js"; import createTaskProcessorWorker from "./createTaskProcessorWorker.js"; @@ -26,27 +27,35 @@ function getModule(moduleName) { function createGeometry(parameters, transferableObjects) { const subTasks = parameters.subTasks; const length = subTasks.length; - const resultsOrPromises = new Array(length); + return deserializeMapProjection(parameters.serializedMapProjection).then( + function (mapProjection) { + const resultsOrPromises = new Array(length); - for (let i = 0; i < length; i++) { - const task = subTasks[i]; - const geometry = task.geometry; - const moduleName = task.moduleName; + for (let i = 0; i < length; i++) { + const task = subTasks[i]; + const geometry = task.geometry; + const moduleName = task.moduleName; - if (defined(moduleName)) { - const createFunction = getModule(moduleName); - resultsOrPromises[i] = createFunction(geometry, task.offset); - } else { - //Already created geometry - resultsOrPromises[i] = geometry; - } - } + if (defined(moduleName)) { + const createFunction = getModule(moduleName); + resultsOrPromises[i] = createFunction( + geometry, + task.offset, + mapProjection + ); + } else { + //Already created geometry + resultsOrPromises[i] = geometry; + } + } - return Promise.all(resultsOrPromises).then(function (results) { - return PrimitivePipeline.packCreateGeometryResults( - results, - transferableObjects - ); - }); + return Promise.all(resultsOrPromises).then(function (results) { + return PrimitivePipeline.packCreateGeometryResults( + results, + transferableObjects + ); + }); + } + ); } export default createTaskProcessorWorker(createGeometry); diff --git a/Source/WorkersES6/createGroundPolylineGeometry.js b/Source/WorkersES6/createGroundPolylineGeometry.js index 78705ce7c8ad..b726d682ea16 100644 --- a/Source/WorkersES6/createGroundPolylineGeometry.js +++ b/Source/WorkersES6/createGroundPolylineGeometry.js @@ -2,7 +2,11 @@ import ApproximateTerrainHeights from "../Core/ApproximateTerrainHeights.js"; import defined from "../Core/defined.js"; import GroundPolylineGeometry from "../Core/GroundPolylineGeometry.js"; -function createGroundPolylineGeometry(groundPolylineGeometry, offset) { +function createGroundPolylineGeometry( + groundPolylineGeometry, + offset, + mapProjection +) { return ApproximateTerrainHeights.initialize().then(function () { if (defined(offset)) { groundPolylineGeometry = GroundPolylineGeometry.unpack( @@ -10,6 +14,7 @@ function createGroundPolylineGeometry(groundPolylineGeometry, offset) { offset ); } + GroundPolylineGeometry.setProjection(groundPolylineGeometry, mapProjection); return GroundPolylineGeometry.createGeometry(groundPolylineGeometry); }); } diff --git a/Source/WorkersES6/createVerticesFromGoogleEarthEnterpriseBuffer.js b/Source/WorkersES6/createVerticesFromGoogleEarthEnterpriseBuffer.js index 7d41bcb9438c..32a33e511f68 100644 --- a/Source/WorkersES6/createVerticesFromGoogleEarthEnterpriseBuffer.js +++ b/Source/WorkersES6/createVerticesFromGoogleEarthEnterpriseBuffer.js @@ -5,6 +5,7 @@ import Cartesian3 from "../Core/Cartesian3.js"; import Cartographic from "../Core/Cartographic.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; +import deserializeMapProjection from "../Core/deserializeMapProjection.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import EllipsoidalOccluder from "../Core/EllipsoidalOccluder.js"; import CesiumMath from "../Core/Math.js"; @@ -42,41 +43,46 @@ function createVerticesFromGoogleEarthEnterpriseBuffer( parameters.ellipsoid = Ellipsoid.clone(parameters.ellipsoid); parameters.rectangle = Rectangle.clone(parameters.rectangle); - const statistics = processBuffer( - parameters.buffer, - parameters.relativeToCenter, - parameters.ellipsoid, - parameters.rectangle, - parameters.nativeRectangle, - parameters.exaggeration, - parameters.exaggerationRelativeHeight, - parameters.skirtHeight, - parameters.includeWebMercatorT, - parameters.negativeAltitudeExponentBias, - parameters.negativeElevationThreshold + return deserializeMapProjection(parameters.serializedMapProjection).then( + function (mapProjection) { + const statistics = processBuffer( + parameters.buffer, + parameters.relativeToCenter, + parameters.ellipsoid, + parameters.rectangle, + parameters.nativeRectangle, + parameters.exaggeration, + parameters.exaggerationRelativeHeight, + parameters.skirtHeight, + parameters.includeWebMercatorT, + parameters.negativeAltitudeExponentBias, + parameters.negativeElevationThreshold, + mapProjection + ); + const vertices = statistics.vertices; + transferableObjects.push(vertices.buffer); + const indices = statistics.indices; + transferableObjects.push(indices.buffer); + + return { + vertices: vertices.buffer, + indices: indices.buffer, + numberOfAttributes: statistics.encoding.stride, + minimumHeight: statistics.minimumHeight, + maximumHeight: statistics.maximumHeight, + boundingSphere3D: statistics.boundingSphere3D, + orientedBoundingBox: statistics.orientedBoundingBox, + occludeePointInScaledSpace: statistics.occludeePointInScaledSpace, + encoding: statistics.encoding, + vertexCountWithoutSkirts: statistics.vertexCountWithoutSkirts, + indexCountWithoutSkirts: statistics.indexCountWithoutSkirts, + westIndicesSouthToNorth: statistics.westIndicesSouthToNorth, + southIndicesEastToWest: statistics.southIndicesEastToWest, + eastIndicesNorthToSouth: statistics.eastIndicesNorthToSouth, + northIndicesWestToEast: statistics.northIndicesWestToEast, + }; + } ); - const vertices = statistics.vertices; - transferableObjects.push(vertices.buffer); - const indices = statistics.indices; - transferableObjects.push(indices.buffer); - - return { - vertices: vertices.buffer, - indices: indices.buffer, - numberOfAttributes: statistics.encoding.stride, - minimumHeight: statistics.minimumHeight, - maximumHeight: statistics.maximumHeight, - boundingSphere3D: statistics.boundingSphere3D, - orientedBoundingBox: statistics.orientedBoundingBox, - occludeePointInScaledSpace: statistics.occludeePointInScaledSpace, - encoding: statistics.encoding, - vertexCountWithoutSkirts: statistics.vertexCountWithoutSkirts, - indexCountWithoutSkirts: statistics.indexCountWithoutSkirts, - westIndicesSouthToNorth: statistics.westIndicesSouthToNorth, - southIndicesEastToWest: statistics.southIndicesEastToWest, - eastIndicesNorthToSouth: statistics.eastIndicesNorthToSouth, - northIndicesWestToEast: statistics.northIndicesWestToEast, - }; } const scratchCartographic = new Cartographic(); @@ -84,7 +90,8 @@ const scratchCartesian = new Cartesian3(); const minimumScratch = new Cartesian3(); const maximumScratch = new Cartesian3(); const matrix4Scratch = new Matrix4(); - +const projectedCartesian3Scratch = new Cartesian3(); +const relativeToCenter2dScratch = new Cartesian3(); function processBuffer( buffer, relativeToCenter, @@ -96,13 +103,15 @@ function processBuffer( skirtHeight, includeWebMercatorT, negativeAltitudeExponentBias, - negativeElevationThreshold + negativeElevationThreshold, + mapProjection ) { let geographicWest; let geographicSouth; let geographicEast; let geographicNorth; let rectangleWidth, rectangleHeight; + const hasCustomProjection = !mapProjection.isNormalCylindrical; if (!defined(rectangle)) { geographicWest = CesiumMath.toRadians(nativeRectangle.west); @@ -130,6 +139,18 @@ function processBuffer( ); const toENU = Matrix4.inverseTransformation(fromENU, matrix4Scratch); + let relativeToCenter2D; + if (hasCustomProjection) { + const cartographicRTC = ellipsoid.cartesianToCartographic( + relativeToCenter, + scratchCartographic + ); + relativeToCenter2D = mapProjection.project( + cartographicRTC, + relativeToCenter2dScratch + ); + } + let southMercatorY; let oneOverMercatorHeight; if (includeWebMercatorT) { @@ -201,6 +222,10 @@ function processBuffer( // Create arrays const positions = new Array(size); + let positions2D; + if (hasCustomProjection) { + positions2D = new Array(size); + } const uvs = new Array(size); const heights = new Array(size); const webMercatorTs = includeWebMercatorT ? new Array(size) : []; @@ -320,6 +345,10 @@ function processBuffer( const pos = ellipsoid.cartographicToCartesian(scratchCartographic); positions[pointOffset] = pos; + if (hasCustomProjection) { + positions2D[pointOffset] = mapProjection.project(scratchCartographic); + } + if (includeWebMercatorT) { webMercatorTs[pointOffset] = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - @@ -359,6 +388,9 @@ function processBuffer( } positions.length = pointOffset; + if (hasCustomProjection) { + positions2D.length = pointOffset; + } uvs.length = pointOffset; heights.length = pointOffset; if (includeWebMercatorT) { @@ -409,7 +441,9 @@ function processBuffer( westBorder, -percentage * rectangleWidth, true, - -percentage * rectangleHeight + -percentage * rectangleHeight, + positions2D, + mapProjection ); addSkirt( positions, @@ -421,7 +455,10 @@ function processBuffer( skirtOptions, southBorder, -percentage * rectangleHeight, - false + false, + 0.0, + positions2D, + mapProjection ); addSkirt( positions, @@ -434,7 +471,9 @@ function processBuffer( eastBorder, percentage * rectangleWidth, true, - percentage * rectangleHeight + percentage * rectangleHeight, + positions2D, + mapProjection ); addSkirt( positions, @@ -446,7 +485,10 @@ function processBuffer( skirtOptions, northBorder, percentage * rectangleHeight, - false + false, + 0.0, + positions2D, + mapProjection ); // Since the corner between the north and west sides is in the west array, generate the last @@ -498,22 +540,40 @@ function processBuffer( includeWebMercatorT, includeGeodeticSurfaceNormals, exaggeration, - exaggerationRelativeHeight + exaggerationRelativeHeight, + relativeToCenter2D ); const vertices = new Float32Array(size * encoding.stride); let bufferIndex = 0; - for (let k = 0; k < size; ++k) { - bufferIndex = encoding.encode( - vertices, - bufferIndex, - positions[k], - uvs[k], - heights[k], - undefined, - webMercatorTs[k], - geodeticSurfaceNormals[k] - ); + let k; + if (hasCustomProjection) { + for (k = 0; k < size; ++k) { + bufferIndex = encoding.encode( + vertices, + bufferIndex, + positions[k], + uvs[k], + heights[k], + undefined, + webMercatorTs[k], + geodeticSurfaceNormals[k], + positions2D[k] + ); + } + } else { + for (k = 0; k < size; ++k) { + bufferIndex = encoding.encode( + vertices, + bufferIndex, + positions[k], + uvs[k], + heights[k], + undefined, + webMercatorTs[k], + geodeticSurfaceNormals[k] + ); + } } const westIndicesSouthToNorth = westBorder @@ -576,8 +636,11 @@ function addSkirt( borderPoints, fudgeFactor, eastOrWest, - cornerFudge + cornerFudge, + positions2D, + mapProjection ) { + const hasCustomProjection = !mapProjection.isNormalCylindrical; const count = borderPoints.length; for (let j = 0; j < count; ++j) { const borderPoint = borderPoints[j]; @@ -624,6 +687,13 @@ function addSkirt( if (geodeticSurfaceNormals.length > 0) { geodeticSurfaceNormals.push(geodeticSurfaceNormals[borderIndex]); } + if (hasCustomProjection) { + const pos2D = mapProjection.project( + scratchCartographic, + projectedCartesian3Scratch + ); + positions2D.push(new Cartesian2(pos2D.x, pos2D.y)); + } Matrix4.multiplyByPoint(skirtOptions.toENU, pos, scratchCartesian); diff --git a/Source/WorkersES6/createVerticesFromHeightmap.js b/Source/WorkersES6/createVerticesFromHeightmap.js index 597338502745..7946f167e6a5 100644 --- a/Source/WorkersES6/createVerticesFromHeightmap.js +++ b/Source/WorkersES6/createVerticesFromHeightmap.js @@ -1,3 +1,4 @@ +import deserializeMapProjection from "../Core/deserializeMapProjection.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import HeightmapEncoding from "../Core/HeightmapEncoding.js"; import HeightmapTessellator from "../Core/HeightmapTessellator.js"; @@ -29,25 +30,32 @@ function createVerticesFromHeightmap(parameters, transferableObjects) { parameters.ellipsoid = Ellipsoid.clone(parameters.ellipsoid); parameters.rectangle = Rectangle.clone(parameters.rectangle); - const statistics = HeightmapTessellator.computeVertices(parameters); - const vertices = statistics.vertices; - transferableObjects.push(vertices.buffer); + return deserializeMapProjection(parameters.serializedMapProjection).then( + function (mapProjection) { + const statistics = HeightmapTessellator.computeVertices( + parameters, + mapProjection + ); + const vertices = statistics.vertices; + transferableObjects.push(vertices.buffer); - return { - vertices: vertices.buffer, - numberOfAttributes: statistics.encoding.stride, - minimumHeight: statistics.minimumHeight, - maximumHeight: statistics.maximumHeight, - gridWidth: parameters.width, - gridHeight: parameters.height, - boundingSphere3D: statistics.boundingSphere3D, - orientedBoundingBox: statistics.orientedBoundingBox, - occludeePointInScaledSpace: statistics.occludeePointInScaledSpace, - encoding: statistics.encoding, - westIndicesSouthToNorth: statistics.westIndicesSouthToNorth, - southIndicesEastToWest: statistics.southIndicesEastToWest, - eastIndicesNorthToSouth: statistics.eastIndicesNorthToSouth, - northIndicesWestToEast: statistics.northIndicesWestToEast, - }; + return { + vertices: vertices.buffer, + numberOfAttributes: statistics.encoding.stride, + minimumHeight: statistics.minimumHeight, + maximumHeight: statistics.maximumHeight, + gridWidth: parameters.width, + gridHeight: parameters.height, + boundingSphere3D: statistics.boundingSphere3D, + orientedBoundingBox: statistics.orientedBoundingBox, + occludeePointInScaledSpace: statistics.occludeePointInScaledSpace, + encoding: statistics.encoding, + westIndicesSouthToNorth: statistics.westIndicesSouthToNorth, + southIndicesEastToWest: statistics.southIndicesEastToWest, + eastIndicesNorthToSouth: statistics.eastIndicesNorthToSouth, + northIndicesWestToEast: statistics.northIndicesWestToEast, + }; + } + ); } export default createTaskProcessorWorker(createVerticesFromHeightmap); diff --git a/Source/WorkersES6/createVerticesFromQuantizedTerrainMesh.js b/Source/WorkersES6/createVerticesFromQuantizedTerrainMesh.js index 743a62361ebd..db5f82bda720 100644 --- a/Source/WorkersES6/createVerticesFromQuantizedTerrainMesh.js +++ b/Source/WorkersES6/createVerticesFromQuantizedTerrainMesh.js @@ -3,6 +3,7 @@ import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartographic from "../Core/Cartographic.js"; import defined from "../Core/defined.js"; +import deserializeMapProjection from "../Core/deserializeMapProjection.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import EllipsoidalOccluder from "../Core/EllipsoidalOccluder.js"; import IndexDatatype from "../Core/IndexDatatype.js"; @@ -17,16 +18,26 @@ import createTaskProcessorWorker from "./createTaskProcessorWorker.js"; const maxShort = 32767; +function createVerticesFromQuantizedTerrainMesh( + parameters, + transferableObjects +) { + return deserializeMapProjection(parameters.serializedMapProjection).then( + function (mapProjection) { + return implementation(parameters, transferableObjects, mapProjection); + } + ); +} + const cartesian3Scratch = new Cartesian3(); const scratchMinimum = new Cartesian3(); const scratchMaximum = new Cartesian3(); const cartographicScratch = new Cartographic(); const toPack = new Cartesian2(); - -function createVerticesFromQuantizedTerrainMesh( - parameters, - transferableObjects -) { +const projectedCartesian3Scratch = new Cartesian3(); +const projectedCartographicScratch = new Cartographic(); +const relativeToCenter2dScratch = new Cartesian3(); +function implementation(parameters, transferableObjects, mapProjection) { const quantizedVertices = parameters.quantizedVertices; const quantizedVertexCount = quantizedVertices.length / 3; const octEncodedNormals = parameters.octEncodedNormals; @@ -56,6 +67,7 @@ function createVerticesFromQuantizedTerrainMesh( const center = parameters.relativeToCenter; const fromENU = Transforms.eastNorthUpToFixedFrame(center, ellipsoid); const toENU = Matrix4.inverseTransformation(fromENU, new Matrix4()); + const hasCustomProjection = !mapProjection.isNormalCylindrical; let southMercatorY; let oneOverMercatorHeight; @@ -89,6 +101,22 @@ function createVerticesFromQuantizedTerrainMesh( const geodeticSurfaceNormals = includeGeodeticSurfaceNormals ? new Array(quantizedVertexCount) : []; + let positions2D; + if (hasCustomProjection) { + positions2D = new Array(quantizedVertexCount); + } + + let relativeToCenter2D; + if (hasCustomProjection) { + const cartographicRTC = ellipsoid.cartesianToCartographic( + center, + projectedCartographicScratch + ); + relativeToCenter2D = mapProjection.project( + cartographicRTC, + relativeToCenter2dScratch + ); + } const minimum = scratchMinimum; minimum.x = Number.POSITIVE_INFINITY; @@ -131,6 +159,9 @@ function createVerticesFromQuantizedTerrainMesh( uvs[i] = new Cartesian2(u, v); heights[i] = height; positions[i] = position; + if (hasCustomProjection) { + positions2D[i] = mapProjection.project(cartographicScratch); + } if (includeWebMercatorT) { webMercatorTs[i] = @@ -256,7 +287,8 @@ function createVerticesFromQuantizedTerrainMesh( includeWebMercatorT, includeGeodeticSurfaceNormals, exaggeration, - exaggerationRelativeHeight + exaggerationRelativeHeight, + relativeToCenter2D ); const vertexStride = encoding.stride; const size = @@ -270,17 +302,30 @@ function createVerticesFromQuantizedTerrainMesh( toPack.x = octEncodedNormals[n]; toPack.y = octEncodedNormals[n + 1]; } - - bufferIndex = encoding.encode( - vertexBuffer, - bufferIndex, - positions[j], - uvs[j], - heights[j], - toPack, - webMercatorTs[j], - geodeticSurfaceNormals[j] - ); + if (hasCustomProjection) { + bufferIndex = encoding.encode( + vertexBuffer, + bufferIndex, + positions[j], + uvs[j], + heights[j], + toPack, + webMercatorTs[j], + geodeticSurfaceNormals[j], + positions2D[j] + ); + } else { + bufferIndex = encoding.encode( + vertexBuffer, + bufferIndex, + positions[j], + uvs[j], + heights[j], + toPack, + webMercatorTs[j], + geodeticSurfaceNormals[j] + ); + } } const edgeTriangleCount = Math.max(0, (edgeVertexCount - 4) * 2); @@ -319,7 +364,8 @@ function createVerticesFromQuantizedTerrainMesh( southMercatorY, oneOverMercatorHeight, westLongitudeOffset, - westLatitudeOffset + westLatitudeOffset, + mapProjection ); vertexBufferIndex += parameters.westIndices.length * vertexStride; addSkirt( @@ -336,7 +382,8 @@ function createVerticesFromQuantizedTerrainMesh( southMercatorY, oneOverMercatorHeight, southLongitudeOffset, - southLatitudeOffset + southLatitudeOffset, + mapProjection ); vertexBufferIndex += parameters.southIndices.length * vertexStride; addSkirt( @@ -353,7 +400,8 @@ function createVerticesFromQuantizedTerrainMesh( southMercatorY, oneOverMercatorHeight, eastLongitudeOffset, - eastLatitudeOffset + eastLatitudeOffset, + mapProjection ); vertexBufferIndex += parameters.eastIndices.length * vertexStride; addSkirt( @@ -370,7 +418,8 @@ function createVerticesFromQuantizedTerrainMesh( southMercatorY, oneOverMercatorHeight, northLongitudeOffset, - northLatitudeOffset + northLatitudeOffset, + mapProjection ); TerrainProvider.addSkirtIndices( @@ -448,6 +497,7 @@ function findMinMaxSkirts( return hMin; } +const skirtCartesian2Scratch = new Cartesian2(); function addSkirt( vertexBuffer, vertexBufferIndex, @@ -462,7 +512,8 @@ function addSkirt( southMercatorY, oneOverMercatorHeight, longitudeOffset, - latitudeOffset + latitudeOffset, + mapProjection ) { const hasVertexNormals = defined(octEncodedNormals); @@ -475,6 +526,7 @@ function addSkirt( east += CesiumMath.TWO_PI; } + const hasCustomProjection = !mapProjection.isNormalCylindrical; const length = edgeVertices.length; for (let i = 0; i < length; ++i) { const index = edgeVertices[i]; @@ -492,6 +544,21 @@ function addSkirt( cartesian3Scratch ); + let position2D; + if (hasCustomProjection) { + position2D = skirtCartesian2Scratch; + const heightlessCartographic = projectedCartographicScratch; + heightlessCartographic.longitude = cartographicScratch.longitude; + heightlessCartographic.latitude = cartographicScratch.latitude; + heightlessCartographic.height = 0.0; + const projectedPosition = mapProjection.project( + heightlessCartographic, + projectedCartesian3Scratch + ); + position2D.x = projectedPosition.x; + position2D.y = projectedPosition.y; + } + if (hasVertexNormals) { const n = index * 2.0; toPack.x = octEncodedNormals[n]; @@ -521,7 +588,8 @@ function addSkirt( cartographicScratch.height, toPack, webMercatorT, - geodeticSurfaceNormal + geodeticSurfaceNormal, + position2D ); } } diff --git a/Specs/Core/BoundingSphereSpec.js b/Specs/Core/BoundingSphereSpec.js index 826f3b42516c..73a0927f735f 100644 --- a/Specs/Core/BoundingSphereSpec.js +++ b/Specs/Core/BoundingSphereSpec.js @@ -10,6 +10,7 @@ import { Math as CesiumMath } from "../../Source/Cesium.js"; import { Matrix4 } from "../../Source/Cesium.js"; import { OrientedBoundingBox } from "../../Source/Cesium.js"; import { Plane } from "../../Source/Cesium.js"; +import { Proj4Projection } from "../../Source/Cesium.js"; import { Quaternion } from "../../Source/Cesium.js"; import { Rectangle } from "../../Source/Cesium.js"; import createPackableSpecs from "../createPackableSpecs.js"; @@ -1032,18 +1033,13 @@ describe("Core/BoundingSphere", function () { expect(distanceFromCenter).toBeLessThanOrEqual(boundingSphere.radius); } - it("fromRectangleWithHeights2D includes specified min and max heights", function () { - const rectangle = new Rectangle(0.1, 0.5, 0.2, 0.6); - const projection = new GeographicProjection(); - const minHeight = -327.0; - const maxHeight = 2456.0; - const boundingSphere = BoundingSphere.fromRectangleWithHeights2D( - rectangle, - projection, - minHeight, - maxHeight - ); - + function checkPerimeterAndCenter( + rectangle, + boundingSphere, + projection, + minHeight, + maxHeight + ) { // Test that the corners are inside the bounding sphere. let point = Rectangle.southwest(rectangle).clone(); point.height = minHeight; @@ -1142,6 +1138,51 @@ describe("Core/BoundingSphere", function () { maxHeight ); expectBoundingSphereToContainPoint(boundingSphere, point, projection); + } + + it("fromRectangleWithHeights2D includes specified min and max heights", function () { + const rectangle = new Rectangle(0.1, 0.5, 0.2, 0.6); + const projection = new GeographicProjection(); + const minHeight = -327.0; + const maxHeight = 2456.0; + const boundingSphere = BoundingSphere.fromRectangleWithHeights2D( + rectangle, + projection, + minHeight, + maxHeight + ); + + checkPerimeterAndCenter( + rectangle, + boundingSphere, + projection, + minHeight, + maxHeight + ); + }); + + it("fromRectangleWithHeights2D includes specified min and max heights when using non cylindrical, non-equatorial projections", function () { + const rectangle = Rectangle.MAX_VALUE; + const projection = new Proj4Projection({ + wellKnownText: + "+proj=moll +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs", + }); + const minHeight = -327.0; + const maxHeight = 2456.0; + const boundingSphere = BoundingSphere.fromRectangleWithHeights2D( + rectangle, + projection, + minHeight, + maxHeight + ); + + checkPerimeterAndCenter( + rectangle, + boundingSphere, + projection, + minHeight, + maxHeight + ); }); it("computes the volume of a BoundingSphere", function () { diff --git a/Specs/Core/GeographicProjectionSpec.js b/Specs/Core/GeographicProjectionSpec.js index cd3f14c40611..a51ff2199c5f 100644 --- a/Specs/Core/GeographicProjectionSpec.js +++ b/Specs/Core/GeographicProjectionSpec.js @@ -95,4 +95,17 @@ describe("Core/GeographicProjection", function () { return projection.unproject(); }).toThrowDeveloperError(); }); + + it("serializes and deserializes", function () { + const projection = new GeographicProjection(Ellipsoid.UNIT_SPHERE); + const serialized = projection.serialize(); + + return GeographicProjection.deserialize(serialized).then(function ( + deserializedProjection + ) { + expect( + projection.ellipsoid.equals(deserializedProjection.ellipsoid) + ).toBe(true); + }); + }); }); diff --git a/Specs/Core/GoogleEarthEnterpriseTerrainDataSpec.js b/Specs/Core/GoogleEarthEnterpriseTerrainDataSpec.js index 7d79b735f5a6..5b7446c96e53 100644 --- a/Specs/Core/GoogleEarthEnterpriseTerrainDataSpec.js +++ b/Specs/Core/GoogleEarthEnterpriseTerrainDataSpec.js @@ -1,6 +1,7 @@ import { Cartesian3 } from "../../Source/Cesium.js"; import { Cartographic } from "../../Source/Cesium.js"; import { Ellipsoid } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { GeographicTilingScheme } from "../../Source/Cesium.js"; import { GoogleEarthEnterpriseTerrainData } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; @@ -145,12 +146,16 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { tilingScheme.tileXYToRectangle(1, 1, 1), ]; + const geographicProjection = new GeographicProjection(); + const serializedMapProjection = geographicProjection.serialize(); + return Promise.resolve( data.createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, }) ) .then(function () { @@ -239,6 +244,8 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { let data; let tilingScheme; let buffer; + const geographicProjection = new GeographicProjection(); + const serializedMapProjection = geographicProjection.serialize(); beforeEach(function () { tilingScheme = new GeographicTilingScheme(); @@ -258,6 +265,7 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { x: 0, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -269,6 +277,7 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { x: undefined, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -280,6 +289,7 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { x: 0, y: undefined, level: 0, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -291,6 +301,19 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { x: 0, y: 0, level: undefined, + serializedMapProjection: serializedMapProjection, + }); + }).toThrowDeveloperError(); + }); + + it("requires serializedMapProjection", function () { + expect(function () { + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: undefined, }); }).toThrowDeveloperError(); }); @@ -305,6 +328,7 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { x: 0, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, }) .then(function (mesh) { expect(mesh).toBeInstanceOf(TerrainMesh); @@ -346,6 +370,7 @@ describe("Core/GoogleEarthEnterpriseTerrainData", function () { y: 0, level: 0, exaggeration: 2, + serializedMapProjection: serializedMapProjection, }) .then(function (mesh) { expect(mesh).toBeInstanceOf(TerrainMesh); diff --git a/Specs/Core/GroundPolylineGeometrySpec.js b/Specs/Core/GroundPolylineGeometrySpec.js index 6068d53b2dd8..c71988a8170d 100644 --- a/Specs/Core/GroundPolylineGeometrySpec.js +++ b/Specs/Core/GroundPolylineGeometrySpec.js @@ -705,9 +705,9 @@ describe("Core/GroundPolylineGeometry", function () { granularity: 10.0, // no interpolative subdivision }); groundPolylineGeometry._scene3DOnly = true; - GroundPolylineGeometry.setProjectionAndEllipsoid( + GroundPolylineGeometry.setProjection( groundPolylineGeometry, - new WebMercatorProjection(Ellipsoid.UNIT_SPHERE) + new WebMercatorProjection(Ellipsoid.WGS84) ); const packedArray = [0]; @@ -733,9 +733,7 @@ describe("Core/GroundPolylineGeometry", function () { ).toBe(true); expect(scratch.loop).toBe(true); expect(scratch.granularity).toEqual(10.0); - expect(scratch._ellipsoid.equals(Ellipsoid.UNIT_SPHERE)).toBe(true); expect(scratch._scene3DOnly).toBe(true); - expect(scratch._projectionIndex).toEqual(1); }); it("can unpack onto a new instance", function () { @@ -745,7 +743,7 @@ describe("Core/GroundPolylineGeometry", function () { granularity: 10.0, // no interpolative subdivision }); groundPolylineGeometry._scene3DOnly = true; - GroundPolylineGeometry.setProjectionAndEllipsoid( + GroundPolylineGeometry.setProjection( groundPolylineGeometry, new WebMercatorProjection(Ellipsoid.UNIT_SPHERE) ); @@ -770,27 +768,20 @@ describe("Core/GroundPolylineGeometry", function () { ).toBe(true); expect(result.loop).toBe(true); expect(result.granularity).toEqual(10.0); - expect(result._ellipsoid.equals(Ellipsoid.UNIT_SPHERE)).toBe(true); expect(result._scene3DOnly).toBe(true); - expect(result._projectionIndex).toEqual(1); }); - it("provides a method for setting projection and ellipsoid", function () { + it("provides a method for setting projection", function () { const groundPolylineGeometry = new GroundPolylineGeometry({ positions: Cartesian3.fromDegreesArray([-1.0, 0.0, 1.0, 0.0]), loop: true, granularity: 10.0, // no interpolative subdivision }); - GroundPolylineGeometry.setProjectionAndEllipsoid( - groundPolylineGeometry, - new WebMercatorProjection(Ellipsoid.UNIT_SPHERE) - ); + const projection = new WebMercatorProjection(Ellipsoid.UNIT_SPHERE); + GroundPolylineGeometry.setProjection(groundPolylineGeometry, projection); - expect(groundPolylineGeometry._projectionIndex).toEqual(1); - expect( - groundPolylineGeometry._ellipsoid.equals(Ellipsoid.UNIT_SPHERE) - ).toBe(true); + expect(groundPolylineGeometry._projection).toEqual(projection); }); const positions = Cartesian3.fromDegreesArray([ @@ -868,10 +859,6 @@ describe("Core/GroundPolylineGeometry", function () { packedInstance.push(polyline.granularity); packedInstance.push(polyline.loop ? 1.0 : 0.0); packedInstance.push(polyline.arcType); - - Ellipsoid.pack(Ellipsoid.WGS84, packedInstance, packedInstance.length); - - packedInstance.push(0.0); // projection index for Geographic (default) packedInstance.push(0.0); // scene3DModeOnly = false createPackableSpecs(GroundPolylineGeometry, polyline, packedInstance); diff --git a/Specs/Core/HeightmapTerrainDataSpec.js b/Specs/Core/HeightmapTerrainDataSpec.js index 1d58e3469b3d..e55d845550a3 100644 --- a/Specs/Core/HeightmapTerrainDataSpec.js +++ b/Specs/Core/HeightmapTerrainDataSpec.js @@ -1,4 +1,5 @@ import { defined } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { GeographicTilingScheme } from "../../Source/Cesium.js"; import { HeightmapEncoding } from "../../Source/Cesium.js"; import { HeightmapTerrainData } from "../../Source/Cesium.js"; @@ -68,6 +69,8 @@ describe("Core/HeightmapTerrainData", function () { describe("createMesh", function () { let data; let tilingScheme; + const geographicProjection = new GeographicProjection(); + const serializedMapProjection = geographicProjection.serialize(); function createSampleTerrainData() { return new HeightmapTerrainData({ @@ -84,7 +87,13 @@ describe("Core/HeightmapTerrainData", function () { it("requires tilingScheme", function () { expect(function () { - data.createMesh({ tilingScheme: undefined, x: 0, y: 0, level: 0 }); + data.createMesh({ + tilingScheme: undefined, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }); }).toThrowDeveloperError(); }); @@ -95,6 +104,7 @@ describe("Core/HeightmapTerrainData", function () { x: undefined, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -106,6 +116,7 @@ describe("Core/HeightmapTerrainData", function () { x: 0, y: undefined, level: 0, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -117,6 +128,31 @@ describe("Core/HeightmapTerrainData", function () { x: 0, y: 0, level: undefined, + serializedMapProjection: serializedMapProjection, + }); + }).toThrowDeveloperError(); + }); + + it("requires serializedMapProjection", function () { + expect(function () { + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: undefined, + }); + }).toThrowDeveloperError(); + }); + + it("requires level", function () { + expect(function () { + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: undefined, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -128,6 +164,7 @@ describe("Core/HeightmapTerrainData", function () { y: 0, level: 0, throttle: true, + serializedMapProjection: serializedMapProjection, }; const taskCount = TerrainData.maximumAsynchronousTasks + 1; const promises = new Array(); @@ -149,6 +186,7 @@ describe("Core/HeightmapTerrainData", function () { y: 0, level: 0, throttle: false, + serializedMapProjection: serializedMapProjection, }; const taskCount = TerrainData.maximumAsynchronousTasks + 1; const promises = new Array(); @@ -168,6 +206,9 @@ describe("Core/HeightmapTerrainData", function () { let data; let tilingScheme; + const geographicProjection = new GeographicProjection(); + const serializedMapProjection = geographicProjection.serialize(); + beforeEach(function () { tilingScheme = new GeographicTilingScheme(); data = new HeightmapTerrainData({ @@ -250,7 +291,13 @@ describe("Core/HeightmapTerrainData", function () { }); return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function () { return data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); }) @@ -340,7 +387,13 @@ describe("Core/HeightmapTerrainData", function () { }); return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function () { return data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); }) @@ -463,7 +516,13 @@ describe("Core/HeightmapTerrainData", function () { }); return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function () { return data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); }) @@ -549,7 +608,13 @@ describe("Core/HeightmapTerrainData", function () { }); return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function () { return data.upsample(tilingScheme, 0, 0, 0, 1, 0, 1); }) @@ -639,7 +704,13 @@ describe("Core/HeightmapTerrainData", function () { }); return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function () { return data.upsample(tilingScheme, 0, 0, 0, 1, 0, 1); }) @@ -731,7 +802,13 @@ describe("Core/HeightmapTerrainData", function () { }); return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function () { return data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); }) diff --git a/Specs/Core/QuantizedMeshTerrainDataSpec.js b/Specs/Core/QuantizedMeshTerrainDataSpec.js index 6d614a78862b..4e44e538d84f 100644 --- a/Specs/Core/QuantizedMeshTerrainDataSpec.js +++ b/Specs/Core/QuantizedMeshTerrainDataSpec.js @@ -1,6 +1,7 @@ import { BoundingSphere } from "../../Source/Cesium.js"; import { Cartesian3 } from "../../Source/Cesium.js"; import { defined } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { GeographicTilingScheme } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; import { QuantizedMeshTerrainData } from "../../Source/Cesium.js"; @@ -13,6 +14,9 @@ describe("Core/QuantizedMeshTerrainData", function () { }); describe("upsample", function () { + const geographicProjection = new GeographicProjection(); + const serializedMapProjection = geographicProjection.serialize(); + function findVertexWithCoordinates(uBuffer, vBuffer, u, v) { u *= 32767; u |= 0; @@ -101,7 +105,13 @@ describe("Core/QuantizedMeshTerrainData", function () { const tilingScheme = new GeographicTilingScheme(); return Promise.resolve( - data.createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) ) .then(function () { const swPromise = data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); @@ -197,7 +207,13 @@ describe("Core/QuantizedMeshTerrainData", function () { const tilingScheme = new GeographicTilingScheme(); return Promise.resolve( - data.createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) ) .then(function () { const swPromise = data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); @@ -266,7 +282,13 @@ describe("Core/QuantizedMeshTerrainData", function () { const tilingScheme = new GeographicTilingScheme(); return Promise.resolve( - data.createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) ) .then(function () { return data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); @@ -377,7 +399,13 @@ describe("Core/QuantizedMeshTerrainData", function () { const tilingScheme = new GeographicTilingScheme(); return Promise.resolve( - data.createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) ) .then(function () { const nwPromise = data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1); @@ -450,6 +478,8 @@ describe("Core/QuantizedMeshTerrainData", function () { describe("createMesh", function () { let data; let tilingScheme; + const geographicProjection = new GeographicProjection(); + const serializedMapProjection = geographicProjection.serialize(); function createSampleTerrainData() { return new QuantizedMeshTerrainData({ @@ -494,7 +524,13 @@ describe("Core/QuantizedMeshTerrainData", function () { it("requires tilingScheme", function () { expect(function () { - data.createMesh({ tilingScheme: undefined, x: 0, y: 0, level: 0 }); + data.createMesh({ + tilingScheme: undefined, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }); }).toThrowDeveloperError(); }); @@ -505,6 +541,7 @@ describe("Core/QuantizedMeshTerrainData", function () { x: undefined, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -516,6 +553,7 @@ describe("Core/QuantizedMeshTerrainData", function () { x: 0, y: undefined, level: 0, + serializedMapProjection: serializedMapProjection, }); }).toThrowDeveloperError(); }); @@ -526,14 +564,33 @@ describe("Core/QuantizedMeshTerrainData", function () { tilingScheme: tilingScheme, x: 0, y: 0, + serializedMapProjection: serializedMapProjection, level: undefined, }); }).toThrowDeveloperError(); }); + it("requires serializedMapProjection", function () { + expect(function () { + data.createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + serializedMapProjection: undefined, + level: 0, + }); + }).toThrowDeveloperError(); + }); + it("creates specified vertices plus skirt vertices", function () { return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function (mesh) { expect(mesh).toBeInstanceOf(TerrainMesh); expect(mesh.vertices.length).toBe(12 * mesh.encoding.stride); // 4 regular vertices, 8 skirt vertices. @@ -552,6 +609,7 @@ describe("Core/QuantizedMeshTerrainData", function () { y: 0, level: 0, exaggeration: 2, + serializedMapProjection: serializedMapProjection, }) .then(function (mesh) { expect(mesh).toBeInstanceOf(TerrainMesh); @@ -602,7 +660,13 @@ describe("Core/QuantizedMeshTerrainData", function () { }); return data - .createMesh({ tilingScheme: tilingScheme, x: 0, y: 0, level: 0 }) + .createMesh({ + tilingScheme: tilingScheme, + x: 0, + y: 0, + level: 0, + serializedMapProjection: serializedMapProjection, + }) .then(function (mesh) { expect(mesh).toBeInstanceOf(TerrainMesh); expect(mesh.indices.BYTES_PER_ELEMENT).toBe(4); @@ -615,6 +679,7 @@ describe("Core/QuantizedMeshTerrainData", function () { x: 0, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, throttle: true, }; const taskCount = TerrainData.maximumAsynchronousTasks + 1; @@ -636,6 +701,7 @@ describe("Core/QuantizedMeshTerrainData", function () { x: 0, y: 0, level: 0, + serializedMapProjection: serializedMapProjection, throttle: false, }; const taskCount = TerrainData.maximumAsynchronousTasks + 1; diff --git a/Specs/Core/RectangleCollisionCheckerSpec.js b/Specs/Core/RectangleCollisionCheckerSpec.js index 0195a092fcc8..8b9c576be0f1 100644 --- a/Specs/Core/RectangleCollisionCheckerSpec.js +++ b/Specs/Core/RectangleCollisionCheckerSpec.js @@ -1,3 +1,4 @@ +import { GeographicProjection } from "../../Source/Cesium.js"; import { Rectangle } from "../../Source/Cesium.js"; import { RectangleCollisionChecker } from "../../Source/Cesium.js"; @@ -7,7 +8,9 @@ describe("Core/RectangleCollisionChecker", function () { const testRectangle3 = new Rectangle(1.1, 1.1, 1.2, 1.2); it("Checks for collisions with contained rectangles", function () { - const collisionChecker = new RectangleCollisionChecker(); + const collisionChecker = new RectangleCollisionChecker( + new GeographicProjection() + ); collisionChecker.insert("test1", testRectangle1); expect(collisionChecker.collides(testRectangle2)).toBe(false); @@ -17,7 +20,9 @@ describe("Core/RectangleCollisionChecker", function () { }); it("removes rectangles", function () { - const collisionChecker = new RectangleCollisionChecker(); + const collisionChecker = new RectangleCollisionChecker( + new GeographicProjection() + ); collisionChecker.insert("test1", testRectangle1); collisionChecker.insert("test3", testRectangle3); diff --git a/Specs/Core/RectangleSpec.js b/Specs/Core/RectangleSpec.js index 3eee2733dfd3..15f268c9dec0 100644 --- a/Specs/Core/RectangleSpec.js +++ b/Specs/Core/RectangleSpec.js @@ -1,7 +1,9 @@ import { Cartesian3 } from "../../Source/Cesium.js"; import { Cartographic } from "../../Source/Cesium.js"; import { Ellipsoid } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; +import { Proj4Projection } from "../../Source/Cesium.js"; import { Rectangle } from "../../Source/Cesium.js"; import createPackableSpecs from "../createPackableSpecs.js"; @@ -11,6 +13,26 @@ describe("Core/Rectangle", function () { const east = 1.4; const north = 1.0; const center = new Cartographic((west + east) / 2.0, (south + north) / 2.0); + let arcticProjection; + let antarcticProjection; + + beforeAll(function () { + const epsg3411bounds = Rectangle.fromDegrees(-180.0, 30.0, 180.0, 90.0); + const epsg3411wkt = + "+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 +a=6378273 +b=6356889.449 +units=m +no_defs"; + arcticProjection = new Proj4Projection({ + wellKnownText: epsg3411wkt, + wgs84Bounds: epsg3411bounds, + }); + + const epsg3031Bounds = Rectangle.fromDegrees(-180.0, -90.0, 180.0, -60.0); + const epsg3031wkt = + "+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"; + antarcticProjection = new Proj4Projection({ + wellKnownText: epsg3031wkt, + wgs84Bounds: epsg3031Bounds, + }); + }); it("default constructor sets expected values.", function () { const rectangle = new Rectangle(); @@ -1442,6 +1464,109 @@ describe("Core/Rectangle", function () { }).toThrowDeveloperError(); }); + it("approximates the projected extents of a cartographic Rectangle", function () { + const projection = new GeographicProjection(); + const cartographicRectangle = Rectangle.fromDegrees(-90, -45, 90, 45); + const projectedRectangle = Rectangle.approximateProjectedExtents({ + cartographicRectangle: cartographicRectangle, + mapProjection: projection, + }); + expect(projectedRectangle.west).toEqualEpsilon( + -CesiumMath.PI_OVER_TWO * Ellipsoid.WGS84.maximumRadius, + CesiumMath.EPSILON7 + ); + expect(projectedRectangle.east).toEqualEpsilon( + CesiumMath.PI_OVER_TWO * Ellipsoid.WGS84.maximumRadius, + CesiumMath.EPSILON7 + ); + expect(projectedRectangle.south).toEqualEpsilon( + -CesiumMath.PI_OVER_FOUR * Ellipsoid.WGS84.maximumRadius, + CesiumMath.EPSILON7 + ); + expect(projectedRectangle.north).toEqualEpsilon( + CesiumMath.PI_OVER_FOUR * Ellipsoid.WGS84.maximumRadius, + CesiumMath.EPSILON7 + ); + }); + + it("approximates the cartographic extents of a projected Rectangle", function () { + const projection = new GeographicProjection(); + + const west = -CesiumMath.PI_OVER_TWO * Ellipsoid.WGS84.maximumRadius; + const east = CesiumMath.PI_OVER_TWO * Ellipsoid.WGS84.maximumRadius; + const south = -CesiumMath.PI_OVER_FOUR * Ellipsoid.WGS84.maximumRadius; + const north = CesiumMath.PI_OVER_FOUR * Ellipsoid.WGS84.maximumRadius; + + const projectedRectangle = new Rectangle(west, south, east, north); + const cartographicRectangle = Rectangle.approximateCartographicExtents({ + projectedRectangle: projectedRectangle, + mapProjection: projection, + }); + expect(cartographicRectangle.west).toEqualEpsilon( + -CesiumMath.PI_OVER_TWO, + CesiumMath.EPSILON7 + ); + expect(cartographicRectangle.east).toEqualEpsilon( + CesiumMath.PI_OVER_TWO, + CesiumMath.EPSILON7 + ); + expect(cartographicRectangle.south).toEqualEpsilon( + -CesiumMath.PI_OVER_FOUR, + CesiumMath.EPSILON7 + ); + expect(cartographicRectangle.north).toEqualEpsilon( + CesiumMath.PI_OVER_FOUR, + CesiumMath.EPSILON7 + ); + }); + + it("detects poles when approximating cartographic extents of projected Rectangles", function () { + // Rectangle around North pole in EPSG:3411 and around South pole in EPSG:3031 + const west = -300000; + const east = 300000; + const south = -300000; + const north = 300000; + const projectedRectangle = new Rectangle(west, south, east, north); + + const cartographicRectangleArctic = Rectangle.approximateCartographicExtents( + { + projectedRectangle: projectedRectangle, + mapProjection: arcticProjection, + } + ); + expect(cartographicRectangleArctic.north).toEqual(CesiumMath.PI_OVER_TWO); + expect(cartographicRectangleArctic.east).toEqual(CesiumMath.PI); + expect(cartographicRectangleArctic.west).toEqual(-CesiumMath.PI); + + const cartographicRectangleAntarctic = Rectangle.approximateCartographicExtents( + { + projectedRectangle: projectedRectangle, + mapProjection: antarcticProjection, + } + ); + expect(cartographicRectangleAntarctic.south).toEqual( + -CesiumMath.PI_OVER_TWO + ); + expect(cartographicRectangleAntarctic.east).toEqual(CesiumMath.PI); + expect(cartographicRectangleAntarctic.west).toEqual(-CesiumMath.PI); + }); + + it("detects rectangles crossing the IDL when approximating cartographic extents of projected Rectangles", function () { + // Rectangle in EPSG:3411 that crosses the IDL + const west = -1300000; + const east = -700000; + const south = 700000; + const north = 1300000; + const projectedRectangle = new Rectangle(west, south, east, north); + + const cartographicRectangle = Rectangle.approximateCartographicExtents({ + projectedRectangle: projectedRectangle, + mapProjection: arcticProjection, + }); + expect(cartographicRectangle.west > cartographicRectangle.east).toBe(true); + expect(cartographicRectangle.width < CesiumMath.PI).toBe(true); + }); + const rectangle = new Rectangle(west, south, east, north); const packedInstance = [west, south, east, north]; createPackableSpecs(Rectangle, rectangle, packedInstance); diff --git a/Specs/Core/TerrainEncodingSpec.js b/Specs/Core/TerrainEncodingSpec.js index 153b716177c6..bff559c8c70a 100644 --- a/Specs/Core/TerrainEncodingSpec.js +++ b/Specs/Core/TerrainEncodingSpec.js @@ -378,6 +378,41 @@ describe("Core/TerrainEncoding", function () { 200.0 / 4095.0 ); }); + it("encodes with 2D positions", function () { + const center2D = new Cartesian3(center.x, center.y, 0.0); + const encoding = new TerrainEncoding( + aabox, + minimumHeight, + maximumHeight, + fromENU, + false, + false, + center2D + ); + const position2D = new Cartesian3(center.x, center.y, 1.0); + + const buffer = []; + const height = (maximumHeight + minimumHeight) * 0.5; + encoding.encode( + buffer, + 0, + center, + Cartesian2.ZERO, + height, + Cartesian3.UNIT_X, + 0.0, + position2D + ); + + expect(encoding.getStride()).toEqual(6); + expect(buffer.length).toEqual(encoding.getStride()); + + expect(buffer[3]).toEqual(0.0); + expect(buffer[4]).toEqual(0.0); + expect(buffer[5]).toEqual(1.0); + + expect(encoding.decodePosition2D(buffer, 0)).toEqual(position2D); + }); it("gets oct-encoded normal", function () { const hasVertexNormals = true; @@ -475,6 +510,40 @@ describe("Core/TerrainEncoding", function () { expect(newBuffer.length).toEqual(newStride); }); + it("gets oct-encoded normal when encoding includes 2D positions", function () { + const hasVertexNormals = true; + const encoding = new TerrainEncoding( + aabox, + minimumHeight, + maximumHeight, + fromENU, + hasVertexNormals, + false, + new Cartesian3(0.0, 0.0, 0.0) + ); + + const normal = new Cartesian3(1.0, 1.0, 1.0); + Cartesian3.normalize(normal, normal); + const octNormal = AttributeCompression.octEncode(normal, new Cartesian2()); + + const buffer = []; + encoding.encode( + buffer, + 0, + center, + Cartesian2.ZERO, + minimumHeight, + octNormal, + undefined, + new Cartesian3(1.0, 1.0, 1.0) + ); + + expect(encoding.getStride()).toEqual(7); + expect(buffer.length).toEqual(encoding.getStride()); + + expect(encoding.getOctEncodedNormal(buffer, 0)).toEqual(octNormal); + }); + it("gets attributes", function () { const center = Cartesian3.fromDegrees(0.0, 0.0); const maximum = new Cartesian3(1.0e6, 1.0e6, 1.0e6); @@ -530,6 +599,144 @@ describe("Core/TerrainEncoding", function () { expect(attributeLocations).toBeDefined(); }); + it("gets attributes with 2D positions", function () { + // without quantization + const center = Cartesian3.fromDegrees(0.0, 0.0); + const center2D = new Cartesian3(center.x, center.y, 0.0); + let maximum = new Cartesian3(1.0e6, 1.0e6, 1.0e6); + let minimum = Cartesian3.negate(maximum, new Cartesian3()); + let aabox = new AxisAlignedBoundingBox(minimum, maximum, center); + + let maximumHeight = 1.0e6; + let minimumHeight = maximumHeight; + + const fromENU = Transforms.eastNorthUpToFixedFrame(center); + + let hasVertexNormals = false; + let hasWebMercatorT = false; + + let encoding = new TerrainEncoding( + aabox, + minimumHeight, + maximumHeight, + fromENU, + hasVertexNormals, + hasWebMercatorT, + center2D + ); + + let buffer = []; + let attributes = encoding.getAttributes(buffer); + + expect(attributes).toBeDefined(); + expect(attributes.length).toEqual(3); + + // with quantization, with webMercatorT and vertex normals + maximum = new Cartesian3(1.0e2, 1.0e2, 1.0e2); + minimum = Cartesian3.negate(maximum, new Cartesian3()); + aabox = new AxisAlignedBoundingBox(minimum, maximum, center); + + maximumHeight = 1.0e2; + minimumHeight = maximumHeight; + + hasVertexNormals = true; + hasWebMercatorT = true; + + encoding = new TerrainEncoding( + aabox, + minimumHeight, + maximumHeight, + fromENU, + hasVertexNormals, + hasWebMercatorT, + center2D + ); + + buffer = []; + attributes = encoding.getAttributes(buffer); + + expect(attributes).toBeDefined(); + expect(attributes.length).toEqual(3); + + // with quantization, without webMercatorT and vertex normals + maximum = new Cartesian3(1.0e2, 1.0e2, 1.0e2); + minimum = Cartesian3.negate(maximum, new Cartesian3()); + aabox = new AxisAlignedBoundingBox(minimum, maximum, center); + + maximumHeight = 1.0e2; + minimumHeight = maximumHeight; + + hasVertexNormals = false; + hasWebMercatorT = false; + + encoding = new TerrainEncoding( + aabox, + minimumHeight, + maximumHeight, + fromENU, + hasVertexNormals, + hasWebMercatorT, + center2D + ); + + buffer = []; + attributes = encoding.getAttributes(buffer); + + expect(attributes).toBeDefined(); + expect(attributes.length).toEqual(2); + }); + + it("gets attribute locations with 2D positions", function () { + const center = Cartesian3.fromDegrees(0.0, 0.0); + const center2D = new Cartesian3(center.x, center.y); + + let maximum = new Cartesian3(1.0e6, 1.0e6, 1.0e6); + let minimum = Cartesian3.negate(maximum, new Cartesian3()); + let aabox = new AxisAlignedBoundingBox(minimum, maximum, center); + + let maximumHeight = 1.0e6; + let minimumHeight = maximumHeight; + + const fromENU = Transforms.eastNorthUpToFixedFrame(center); + + const hasVertexNormals = false; + + let encoding = new TerrainEncoding( + aabox, + minimumHeight, + maximumHeight, + fromENU, + hasVertexNormals, + false, + center2D + ); + const attributeLocations = encoding.getAttributeLocations(); + + expect(attributeLocations).toBeDefined(); + + // with quantization + maximum = new Cartesian3(1.0e2, 1.0e2, 1.0e2); + minimum = Cartesian3.negate(maximum, new Cartesian3()); + aabox = new AxisAlignedBoundingBox(minimum, maximum, center); + + maximumHeight = 1.0e2; + minimumHeight = maximumHeight; + + encoding = new TerrainEncoding( + aabox, + minimumHeight, + maximumHeight, + fromENU, + hasVertexNormals, + false, + center2D + ); + + const quantizedAttributeLocations = encoding.getAttributeLocations(); + + expect(quantizedAttributeLocations).toBeDefined(); + }); + it("clones", function () { const center = Cartesian3.fromDegrees(0.0, 0.0); const maximum = new Cartesian3(1.0e6, 1.0e6, 1.0e6); diff --git a/Specs/Core/WebMercatorProjectionSpec.js b/Specs/Core/WebMercatorProjectionSpec.js index 8750e6866535..96ac09f0a590 100644 --- a/Specs/Core/WebMercatorProjectionSpec.js +++ b/Specs/Core/WebMercatorProjectionSpec.js @@ -220,4 +220,17 @@ describe("Core/WebMercatorProjection", function () { return projection.unproject(); }).toThrowDeveloperError(); }); + + it("serializes and deserializes", function () { + const projection = new WebMercatorProjection(Ellipsoid.UNIT_SPHERE); + const serialized = projection.serialize(); + + return WebMercatorProjection.deserialize(serialized).then(function ( + deserializedProjection + ) { + expect( + projection.ellipsoid.equals(deserializedProjection.ellipsoid) + ).toBe(true); + }); + }); }); diff --git a/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js b/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js index da18cb025d4f..aa1e7fe42b63 100644 --- a/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js +++ b/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js @@ -2,6 +2,7 @@ import { ApproximateTerrainHeights } from "../../Source/Cesium.js"; import { Cartesian3 } from "../../Source/Cesium.js"; import { Color } from "../../Source/Cesium.js"; import { DistanceDisplayCondition } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { JulianDate } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; import { TimeInterval } from "../../Source/Cesium.js"; @@ -42,7 +43,8 @@ describe("DataSources/StaticGroundGeometryColorBatch", function () { const batch = new StaticGroundGeometryColorBatch( scene.groundPrimitives, - ClassificationType.BOTH + ClassificationType.BOTH, + new GeographicProjection() ); const entity = new Entity({ position: new Cartesian3(1234, 5678, 9101112), @@ -105,7 +107,8 @@ describe("DataSources/StaticGroundGeometryColorBatch", function () { const batch = new StaticGroundGeometryColorBatch( scene.groundPrimitives, - ClassificationType.BOTH + ClassificationType.BOTH, + new GeographicProjection() ); const entity = new Entity({ position: new Cartesian3(1234, 5678, 9101112), @@ -167,7 +170,8 @@ describe("DataSources/StaticGroundGeometryColorBatch", function () { const batch = new StaticGroundGeometryColorBatch( scene.groundPrimitives, - ClassificationType.BOTH + ClassificationType.BOTH, + new GeographicProjection() ); const updater = new EllipseGeometryUpdater(entity, scene); @@ -225,7 +229,8 @@ describe("DataSources/StaticGroundGeometryColorBatch", function () { const batch = new StaticGroundGeometryColorBatch( scene.groundPrimitives, - ClassificationType.BOTH + ClassificationType.BOTH, + new GeographicProjection() ); const updater = new EllipseGeometryUpdater(entity, scene); @@ -260,7 +265,8 @@ describe("DataSources/StaticGroundGeometryColorBatch", function () { const batch = new StaticGroundGeometryColorBatch( scene.groundPrimitives, - ClassificationType.BOTH + ClassificationType.BOTH, + new GeographicProjection() ); function renderScene() { @@ -335,7 +341,8 @@ describe("DataSources/StaticGroundGeometryColorBatch", function () { const batch = new StaticGroundGeometryColorBatch( scene.groundPrimitives, - ClassificationType.BOTH + ClassificationType.BOTH, + new GeographicProjection() ); function renderScene() { diff --git a/Specs/DataSources/StaticGroundGeometryPerMaterialBatchSpec.js b/Specs/DataSources/StaticGroundGeometryPerMaterialBatchSpec.js index 909f3321a240..fca62940f505 100644 --- a/Specs/DataSources/StaticGroundGeometryPerMaterialBatchSpec.js +++ b/Specs/DataSources/StaticGroundGeometryPerMaterialBatchSpec.js @@ -3,6 +3,7 @@ import { Cartesian2 } from "../../Source/Cesium.js"; import { Cartesian3 } from "../../Source/Cesium.js"; import { Color } from "../../Source/Cesium.js"; import { DistanceDisplayCondition } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { JulianDate } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; import { TimeInterval } from "../../Source/Cesium.js"; @@ -51,7 +52,8 @@ describe("DataSources/StaticGroundGeometryPerMaterialBatch", function () { const batch = new StaticGroundGeometryPerMaterialBatch( scene.primitives, ClassificationType.BOTH, - MaterialAppearance + MaterialAppearance, + new GeographicProjection() ); const ellipse = new EllipseGraphics(); @@ -138,7 +140,8 @@ describe("DataSources/StaticGroundGeometryPerMaterialBatch", function () { const batch = new StaticGroundGeometryPerMaterialBatch( scene.primitives, ClassificationType.BOTH, - MaterialAppearance + MaterialAppearance, + new GeographicProjection() ); const updater = new EllipseGeometryUpdater(entity, scene); @@ -205,7 +208,8 @@ describe("DataSources/StaticGroundGeometryPerMaterialBatch", function () { const batch = new StaticGroundGeometryPerMaterialBatch( scene.primitives, ClassificationType.BOTH, - MaterialAppearance + MaterialAppearance, + new GeographicProjection() ); const updater = new EllipseGeometryUpdater(entity, scene); @@ -245,7 +249,8 @@ describe("DataSources/StaticGroundGeometryPerMaterialBatch", function () { const batch = new StaticGroundGeometryPerMaterialBatch( scene.primitives, ClassificationType.BOTH, - MaterialAppearance + MaterialAppearance, + new GeographicProjection() ); function buildEntity(x, y, z) { @@ -326,7 +331,8 @@ describe("DataSources/StaticGroundGeometryPerMaterialBatch", function () { const batch = new StaticGroundGeometryPerMaterialBatch( scene.primitives, ClassificationType.BOTH, - MaterialAppearance + MaterialAppearance, + new GeographicProjection() ); const ellipse = new EllipseGraphics(); @@ -378,7 +384,8 @@ describe("DataSources/StaticGroundGeometryPerMaterialBatch", function () { const batch = new StaticGroundGeometryPerMaterialBatch( scene.primitives, ClassificationType.BOTH, - MaterialAppearance + MaterialAppearance, + new GeographicProjection() ); const entity = new Entity({ position: new Cartesian3(1234, 5678, 9101112), @@ -431,7 +438,8 @@ describe("DataSources/StaticGroundGeometryPerMaterialBatch", function () { const batch = new StaticGroundGeometryPerMaterialBatch( scene.primitives, ClassificationType.BOTH, - MaterialAppearance + MaterialAppearance, + new GeographicProjection() ); function buildEntity(x, y, z) { diff --git a/Specs/Scene/CameraSpec.js b/Specs/Scene/CameraSpec.js index 024c3d51aff3..6f8d65fccd8b 100644 --- a/Specs/Scene/CameraSpec.js +++ b/Specs/Scene/CameraSpec.js @@ -13,6 +13,7 @@ import { Matrix4 } from "../../Source/Cesium.js"; import { OrthographicFrustum } from "../../Source/Cesium.js"; import { OrthographicOffCenterFrustum } from "../../Source/Cesium.js"; import { PerspectiveFrustum } from "../../Source/Cesium.js"; +import { Proj4Projection } from "../../Source/Cesium.js"; import { Rectangle } from "../../Source/Cesium.js"; import { Transforms } from "../../Source/Cesium.js"; import { WebMercatorProjection } from "../../Source/Cesium.js"; @@ -35,6 +36,13 @@ describe("Scene/Camera", function () { const turnAmount = CesiumMath.PI_OVER_TWO; const rotateAmount = CesiumMath.PI_OVER_TWO; const zoomAmount = 1.0; + const epsg3411polarWkt = + "+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 +a=6378273 +b=6356889.449 +units=m +no_defs"; + const epsg3411polarBounds = Rectangle.fromDegrees(-180.0, 30.0, 180.0, 90.0); + const polarProjection = new Proj4Projection({ + wellKnownText: epsg3411polarWkt, + wgs84Bounds: epsg3411polarBounds, + }); function FakeScene(projection) { this.canvas = { @@ -238,6 +246,45 @@ describe("Scene/Camera", function () { expect(camera.right).toEqualEpsilon(right, CesiumMath.EPSILON8); }); + it("approximate heading in 2D with non-normal-rectangular projections", function () { + const polarScene = new FakeScene(polarProjection); + const polarCamera = new Camera(polarScene); + + polarCamera.up = Cartesian3.clone(Cartesian3.UNIT_Y); + polarCamera.direction = Cartesian3.negate( + Cartesian3.UNIT_Z, + new Cartesian3() + ); + polarCamera.right = Cartesian3.cross(dir, up, new Cartesian3()); + + const frustum = new OrthographicOffCenterFrustum(); + frustum.near = 1.0; + frustum.far = 2.0; + frustum.left = -2.0; + frustum.right = 2.0; + frustum.top = 1.0; + frustum.bottom = -1.0; + polarCamera.frustum = frustum; + polarCamera.update(SceneMode.SCENE2D); + + // In a polar projection, expect the heading to be different + // for the same camera orientation at different positions. + polarCamera.setView({ + destination: Cartesian3.fromDegrees(90, 30, 7000000), + }); + + const longitude90Heading = polarCamera.heading; + + polarCamera.setView({ + destination: Cartesian3.fromDegrees(-90, 30, 7000000), + }); + + expect(longitude90Heading).toEqualEpsilon( + polarCamera.heading - CesiumMath.PI, + CesiumMath.EPSILON7 + ); + }); + it("get heading in CV", function () { camera._mode = SceneMode.COLUMBUS_VIEW; @@ -311,6 +358,47 @@ describe("Scene/Camera", function () { expect(camera.heading).toEqualEpsilon(newHeading, CesiumMath.EPSILON14); }); + it("approximately sets heading in 2D when the map can be rotated and projection is not normal-cylindrical", function () { + const polarScene = new FakeScene(polarProjection); + polarScene.mapMode2D = MapMode2D.ROTATE; + const polarCamera = new Camera(polarScene); + + polarCamera.up = Cartesian3.clone(Cartesian3.UNIT_Y); + polarCamera.direction = Cartesian3.negate( + Cartesian3.UNIT_Z, + new Cartesian3() + ); + polarCamera.right = Cartesian3.cross(dir, up, new Cartesian3()); + + const frustum = new OrthographicOffCenterFrustum(); + frustum.near = 1.0; + frustum.far = 2.0; + frustum.left = -2.0; + frustum.right = 2.0; + frustum.top = 1.0; + frustum.bottom = -1.0; + polarCamera.frustum = frustum; + polarCamera.update(SceneMode.SCENE2D); + + polarCamera.setView({ + destination: Cartesian3.fromDegrees(30, 45, 7000000), + }); + + const heading = polarCamera.heading; + const positionCartographic = polarCamera.positionCartographic; + + const newHeading = CesiumMath.toRadians(90.0); + polarCamera.setView({ + orientation: { + heading: newHeading, + }, + }); + + expect(polarCamera.positionCartographic).toEqual(positionCartographic); + expect(polarCamera.heading).not.toEqual(heading); + expect(polarCamera.heading).toEqualEpsilon(newHeading, CesiumMath.EPSILON7); + }); + it("does not set heading in 2D for infinite scrolling mode", function () { camera._mode = SceneMode.SCENE2D; diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index 66a1a03cf876..a6b2904152d8 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -7,6 +7,7 @@ import { createWorldTerrain } from "../../Source/Cesium.js"; import { defer } from "../../Source/Cesium.js"; import { Ellipsoid } from "../../Source/Cesium.js"; import { EllipsoidTerrainProvider } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { GeographicTilingScheme } from "../../Source/Cesium.js"; import { Ray } from "../../Source/Cesium.js"; import { GlobeSurfaceTile } from "../../Source/Cesium.js"; @@ -27,10 +28,13 @@ describe("Scene/GlobeSurfaceTile", function () { let processor; beforeEach(function () { + const mapProjection = new GeographicProjection(); frameState = { context: { cache: {}, }, + mapProjection: mapProjection, + serializedMapProjection: mapProjection.serialize(), }; tilingScheme = new GeographicTilingScheme(); diff --git a/Specs/Scene/HeightmapTessellatorSpec.js b/Specs/Scene/HeightmapTessellatorSpec.js index 241e13e2948a..a00d60643743 100644 --- a/Specs/Scene/HeightmapTessellatorSpec.js +++ b/Specs/Scene/HeightmapTessellatorSpec.js @@ -1,84 +1,122 @@ import { Cartesian2 } from "../../Source/Cesium.js"; import { Cartesian3 } from "../../Source/Cesium.js"; +import { Cartographic } from "../../Source/Cesium.js"; +import { CustomProjection } from "../../Source/Cesium.js"; import { Ellipsoid } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { HeightmapTessellator } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; import { Rectangle } from "../../Source/Cesium.js"; import { WebMercatorProjection } from "../../Source/Cesium.js"; describe("Scene/HeightmapTessellator", function () { + const geographicProjection = new GeographicProjection(); + it("throws when heightmap is not provided", function () { expect(function () { - HeightmapTessellator.computeVertices(); + HeightmapTessellator.computeVertices(undefined, geographicProjection); }).toThrowDeveloperError(); expect(function () { - HeightmapTessellator.computeVertices({ - width: 2, - height: 2, - vertices: [], - nativeRectangle: { - west: 10.0, - south: 20.0, - east: 20.0, - north: 30.0, + HeightmapTessellator.computeVertices( + { + width: 2, + height: 2, + vertices: [], + nativeRectangle: { + west: 10.0, + south: 20.0, + east: 20.0, + north: 30.0, + }, + skirtHeight: 10.0, }, - skirtHeight: 10.0, - }); + geographicProjection + ); }).toThrowDeveloperError(); }); it("throws when width or height is not provided", function () { expect(function () { - HeightmapTessellator.computeVertices({ - heightmap: [1.0, 2.0, 3.0, 4.0], - height: 2, - vertices: [], - nativeRectangle: { - west: 10.0, - south: 20.0, - east: 20.0, - north: 30.0, + HeightmapTessellator.computeVertices( + { + heightmap: [1.0, 2.0, 3.0, 4.0], + height: 2, + vertices: [], + nativeRectangle: { + west: 10.0, + south: 20.0, + east: 20.0, + north: 30.0, + }, + skirtHeight: 10.0, }, - skirtHeight: 10.0, - }); + geographicProjection + ); }).toThrowDeveloperError(); expect(function () { - HeightmapTessellator.computeVertices({ - heightmap: [1.0, 2.0, 3.0, 4.0], - width: 2, - vertices: [], - nativeRectangle: { - west: 10.0, - south: 20.0, - east: 20.0, - north: 30.0, + HeightmapTessellator.computeVertices( + { + heightmap: [1.0, 2.0, 3.0, 4.0], + width: 2, + vertices: [], + nativeRectangle: { + west: 10.0, + south: 20.0, + east: 20.0, + north: 30.0, + }, + skirtHeight: 10.0, }, - skirtHeight: 10.0, - }); + geographicProjection + ); }).toThrowDeveloperError(); }); it("throws when nativeRectangle is not provided", function () { expect(function () { - HeightmapTessellator.computeVertices({ - heightmap: [1.0, 2.0, 3.0, 4.0], - width: 2, - height: 2, - vertices: [], - skirtHeight: 10.0, - }); + HeightmapTessellator.computeVertices( + { + heightmap: [1.0, 2.0, 3.0, 4.0], + width: 2, + height: 2, + vertices: [], + skirtHeight: 10.0, + }, + geographicProjection + ); }).toThrowDeveloperError(); }); it("throws when skirtHeight is not provided", function () { + expect(function () { + HeightmapTessellator.computeVertices( + { + heightmap: [1.0, 2.0, 3.0, 4.0], + width: 2, + height: 2, + vertices: [], + nativeRectangle: { + west: 10.0, + south: 20.0, + east: 20.0, + north: 30.0, + }, + }, + geographicProjection + ); + }).toThrowDeveloperError(); + }); + + it("throws when mapProjection is not provided", function () { expect(function () { HeightmapTessellator.computeVertices({ heightmap: [1.0, 2.0, 3.0, 4.0], width: 2, height: 2, vertices: [], + skirtHeight: 10.0, nativeRectangle: { west: 10.0, south: 20.0, @@ -212,7 +250,10 @@ describe("Scene/HeightmapTessellator", function () { CesiumMath.toRadians(40.0) ), }; - const results = HeightmapTessellator.computeVertices(options); + const results = HeightmapTessellator.computeVertices( + options, + geographicProjection + ); const vertices = results.vertices; const ellipsoid = Ellipsoid.WGS84; @@ -254,7 +295,10 @@ describe("Scene/HeightmapTessellator", function () { north: 40.0, }, }; - const results = HeightmapTessellator.computeVertices(options); + const results = HeightmapTessellator.computeVertices( + options, + geographicProjection + ); const vertices = results.vertices; const ellipsoid = Ellipsoid.WGS84; @@ -367,7 +411,10 @@ describe("Scene/HeightmapTessellator", function () { north: 0.02, }, }; - const results = HeightmapTessellator.computeVertices(options); + const results = HeightmapTessellator.computeVertices( + options, + geographicProjection + ); const vertices = results.vertices; const ellipsoid = Ellipsoid.WGS84; @@ -486,11 +533,11 @@ describe("Scene/HeightmapTessellator", function () { }, isGeographic: false, }; - const results = HeightmapTessellator.computeVertices(options); - const vertices = results.vertices; - const ellipsoid = Ellipsoid.WGS84; const projection = new WebMercatorProjection(ellipsoid); + const results = HeightmapTessellator.computeVertices(options, projection); + const vertices = results.vertices; + const nativeRectangle = options.nativeRectangle; const geographicSouthwest = projection.unproject( @@ -553,6 +600,147 @@ describe("Scene/HeightmapTessellator", function () { } }); + it("generates 2D position attributes for projections other than Geographic and Web Mercator", function () { + const width = 3; + const height = 3; + const projection = new CustomProjection("Data/UserGeographic.js"); + + return projection.readyPromise.then(function () { + const options = { + heightmap: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + width: width, + height: height, + skirtHeight: 0.0, + nativeRectangle: { + west: 10.0, + south: 30.0, + east: 20.0, + north: 40.0, + }, + rectangle: new Rectangle( + CesiumMath.toRadians(10.0), + CesiumMath.toRadians(30.0), + CesiumMath.toRadians(20.0), + CesiumMath.toRadians(40.0) + ), + }; + const results = HeightmapTessellator.computeVertices(options, projection); + const vertices = results.vertices; + + const rectangle = options.rectangle; + + for (let j = 0; j < height; ++j) { + const latitude = CesiumMath.lerp( + rectangle.north, + rectangle.south, + j / (height - 1) + ); + for (let i = 0; i < width; ++i) { + const longitude = CesiumMath.lerp( + rectangle.west, + rectangle.east, + i / (width - 1) + ); + + const expectedVertexPosition2d = projection.project( + new Cartographic(longitude, latitude) + ); + + const index = (j * width + i) * 9 + 6; + const vertexPosition2d = new Cartesian3( + vertices[index], + vertices[index + 1], + 0.0 + ); + + expect( + Cartesian3.equalsEpsilon( + vertexPosition2d, + expectedVertexPosition2d, + CesiumMath.EPSILON7 + ) + ).toBe(true); + + const heightSample = vertices[index + 2]; + expect(1.0 <= heightSample && heightSample <= 9.0).toBe(true); + } + } + }); + }); + + it("generates 2D position attributes with relative-to-center", function () { + const width = 3; + const height = 3; + const projection = new CustomProjection("Data/UserGeographic.js"); + return projection.readyPromise.then(function () { + const rectangle = new Rectangle( + CesiumMath.toRadians(10.0), + CesiumMath.toRadians(30.0), + CesiumMath.toRadians(20.0), + CesiumMath.toRadians(40.0) + ); + + const options = { + heightmap: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + width: width, + height: height, + skirtHeight: 0.0, + nativeRectangle: { + west: 10.0, + south: 30.0, + east: 20.0, + north: 40.0, + }, + rectangle: rectangle, + relativeToCenter: projection.ellipsoid.cartographicToCartesian( + Rectangle.center(rectangle) + ), + }; + const results = HeightmapTessellator.computeVertices(options, projection); + const vertices = results.vertices; + const center2D = results.encoding.center2D; + + for (let j = 0; j < height; ++j) { + const latitude = CesiumMath.lerp( + rectangle.north, + rectangle.south, + j / (height - 1) + ); + for (let i = 0; i < width; ++i) { + const longitude = CesiumMath.lerp( + rectangle.west, + rectangle.east, + i / (width - 1) + ); + + const expectedVertexPosition2d = projection.project( + new Cartographic(longitude, latitude) + ); + + const index = (j * width + i) * 9 + 6; + const vertexPosition2d = new Cartesian3( + vertices[index], + vertices[index + 1], + 0.0 + ); + vertexPosition2d.x += center2D.x; + vertexPosition2d.y += center2D.y; + + expect( + Cartesian3.equalsEpsilon( + vertexPosition2d, + expectedVertexPosition2d, + CesiumMath.EPSILON7 + ) + ).toBe(true); + + const heightSample = Math.floor(vertices[index + 2] + center2D.z); + expect(1.0 <= heightSample && heightSample <= 9.0).toBe(true); + } + } + }); + }); + it("supports multi-element little endian heights", function () { const width = 3; const height = 3; @@ -607,7 +795,10 @@ describe("Scene/HeightmapTessellator", function () { elementMultiplier: 10, }, }; - const results = HeightmapTessellator.computeVertices(options); + const results = HeightmapTessellator.computeVertices( + options, + geographicProjection + ); const vertices = results.vertices; const ellipsoid = Ellipsoid.WGS84; @@ -715,7 +906,10 @@ describe("Scene/HeightmapTessellator", function () { isBigEndian: true, }, }; - const results = HeightmapTessellator.computeVertices(options); + const results = HeightmapTessellator.computeVertices( + options, + geographicProjection + ); const vertices = results.vertices; const ellipsoid = Ellipsoid.WGS84; diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js index 6fce6ae84691..10300562f9b3 100644 --- a/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/Specs/Scene/QuadtreePrimitiveSpec.js @@ -62,6 +62,8 @@ describe("Scene/QuadtreePrimitive", function () { "computeVisibility", ]), afterRender: [], + mapProjection: scene.mapProjection, + serializedMapProjection: scene.mapProjection.serialize(), pixelRatio: 1.0, terrainExaggeration: 1.0, diff --git a/Specs/Scene/QuadtreeTileSpec.js b/Specs/Scene/QuadtreeTileSpec.js index 94159eb28cc6..ffd1b5d0f5ee 100644 --- a/Specs/Scene/QuadtreeTileSpec.js +++ b/Specs/Scene/QuadtreeTileSpec.js @@ -1,3 +1,7 @@ +import { BoundingSphere } from "../../Source/Cesium.js"; +import { Cartesian3 } from "../../Source/Cesium.js"; +import { Cartographic } from "../../Source/Cesium.js"; +import { GeographicProjection } from "../../Source/Cesium.js"; import { GeographicTilingScheme } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; import { Rectangle } from "../../Source/Cesium.js"; @@ -111,6 +115,71 @@ describe("Scene/QuadtreeTile", function () { }).toThrowDeveloperError(); }); + it("caches a 2D bounding sphere", function () { + const tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX: 1, + numberOfLevelZeroTilesY: 1, + }); + const levelZeroTile = QuadtreeTile.createLevelZeroTiles(tilingScheme)[0]; + const mapProjection = new GeographicProjection(); + const minimumHeight = 0.0; + const maximumHeight = 1000.0; + + const expectedBoundingSphere = BoundingSphere.fromRectangleWithHeights2D( + Rectangle.MAX_VALUE, + mapProjection, + minimumHeight, + maximumHeight + ); + + const boundingSphere2D = levelZeroTile.getBoundingSphere2D( + mapProjection, + minimumHeight, + maximumHeight + ); + expect(boundingSphere2D).toEqual(expectedBoundingSphere); + + const boundingSphere2D2 = levelZeroTile.getBoundingSphere2D( + mapProjection, + minimumHeight, + maximumHeight + ); + expect(boundingSphere2D2).toBe(boundingSphere2D); + }); + + it("caches projected corners", function () { + const tilingScheme = new GeographicTilingScheme({ + numberOfLevelZeroTilesX: 1, + numberOfLevelZeroTilesY: 1, + }); + const levelZeroTile = QuadtreeTile.createLevelZeroTiles(tilingScheme)[0]; + const mapProjection = new GeographicProjection(); + + const southwest = new Cartesian3(); + const northeast = new Cartesian3(); + + const southwestExpected = mapProjection.project( + Cartographic.fromDegrees(-180.0, -90.0) + ); + const northeastExpected = mapProjection.project( + Cartographic.fromDegrees(180.0, 90.0) + ); + + spyOn(GeographicProjection.prototype, "project").and.callThrough(); + + levelZeroTile.getProjectedCorners(mapProjection, southwest, northeast); + expect(southwest).toEqual(southwestExpected); + expect(northeast).toEqual(northeastExpected); + + expect(GeographicProjection.prototype.project.calls.count()).toEqual(2); + + levelZeroTile.getProjectedCorners(mapProjection, southwest, northeast); + expect(southwest).toEqual(southwestExpected); + expect(northeast).toEqual(northeastExpected); + + expect(GeographicProjection.prototype.project.calls.count()).toEqual(2); + }); + it("can get tiles around a root tile", function () { const tilingScheme = new GeographicTilingScheme({ numberOfLevelZeroTilesX: 3, diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js index 973ebb1fe45c..5656c4ece68b 100644 --- a/Specs/Scene/TerrainFillMeshSpec.js +++ b/Specs/Scene/TerrainFillMeshSpec.js @@ -6,6 +6,7 @@ import { GeographicProjection } from "../../Source/Cesium.js"; import { HeightmapTerrainData } from "../../Source/Cesium.js"; import { Intersect } from "../../Source/Cesium.js"; import { Math as CesiumMath } from "../../Source/Cesium.js"; +import { Proj4Projection } from "../../Source/Cesium.js"; import { Camera } from "../../Source/Cesium.js"; import { GlobeSurfaceTileProvider } from "../../Source/Cesium.js"; import { ImageryLayerCollection } from "../../Source/Cesium.js"; @@ -37,9 +38,9 @@ describe("Scene/TerrainFillMesh", function () { let northwest; let northeast; - beforeEach(function () { + function terrainSetup(mapProjection, subsequentSetup) { scene = { - mapProjection: new GeographicProjection(), + mapProjection: mapProjection, drawingBufferWidth: 1000, drawingBufferHeight: 1000, }; @@ -65,6 +66,8 @@ describe("Scene/TerrainFillMesh", function () { "computeVisibility", ]), afterRender: [], + mapProjection: scene.mapProjection, + serializedMapProjection: scene.mapProjection.serialize(), }; frameState.cullingVolume.computeVisibility.and.returnValue( @@ -90,7 +93,9 @@ describe("Scene/TerrainFillMesh", function () { mockTerrain, imageryLayerCollection ); - processor.mockWebGL(); + if (!subsequentSetup) { + processor.mockWebGL(); + } quadtree.render(frameState); rootTiles = quadtree._levelZeroTiles; @@ -281,9 +286,13 @@ describe("Scene/TerrainFillMesh", function () { northeast ) .createMeshWillSucceed(northeast); - }); + } describe("updateFillTiles", function () { + beforeEach(function () { + terrainSetup(new GeographicProjection()); + }); + it("does nothing if no rendered tiles are provided", function () { expect(function () { TerrainFillMesh.updateFillTiles(tileProvider, [], frameState); @@ -696,6 +705,10 @@ describe("Scene/TerrainFillMesh", function () { }); describe("update", function () { + beforeEach(function () { + terrainSetup(new GeographicProjection()); + }); + it("puts a middle height at the four corners and center when there are no adjacent tiles", function () { return processor.process([center]).then(function () { center.data.tileBoundingRegion = new TileBoundingRegion({ @@ -1101,6 +1114,60 @@ describe("Scene/TerrainFillMesh", function () { }); }); + describe("update with arbitrary projection", function () { + it("has positions for 2.5D projected coordinates when using an unusual projection", function () { + let geographicStride; + return processor + .process([center, west, south, east, north]) + .then(function () { + const fill = (center.data.fill = new TerrainFillMesh(center)); + + fill.westTiles.push(west); + fill.westMeshes.push(west.data.mesh); + fill.southTiles.push(south); + fill.southMeshes.push(south.data.mesh); + fill.eastTiles.push(east); + fill.eastMeshes.push(east.data.mesh); + fill.northTiles.push(north); + fill.northMeshes.push(north.data.mesh); + + fill.update(tileProvider, frameState); + + geographicStride = fill.mesh.encoding.getStride(); + }) + .then(function () { + const mollweideWellKnownText = + "+proj=moll +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs"; + terrainSetup( + new Proj4Projection({ + wellKnownText: mollweideWellKnownText, + }), + true + ); + return processor.process([center, west, south, east, north]); + }) + .then(function () { + const fill = (center.data.fill = new TerrainFillMesh(center)); + + fill.westTiles.push(west); + fill.westMeshes.push(west.data.mesh); + fill.southTiles.push(south); + fill.southMeshes.push(south.data.mesh); + fill.eastTiles.push(east); + fill.eastMeshes.push(east.data.mesh); + fill.northTiles.push(north); + fill.northMeshes.push(north.data.mesh); + + fill.update(tileProvider, frameState); + + expectVertexCount(fill, 9); + expect(fill.mesh.encoding.getStride()).toEqual( + geographicStride + 3 + ); + }); + }); + }); + describe("correctly transforms texture coordinates across the anti-meridian", function () { let westernHemisphere; let easternHemisphere; diff --git a/Specs/createFrameState.js b/Specs/createFrameState.js index 5de4d064a50e..3558dba3e38b 100644 --- a/Specs/createFrameState.js +++ b/Specs/createFrameState.js @@ -20,6 +20,8 @@ function createFrameState(context, camera, frameNumber, time) { const projection = new GeographicProjection(); frameState.mapProjection = projection; + frameState.serializedMapProjection = projection.serialize(); + frameState.frameNumber = defaultValue(frameNumber, 1.0); frameState.time = defaultValue( time,