diff --git a/Apps/Sandcastle/gallery/3D Tiles Vertical Exaggeration.html b/Apps/Sandcastle/gallery/3D Tiles Vertical Exaggeration.html new file mode 100644 index 000000000000..ed5b9637b2dc --- /dev/null +++ b/Apps/Sandcastle/gallery/3D Tiles Vertical Exaggeration.html @@ -0,0 +1,154 @@ + + + + + + + + + Cesium Demo + + + + + +
+
+

Loading...

+
+
+ + + + + + + + + + + +
Exaggeration + + +
Relative Height + + +
+
+ + + diff --git a/Apps/Sandcastle/gallery/3D Tiles Vertical Exaggeration.jpg b/Apps/Sandcastle/gallery/3D Tiles Vertical Exaggeration.jpg new file mode 100644 index 000000000000..3db48e2ae825 Binary files /dev/null and b/Apps/Sandcastle/gallery/3D Tiles Vertical Exaggeration.jpg differ diff --git a/Apps/Sandcastle/gallery/Terrain Exaggeration.html b/Apps/Sandcastle/gallery/Terrain Exaggeration.html index 330eef8f1485..6764cdd1546d 100644 --- a/Apps/Sandcastle/gallery/Terrain Exaggeration.html +++ b/Apps/Sandcastle/gallery/Terrain Exaggeration.html @@ -82,8 +82,8 @@ const scene = viewer.scene; const globe = scene.globe; - globe.terrainExaggeration = 2.0; - globe.terrainExaggerationRelativeHeight = 2400.0; + scene.verticalExaggeration = 2.0; + scene.verticalExaggerationRelativeHeight = 2400.0; scene.camera.setView({ destination: new Cesium.Cartesian3( @@ -115,8 +115,8 @@ function updateMaterial() { if (visualizeRelativeHeight) { - const height = globe.terrainExaggerationRelativeHeight; - const exaggeration = globe.terrainExaggeration; + const height = scene.verticalExaggerationRelativeHeight; + const exaggeration = scene.verticalExaggeration; const alpha = Math.min(1.0, exaggeration * 0.25); const layer = { extendUpwards: true, @@ -155,13 +155,13 @@ updateMaterial(); const viewModel = { - exaggeration: globe.terrainExaggeration, - relativeHeight: globe.terrainExaggerationRelativeHeight, + exaggeration: scene.verticalExaggeration, + relativeHeight: scene.verticalExaggerationRelativeHeight, }; function updateExaggeration() { - globe.terrainExaggeration = Number(viewModel.exaggeration); - globe.terrainExaggerationRelativeHeight = Number( + scene.verticalExaggeration = Number(viewModel.exaggeration); + scene.verticalExaggerationRelativeHeight = Number( viewModel.relativeHeight ); updateMaterial(); diff --git a/CHANGES.md b/CHANGES.md index ba1296873239..98ff1d266ba4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,12 +4,20 @@ #### @cesium/engine +##### Additions :tada: + +- Vertical exaggeration can now be applied to a `Cesium3DTileset`. Exaggeration of `Terrain` and `Cesium3DTileset` can be controlled simultaneously via the new `Scene` properties `Scene.verticalExaggeration` and `Scene.verticalExaggerationRelativeHeight`. [#11655](https://github.com/CesiumGS/cesium/pull/11655) + ##### Fixes :wrench: - Changes the default `RequestScheduler.maximumRequestsPerServer` from 6 to 18. This should improve performance on HTTP/2 servers and above [#11627](https://github.com/CesiumGS/cesium/issues/11627) - Corrected JSDoc and Typescript definitions that marked optional arguments as required in `ImageryProvider` constructor [#11625](https://github.com/CesiumGS/cesium/issues/11625) - The `Quaternion.computeAxis` function created an axis that was `(0,0,0)` for the unit quaternion, and an axis that was `(NaN,NaN,NaN)` for the quaternion `(0,0,0,-1)` (which describes a rotation about 360 degrees). Now, it returns the x-axis `(1,0,0)` in both of these cases. +##### Deprecated :hourglass_flowing_sand: + +- `Globe.terrainExaggeration` and `Globe.terrainExaggerationRelativeHeight` have been deprecated in CesiumJS 1.113. They will be removed in 1.116. Use `Scene.verticalExaggeration` and `Scene.verticalExaggerationRelativeHeight` instead. [#11655](https://github.com/CesiumGS/cesium/pull/11655) + ### 1.112 - 2023-12-01 #### @cesium/engine diff --git a/Specs/createFrameState.js b/Specs/createFrameState.js index eb07cff2190b..672a2376fe58 100644 --- a/Specs/createFrameState.js +++ b/Specs/createFrameState.js @@ -43,8 +43,8 @@ function createFrameState(context, camera, frameNumber, time) { camera.up ); - frameState.terrainExaggeration = 1.0; - frameState.terrainExaggerationRelativeHeight = 0.0; + frameState.verticalExaggeration = 1.0; + frameState.verticalExaggerationRelativeHeight = 0.0; frameState.passes.render = true; frameState.passes.pick = false; diff --git a/packages/engine/Source/Core/Ellipsoid.js b/packages/engine/Source/Core/Ellipsoid.js index 88e7937e622d..62d4ba232dbd 100644 --- a/packages/engine/Source/Core/Ellipsoid.js +++ b/packages/engine/Source/Core/Ellipsoid.js @@ -1,3 +1,4 @@ +import Cartesian2 from "./Cartesian2.js"; import Cartesian3 from "./Cartesian3.js"; import Cartographic from "./Cartographic.js"; import Check from "./Check.js"; @@ -694,6 +695,50 @@ Ellipsoid.prototype.getSurfaceNormalIntersectionWithZAxis = function ( return result; }; +const scratchEndpoint = new Cartesian3(); + +/** + * Computes the ellipsoid curvatures at a given position on the surface. + * + * @param {Cartesian3} surfacePosition The position on the ellipsoid surface where curvatures will be calculated. + * @param {Cartesian2} [result] The cartesian to which to copy the result, or undefined to create and return a new instance. + * @returns {Cartesian2} The local curvature of the ellipsoid surface at the provided position, in east and north directions. + * + * @exception {DeveloperError} position is required. + */ +Ellipsoid.prototype.getLocalCurvature = function (surfacePosition, result) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("surfacePosition", surfacePosition); + //>>includeEnd('debug'); + + if (!defined(result)) { + result = new Cartesian2(); + } + + const primeVerticalEndpoint = this.getSurfaceNormalIntersectionWithZAxis( + surfacePosition, + 0.0, + scratchEndpoint + ); + const primeVerticalRadius = Cartesian3.distance( + surfacePosition, + primeVerticalEndpoint + ); + // meridional radius = (1 - e^2) * primeVerticalRadius^3 / a^2 + // where 1 - e^2 = b^2 / a^2, + // so meridional = b^2 * primeVerticalRadius^3 / a^4 + // = (b * primeVerticalRadius / a^2)^2 * primeVertical + const radiusRatio = + (this.minimumRadius * primeVerticalRadius) / this.maximumRadius ** 2; + const meridionalRadius = primeVerticalRadius * radiusRatio ** 2; + + return Cartesian2.fromElements( + 1.0 / primeVerticalRadius, + 1.0 / meridionalRadius, + result + ); +}; + const abscissas = [ 0.14887433898163, 0.43339539412925, diff --git a/packages/engine/Source/Core/RectangleOutlineGeometry.js b/packages/engine/Source/Core/RectangleOutlineGeometry.js index 03d7cd32d210..35988cb7daf8 100644 --- a/packages/engine/Source/Core/RectangleOutlineGeometry.js +++ b/packages/engine/Source/Core/RectangleOutlineGeometry.js @@ -159,11 +159,9 @@ function constructRectangle(geometry, computedOptions) { } function constructExtrudedRectangle(rectangleGeometry, computedOptions) { - const surfaceHeight = rectangleGeometry._surfaceHeight; - const extrudedHeight = rectangleGeometry._extrudedHeight; + const maxHeight = rectangleGeometry._surfaceHeight; + const minHeight = rectangleGeometry._extrudedHeight; const ellipsoid = rectangleGeometry._ellipsoid; - const minHeight = extrudedHeight; - const maxHeight = surfaceHeight; const geo = constructRectangle(rectangleGeometry, computedOptions); const height = computedOptions.height; diff --git a/packages/engine/Source/Core/TerrainEncoding.js b/packages/engine/Source/Core/TerrainEncoding.js index f6a6702babdc..d688341f25b9 100644 --- a/packages/engine/Source/Core/TerrainEncoding.js +++ b/packages/engine/Source/Core/TerrainEncoding.js @@ -6,7 +6,7 @@ import defaultValue from "./defaultValue.js"; import defined from "./defined.js"; import CesiumMath from "./Math.js"; import Matrix4 from "./Matrix4.js"; -import TerrainExaggeration from "./TerrainExaggeration.js"; +import VerticalExaggeration from "./VerticalExaggeration.js"; import TerrainQuantization from "./TerrainQuantization.js"; const cartesian3Scratch = new Cartesian3(); @@ -386,7 +386,7 @@ TerrainEncoding.prototype.getExaggeratedPosition = function ( ); const rawHeight = this.decodeHeight(buffer, index); const heightDifference = - TerrainExaggeration.getHeight( + VerticalExaggeration.getHeight( rawHeight, exaggeration, exaggerationRelativeHeight diff --git a/packages/engine/Source/Core/TerrainExaggeration.js b/packages/engine/Source/Core/TerrainExaggeration.js deleted file mode 100644 index fa5e68377f80..000000000000 --- a/packages/engine/Source/Core/TerrainExaggeration.js +++ /dev/null @@ -1,49 +0,0 @@ -import Cartesian3 from "./Cartesian3.js"; - -/** - * @private - */ -const TerrainExaggeration = {}; - -/** - * Scales a height relative to an offset. - * - * @param {number} height The height. - * @param {number} scale A scalar used to exaggerate the terrain. If the value is 1.0 there will be no effect. - * @param {number} relativeHeight The height relative to which terrain is exaggerated. If the value is 0.0 terrain will be exaggerated relative to the ellipsoid surface. - */ -TerrainExaggeration.getHeight = function (height, scale, relativeHeight) { - return (height - relativeHeight) * scale + relativeHeight; -}; - -const scratchCartographic = new Cartesian3(); - -/** - * Scales a position by exaggeration. - */ -TerrainExaggeration.getPosition = function ( - position, - ellipsoid, - terrainExaggeration, - terrainExaggerationRelativeHeight, - result -) { - const cartographic = ellipsoid.cartesianToCartographic( - position, - scratchCartographic - ); - const newHeight = TerrainExaggeration.getHeight( - cartographic.height, - terrainExaggeration, - terrainExaggerationRelativeHeight - ); - return Cartesian3.fromRadians( - cartographic.longitude, - cartographic.latitude, - newHeight, - ellipsoid, - result - ); -}; - -export default TerrainExaggeration; diff --git a/packages/engine/Source/Core/VerticalExaggeration.js b/packages/engine/Source/Core/VerticalExaggeration.js new file mode 100644 index 000000000000..e049160a89d3 --- /dev/null +++ b/packages/engine/Source/Core/VerticalExaggeration.js @@ -0,0 +1,69 @@ +import Cartesian3 from "./Cartesian3.js"; +import DeveloperError from "./DeveloperError.js"; +import defined from "./defined.js"; + +/** + * @private + */ +const VerticalExaggeration = {}; + +/** + * Scales a height relative to an offset. + * + * @param {number} height The height. + * @param {number} scale A scalar used to exaggerate the terrain. If the value is 1.0 there will be no effect. + * @param {number} relativeHeight The height relative to which terrain is exaggerated. If the value is 0.0 terrain will be exaggerated relative to the ellipsoid surface. + */ +VerticalExaggeration.getHeight = function (height, scale, relativeHeight) { + //>>includeStart('debug', pragmas.debug); + if (!Number.isFinite(scale)) { + throw new DeveloperError("scale must be a finite number."); + } + if (!Number.isFinite(relativeHeight)) { + throw new DeveloperError("relativeHeight must be a finite number."); + } + //>>includeEnd('debug') + return (height - relativeHeight) * scale + relativeHeight; +}; + +const scratchCartographic = new Cartesian3(); + +/** + * Scales a position by exaggeration. + * + * @param {Cartesian3} position The position. + * @param {Ellipsoid} ellipsoid The ellipsoid. + * @param {number} verticalExaggeration A scalar used to exaggerate the terrain. If the value is 1.0 there will be no effect. + * @param {number} verticalExaggerationRelativeHeight The height relative to which terrain is exaggerated. If the value is 0.0 terrain will be exaggerated relative to the ellipsoid surface. + * @param {Cartesian3} [result] The object onto which to store the result. + */ +VerticalExaggeration.getPosition = function ( + position, + ellipsoid, + verticalExaggeration, + verticalExaggerationRelativeHeight, + result +) { + const cartographic = ellipsoid.cartesianToCartographic( + position, + scratchCartographic + ); + // If the position is too near the center of the ellipsoid, exaggeration is undefined. + if (!defined(cartographic)) { + return Cartesian3.clone(position, result); + } + const newHeight = VerticalExaggeration.getHeight( + cartographic.height, + verticalExaggeration, + verticalExaggerationRelativeHeight + ); + return Cartesian3.fromRadians( + cartographic.longitude, + cartographic.latitude, + newHeight, + ellipsoid, + result + ); +}; + +export default VerticalExaggeration; diff --git a/packages/engine/Source/Renderer/AutomaticUniforms.js b/packages/engine/Source/Renderer/AutomaticUniforms.js index a3304ee874b4..2a8930c68edf 100644 --- a/packages/engine/Source/Renderer/AutomaticUniforms.js +++ b/packages/engine/Source/Renderer/AutomaticUniforms.js @@ -939,6 +939,59 @@ const AutomaticUniforms = { }, }), + /** + * An automatic GLSL uniform containing the ellipsoid surface normal + * at the position below the eye (camera), in eye coordinates. + * This uniform is only valid when the {@link SceneMode} is SCENE3D. + */ + czm_eyeEllipsoidNormalEC: new AutomaticUniform({ + size: 1, + datatype: WebGLConstants.FLOAT_VEC3, + getValue: function (uniformState) { + return uniformState.eyeEllipsoidNormalEC; + }, + }), + + /** + * An automatic GLSL uniform containing the ellipsoid radii of curvature at the camera position. + * The .x component is the prime vertical radius, .y is the meridional. + * This uniform is only valid when the {@link SceneMode} is SCENE3D. + */ + czm_eyeEllipsoidCurvature: new AutomaticUniform({ + size: 1, + datatype: WebGLConstants.FLOAT_VEC2, + getValue: function (uniformState) { + return uniformState.eyeEllipsoidCurvature; + }, + }), + + /** + * An automatic GLSL uniform containing the transform from model coordinates + * to an east-north-up coordinate system centered at the position on the + * ellipsoid below the camera. + * This uniform is only valid when the {@link SceneMode} is SCENE3D. + */ + czm_modelToEnu: new AutomaticUniform({ + size: 1, + datatype: WebGLConstants.FLOAT_MAT4, + getValue: function (uniformState) { + return uniformState.modelToEnu; + }, + }), + + /** + * An automatic GLSL uniform containing the the inverse of + * {@link AutomaticUniforms.czm_modelToEnu}. + * This uniform is only valid when the {@link SceneMode} is SCENE3D. + */ + czm_enuToModel: new AutomaticUniform({ + size: 1, + datatype: WebGLConstants.FLOAT_MAT4, + getValue: function (uniformState) { + return uniformState.enuToModel; + }, + }), + /** * An automatic GLSL uniform containing the near distance (x) and the far distance (y) * of the frustum defined by the camera. This is the largest possible frustum, not an individual diff --git a/packages/engine/Source/Renderer/UniformState.js b/packages/engine/Source/Renderer/UniformState.js index ffe2b972ecaa..41e2e821b0aa 100644 --- a/packages/engine/Source/Renderer/UniformState.js +++ b/packages/engine/Source/Renderer/UniformState.js @@ -144,6 +144,10 @@ function UniformState() { this._frustum2DWidth = 0.0; this._eyeHeight = 0.0; this._eyeHeight2D = new Cartesian2(); + this._eyeEllipsoidNormalEC = new Cartesian3(); + this._eyeEllipsoidCurvature = new Cartesian2(); + this._modelToEnu = new Matrix4(); + this._enuToModel = new Matrix4(); this._pixelRatio = 1.0; this._orthographicIn3D = false; this._backgroundColor = new Color(); @@ -696,6 +700,52 @@ Object.defineProperties(UniformState.prototype, { }, }, + /** + * The ellipsoid surface normal at the camera position, in model coordinates. + * @memberof UniformState.prototype + * @type {Cartesian3} + */ + eyeEllipsoidNormalEC: { + get: function () { + return this._eyeEllipsoidNormalEC; + }, + }, + + /** + * The ellipsoid radii of curvature at the camera position. + * The .x component is the prime vertical radius, .y is the meridional. + * @memberof UniformState.prototype + * @type {Cartesian2} + */ + eyeEllipsoidCurvature: { + get: function () { + return this._eyeEllipsoidCurvature; + }, + }, + + /** + * A transform from model coordinates to an east-north-up coordinate system + * centered at the position on the ellipsoid below the camera + * @memberof UniformState.prototype + * @type {Matrix4} + */ + modelToEnu: { + get: function () { + return this._modelToEnu; + }, + }, + + /** + * The inverse of {@link UniformState.prototype.modelToEnu} + * @memberof UniformState.prototype + * @type {Matrix4} + */ + enuToModel: { + get: function () { + return this._enuToModel; + }, + }, + /** * The sun position in 3D world coordinates at the current scene time. * @memberof UniformState.prototype @@ -1068,20 +1118,88 @@ function setInfiniteProjection(uniformState, matrix) { uniformState._modelViewInfiniteProjectionDirty = true; } +const surfacePositionScratch = new Cartesian3(); +const enuTransformScratch = new Matrix4(); + function setCamera(uniformState, camera) { Cartesian3.clone(camera.positionWC, uniformState._cameraPosition); Cartesian3.clone(camera.directionWC, uniformState._cameraDirection); Cartesian3.clone(camera.rightWC, uniformState._cameraRight); Cartesian3.clone(camera.upWC, uniformState._cameraUp); + const ellipsoid = uniformState._ellipsoid; + let surfacePosition; + const positionCartographic = camera.positionCartographic; if (!defined(positionCartographic)) { - uniformState._eyeHeight = -uniformState._ellipsoid.maximumRadius; + uniformState._eyeHeight = -ellipsoid.maximumRadius; + if (Cartesian3.magnitude(camera.positionWC) > 0.0) { + uniformState._eyeEllipsoidNormalEC = Cartesian3.normalize( + camera.positionWC, + uniformState._eyeEllipsoidNormalEC + ); + } + surfacePosition = ellipsoid.scaleToGeodeticSurface( + camera.positionWC, + surfacePositionScratch + ); } else { uniformState._eyeHeight = positionCartographic.height; + uniformState._eyeEllipsoidNormalEC = ellipsoid.geodeticSurfaceNormalCartographic( + positionCartographic, + uniformState._eyeEllipsoidNormalEC + ); + surfacePosition = Cartesian3.fromRadians( + positionCartographic.longitude, + positionCartographic.latitude, + 0.0, + ellipsoid, + surfacePositionScratch + ); } uniformState._encodedCameraPositionMCDirty = true; + + if (!defined(surfacePosition)) { + return; + } + + uniformState._eyeEllipsoidNormalEC = Matrix3.multiplyByVector( + uniformState._viewRotation, + uniformState._eyeEllipsoidNormalEC, + uniformState._eyeEllipsoidNormalEC + ); + + const enuToWorld = Transforms.eastNorthUpToFixedFrame( + surfacePosition, + ellipsoid, + enuTransformScratch + ); + uniformState._enuToModel = Matrix4.multiplyTransformation( + uniformState.inverseModel, + enuToWorld, + uniformState._enuToModel + ); + uniformState._modelToEnu = Matrix4.inverseTransformation( + uniformState._enuToModel, + uniformState._modelToEnu + ); + + if ( + !CesiumMath.equalsEpsilon( + ellipsoid._radii.x, + ellipsoid._radii.y, + CesiumMath.EPSILON15 + ) + ) { + // Ellipsoid curvature calculations assume radii.x === radii.y as is true for WGS84 + return; + } + + uniformState._eyeEllipsoidCurvature = ellipsoid.getLocalCurvature( + surfacePosition, + uniformState._eyeEllipsoidCurvature + ); } let transformMatrix = new Matrix3(); diff --git a/packages/engine/Source/Scene/Cesium3DTile.js b/packages/engine/Source/Scene/Cesium3DTile.js index 86efbeec1cf9..7edd883817d8 100644 --- a/packages/engine/Source/Scene/Cesium3DTile.js +++ b/packages/engine/Source/Scene/Cesium3DTile.js @@ -43,6 +43,7 @@ import TileBoundingS2Cell from "./TileBoundingS2Cell.js"; import TileBoundingSphere from "./TileBoundingSphere.js"; import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; import Pass from "../Renderer/Pass.js"; +import VerticalExaggeration from "../Core/VerticalExaggeration.js"; /** * A tile in a {@link Cesium3DTileset}. When a tile is first created, its content is not loaded; @@ -119,6 +120,9 @@ function Cesium3DTile(tileset, baseResource, header, parent) { */ this.metadata = findTileMetadata(tileset, header); + this._verticalExaggeration = 1.0; + this._verticalExaggerationRelativeHeight = 0.0; + // Important: tile metadata must be parsed before this line so that the // metadata semantics TILE_BOUNDING_BOX, TILE_BOUNDING_REGION, or TILE_BOUNDING_SPHERE // can override header.boundingVolume (if necessary) @@ -1018,7 +1022,7 @@ Cesium3DTile.prototype.updateVisibility = function (frameState) { const parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; - this.updateTransform(parentTransform); + this.updateTransform(parentTransform, frameState); this._distanceToCamera = this.distanceToTile(frameState); this._centerZDepth = this.distanceToTileCenter(frameState); this._screenSpaceError = this.getScreenSpaceError(frameState, false); @@ -1709,12 +1713,18 @@ function createRegion(region, transform, initialTransform, result) { ); } + const rectangleRegion = Rectangle.unpack(region, 0, scratchRectangle); + if (defined(result)) { + result.rectangle = Rectangle.clone(rectangleRegion, result.rectangle); + result.minimumHeight = region[4]; + result.maximumHeight = region[5]; + // The TileBoundingRegion was already constructed with the default + // WGS84 ellipsoid, so keep it consistent when updating. + result.computeBoundingVolumes(Ellipsoid.WGS84); return result; } - const rectangleRegion = Rectangle.unpack(region, 0, scratchRectangle); - return new TileBoundingRegion({ rectangle: rectangleRegion, minimumHeight: region[4], @@ -1793,26 +1803,117 @@ Cesium3DTile.prototype.createBoundingVolume = function ( const { box, region, sphere } = boundingVolumeHeader; if (defined(box)) { - return createBox(box, transform, result); + const tileOrientedBoundingBox = createBox(box, transform, result); + if (this._verticalExaggeration !== 1.0) { + exaggerateBoundingBox( + tileOrientedBoundingBox, + this._verticalExaggeration, + this._verticalExaggerationRelativeHeight + ); + } + return tileOrientedBoundingBox; } if (defined(region)) { - return createRegion(region, transform, this._initialTransform, result); + const tileBoundingVolume = createRegion( + region, + transform, + this._initialTransform, + result + ); + if (this._verticalExaggeration === 1.0) { + return tileBoundingVolume; + } + if (tileBoundingVolume instanceof TileOrientedBoundingBox) { + exaggerateBoundingBox( + tileBoundingVolume, + this._verticalExaggeration, + this._verticalExaggerationRelativeHeight + ); + } else { + tileBoundingVolume.minimumHeight = VerticalExaggeration.getHeight( + tileBoundingVolume.minimumHeight, + this._verticalExaggeration, + this._verticalExaggerationRelativeHeight + ); + tileBoundingVolume.maximumHeight = VerticalExaggeration.getHeight( + tileBoundingVolume.maximumHeight, + this._verticalExaggeration, + this._verticalExaggerationRelativeHeight + ); + tileBoundingVolume.computeBoundingVolumes(Ellipsoid.WGS84); + } + return tileBoundingVolume; } if (defined(sphere)) { - return createSphere(sphere, transform, result); + const tileBoundingSphere = createSphere(sphere, transform, result); + if (this._verticalExaggeration !== 1.0) { + const exaggeratedCenter = VerticalExaggeration.getPosition( + tileBoundingSphere.center, + Ellipsoid.WGS84, + this._verticalExaggeration, + this._verticalExaggerationRelativeHeight, + scratchCenter + ); + const exaggeratedRadius = + tileBoundingSphere.radius * this._verticalExaggeration; + tileBoundingSphere.update(exaggeratedCenter, exaggeratedRadius); + } + return tileBoundingSphere; } throw new RuntimeError( "boundingVolume must contain a sphere, region, or box" ); }; +const scratchExaggeratedCorners = Cartesian3.unpackArray( + new Array(8 * 3).fill(0) +); + +/** + * Exaggerates the bounding box of a tile based on the provided exaggeration factors. + * + * @private + * @param {TileOrientedBoundingBox} tileOrientedBoundingBox - The oriented bounding box of the tile. + * @param {number} exaggeration - The exaggeration factor to apply to the tile's bounding box. + * @param {number} exaggerationRelativeHeight - The height relative to which exaggeration will be applied. + */ +function exaggerateBoundingBox( + tileOrientedBoundingBox, + exaggeration, + exaggerationRelativeHeight +) { + const exaggeratedCorners = tileOrientedBoundingBox.boundingVolume + .computeCorners(scratchExaggeratedCorners) + .map((corner) => + VerticalExaggeration.getPosition( + corner, + Ellipsoid.WGS84, + exaggeration, + exaggerationRelativeHeight, + corner + ) + ); + const exaggeratedBox = OrientedBoundingBox.fromPoints( + exaggeratedCorners, + scratchOrientedBoundingBox + ); + tileOrientedBoundingBox.update( + exaggeratedBox.center, + exaggeratedBox.halfAxes + ); +} + /** * Update the tile's transform. The transform is applied to the tile's bounding volumes. * * @private * @param {Matrix4} parentTransform + * @param {FrameState} [frameState] */ -Cesium3DTile.prototype.updateTransform = function (parentTransform) { +Cesium3DTile.prototype.updateTransform = function ( + parentTransform, + frameState +) { parentTransform = defaultValue(parentTransform, Matrix4.IDENTITY); const computedTransform = Matrix4.multiplyTransformation( parentTransform, @@ -1823,12 +1924,23 @@ Cesium3DTile.prototype.updateTransform = function (parentTransform) { computedTransform, this.computedTransform ); + const exaggerationChanged = + defined(frameState) && + (this._verticalExaggeration !== frameState.verticalExaggeration || + this._verticalExaggerationRelativeHeight !== + frameState.verticalExaggerationRelativeHeight); - if (!transformChanged) { + if (!transformChanged && !exaggerationChanged) { return; } - - Matrix4.clone(computedTransform, this.computedTransform); + if (transformChanged) { + Matrix4.clone(computedTransform, this.computedTransform); + } + if (exaggerationChanged) { + this._verticalExaggeration = frameState.verticalExaggeration; + this._verticalExaggerationRelativeHeight = + frameState.verticalExaggerationRelativeHeight; + } // Update the bounding volumes const header = this._header; diff --git a/packages/engine/Source/Scene/FrameState.js b/packages/engine/Source/Scene/FrameState.js index 14b50cefe1d7..1ed60a2c1e1e 100644 --- a/packages/engine/Source/Scene/FrameState.js +++ b/packages/engine/Source/Scene/FrameState.js @@ -275,18 +275,18 @@ function FrameState(context, creditDisplay, jobScheduler) { }; /** - * A scalar used to exaggerate the terrain. + * A scalar used to vertically exaggerate the scene * @type {number} * @default 1.0 */ - this.terrainExaggeration = 1.0; + this.verticalExaggeration = 1.0; /** - * The height relative to which terrain is exaggerated. + * The height relative to which the scene is vertically exaggerated. * @type {number} * @default 0.0 */ - this.terrainExaggerationRelativeHeight = 0.0; + this.verticalExaggerationRelativeHeight = 0.0; /** * @typedef FrameState.ShadowState diff --git a/packages/engine/Source/Scene/Globe.js b/packages/engine/Source/Scene/Globe.js index 35dd3ea498cf..fac2e6f3bee8 100644 --- a/packages/engine/Source/Scene/Globe.js +++ b/packages/engine/Source/Scene/Globe.js @@ -28,6 +28,7 @@ import ImageryLayerCollection from "./ImageryLayerCollection.js"; import QuadtreePrimitive from "./QuadtreePrimitive.js"; import SceneMode from "./SceneMode.js"; import ShadowMode from "./ShadowMode.js"; +import deprecationWarning from "../Core/deprecationWarning.js"; /** * The globe rendered in the scene, including its terrain ({@link Globe#terrainProvider}) @@ -340,25 +341,9 @@ function Globe(ellipsoid) { */ this.atmosphereBrightnessShift = 0.0; - /** - * A scalar used to exaggerate the terrain. Defaults to 1.0 (no exaggeration). - * A value of 2.0 scales the terrain by 2x. - * A value of 0.0 makes the terrain completely flat. - * Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid. - * @type {number} - * @default 1.0 - */ - this.terrainExaggeration = 1.0; - - /** - * The height from which terrain is exaggerated. Defaults to 0.0 (scaled relative to ellipsoid surface). - * Terrain that is above this height will scale upwards and terrain that is below this height will scale downwards. - * Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid. - * If {@link Globe#terrainExaggeration} is 1.0 this value will have no effect. - * @type {number} - * @default 0.0 - */ - this.terrainExaggerationRelativeHeight = 0.0; + this._terrainExaggerationChanged = false; + this._terrainExaggeration = 1.0; + this._terrainExaggerationRelativeHeight = 0.0; /** * Whether to show terrain skirts. Terrain skirts are geometry extending downwards from a tile's edges used to hide seams between neighboring tiles. @@ -538,6 +523,68 @@ Object.defineProperties(Globe.prototype, { return this._terrainProviderChanged; }, }, + /** + * A scalar used to exaggerate the terrain. Defaults to 1.0 (no exaggeration). + * A value of 2.0 scales the terrain by 2x. + * A value of 0.0 makes the terrain completely flat. + * Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid. + * + * @memberof Globe.prototype + * @type {number} + * @default 1.0 + * + * @deprecated + */ + terrainExaggeration: { + get: function () { + deprecationWarning( + "Globe.terrainExaggeration", + "Globe.terrainExaggeration was deprecated in CesiumJS 1.113. It will be removed in CesiumJS 1.116. Use Scene.verticalExaggeration instead." + ); + return this._terrainExaggeration; + }, + set: function (value) { + deprecationWarning( + "Globe.terrainExaggeration", + "Globe.terrainExaggeration was deprecated in CesiumJS 1.113. It will be removed in CesiumJS 1.116. Use Scene.verticalExaggeration instead." + ); + if (value !== this._terrainExaggeration) { + this._terrainExaggeration = value; + this._terrainExaggerationChanged = true; + } + }, + }, + /** + * The height from which terrain is exaggerated. Defaults to 0.0 (scaled relative to ellipsoid surface). + * Terrain that is above this height will scale upwards and terrain that is below this height will scale downwards. + * Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid. + * If {@link Globe#terrainExaggeration} is 1.0 this value will have no effect. + * + * @memberof Globe.prototype + * @type {number} + * @default 0.0 + * + * @deprecated + */ + terrainExaggerationRelativeHeight: { + get: function () { + deprecationWarning( + "Globe.terrainExaggerationRelativeHeight", + "Globe.terrainExaggerationRelativeHeight was deprecated in CesiumJS 1.113. It will be removed in CesiumJS 1.116. Use Scene.verticalExaggerationRelativeHeight instead." + ); + return this._terrainExaggerationRelativeHeight; + }, + set: function (value) { + deprecationWarning( + "Globe.terrainExaggerationRelativeHeight", + "Globe.terrainExaggerationRelativeHeight was deprecated in CesiumJS 1.113. It will be removed in CesiumJS 1.116. Use Scene.verticalExaggerationRelativeHeight instead." + ); + if (value !== this._terrainExaggerationRelativeHeight) { + this._terrainExaggerationRelativeHeight = value; + this._terrainExaggerationChanged = true; + } + }, + }, /** * Gets an event that's raised when the length of the tile load queue has changed since the last render frame. When the load queue is empty, * all terrain and imagery for the current view have been loaded. The event passes the new length of the tile load queue. diff --git a/packages/engine/Source/Scene/GlobeSurfaceTile.js b/packages/engine/Source/Scene/GlobeSurfaceTile.js index 8ac18d1b6904..c1c7ab20dd52 100644 --- a/packages/engine/Source/Scene/GlobeSurfaceTile.js +++ b/packages/engine/Source/Scene/GlobeSurfaceTile.js @@ -463,9 +463,9 @@ GlobeSurfaceTile.prototype.updateExaggeration = function ( } // Check the tile's terrain encoding to see if it has been exaggerated yet - const exaggeration = frameState.terrainExaggeration; + const exaggeration = frameState.verticalExaggeration; const exaggerationRelativeHeight = - frameState.terrainExaggerationRelativeHeight; + frameState.verticalExaggerationRelativeHeight; const hasExaggerationScale = exaggeration !== 1.0; const encoding = mesh.encoding; @@ -790,9 +790,9 @@ function transform(surfaceTile, frameState, terrainProvider, x, y, level) { createMeshOptions.x = x; createMeshOptions.y = y; createMeshOptions.level = level; - createMeshOptions.exaggeration = frameState.terrainExaggeration; + createMeshOptions.exaggeration = frameState.verticalExaggeration; createMeshOptions.exaggerationRelativeHeight = - frameState.terrainExaggerationRelativeHeight; + frameState.verticalExaggerationRelativeHeight; createMeshOptions.throttle = true; const terrainData = surfaceTile.terrainData; diff --git a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js index bcf38cd60780..e32b797efc48 100644 --- a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js +++ b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js @@ -25,7 +25,7 @@ import OrthographicFrustum from "../Core/OrthographicFrustum.js"; import PrimitiveType from "../Core/PrimitiveType.js"; import Rectangle from "../Core/Rectangle.js"; import SphereOutlineGeometry from "../Core/SphereOutlineGeometry.js"; -import TerrainExaggeration from "../Core/TerrainExaggeration.js"; +import VerticalExaggeration from "../Core/VerticalExaggeration.js"; import TerrainQuantization from "../Core/TerrainQuantization.js"; import Visibility from "../Core/Visibility.js"; import WebMercatorProjection from "../Core/WebMercatorProjection.js"; @@ -179,8 +179,8 @@ function GlobeSurfaceTileProvider(options) { this._hasLoadedTilesThisFrame = false; this._hasFillTilesThisFrame = false; - this._oldTerrainExaggeration = undefined; - this._oldTerrainExaggerationRelativeHeight = undefined; + this._oldVerticalExaggeration = undefined; + this._oldVerticalExaggerationRelativeHeight = undefined; } Object.defineProperties(GlobeSurfaceTileProvider.prototype, { @@ -448,7 +448,7 @@ GlobeSurfaceTileProvider.prototype.endUpdate = function (frameState) { ); } - // When terrain exaggeration changes, all of the loaded tiles need to generate + // When vertical exaggeration changes, all of the loaded tiles need to generate // geodetic surface normals so they can scale properly when rendered. // When exaggeration is reset, geodetic surface normals are removed to decrease // memory usage. Some tiles might have been constructed with the correct @@ -460,16 +460,16 @@ GlobeSurfaceTileProvider.prototype.endUpdate = function (frameState) { // exaggeration changes. const quadtree = this.quadtree; - const exaggeration = frameState.terrainExaggeration; + const exaggeration = frameState.verticalExaggeration; const exaggerationRelativeHeight = - frameState.terrainExaggerationRelativeHeight; + frameState.verticalExaggerationRelativeHeight; const exaggerationChanged = - this._oldTerrainExaggeration !== exaggeration || - this._oldTerrainExaggerationRelativeHeight !== exaggerationRelativeHeight; + this._oldVerticalExaggeration !== exaggeration || + this._oldVerticalExaggerationRelativeHeight !== exaggerationRelativeHeight; // Keep track of the next time there is a change in exaggeration - this._oldTerrainExaggeration = exaggeration; - this._oldTerrainExaggerationRelativeHeight = exaggerationRelativeHeight; + this._oldVerticalExaggeration = exaggeration; + this._oldVerticalExaggerationRelativeHeight = exaggerationRelativeHeight; if (exaggerationChanged) { quadtree.forEachLoadedTile(function (tile) { @@ -1258,18 +1258,18 @@ function updateTileBoundingRegion(tile, tileProvider, frameState) { // Update bounding regions from the min and max heights if (sourceTile !== undefined) { - const exaggeration = frameState.terrainExaggeration; + const exaggeration = frameState.verticalExaggeration; const exaggerationRelativeHeight = - frameState.terrainExaggerationRelativeHeight; + frameState.verticalExaggerationRelativeHeight; const hasExaggeration = exaggeration !== 1.0; if (hasExaggeration) { hasBoundingVolumesFromMesh = false; - tileBoundingRegion.minimumHeight = TerrainExaggeration.getHeight( + tileBoundingRegion.minimumHeight = VerticalExaggeration.getHeight( tileBoundingRegion.minimumHeight, exaggeration, exaggerationRelativeHeight ); - tileBoundingRegion.maximumHeight = TerrainExaggeration.getHeight( + tileBoundingRegion.maximumHeight = VerticalExaggeration.getHeight( tileBoundingRegion.maximumHeight, exaggeration, exaggerationRelativeHeight @@ -1625,8 +1625,8 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) { u_center3D: function () { return this.properties.center3D; }, - u_terrainExaggerationAndRelativeHeight: function () { - return this.properties.terrainExaggerationAndRelativeHeight; + u_verticalExaggerationAndRelativeHeight: function () { + return this.properties.verticalExaggerationAndRelativeHeight; }, u_tileRectangle: function () { return this.properties.tileRectangle; @@ -1808,7 +1808,7 @@ function createTileUniformMap(frameState, globeSurfaceTileProvider) { modifiedModelView: new Matrix4(), tileRectangle: new Cartesian4(), - terrainExaggerationAndRelativeHeight: new Cartesian2(1.0, 0.0), + verticalExaggerationAndRelativeHeight: new Cartesian2(1.0, 0.0), dayTextures: [], dayTextureTranslationAndScale: [], @@ -2174,9 +2174,9 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { const encoding = mesh.encoding; const tileBoundingRegion = surfaceTile.tileBoundingRegion; - const exaggeration = frameState.terrainExaggeration; + const exaggeration = frameState.verticalExaggeration; const exaggerationRelativeHeight = - frameState.terrainExaggerationRelativeHeight; + frameState.verticalExaggerationRelativeHeight; const hasExaggeration = exaggeration !== 1.0; const hasGeodeticSurfaceNormals = encoding.hasGeodeticSurfaceNormals; @@ -2431,8 +2431,8 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) { ); } - uniformMapProperties.terrainExaggerationAndRelativeHeight.x = exaggeration; - uniformMapProperties.terrainExaggerationAndRelativeHeight.y = exaggerationRelativeHeight; + uniformMapProperties.verticalExaggerationAndRelativeHeight.x = exaggeration; + uniformMapProperties.verticalExaggerationAndRelativeHeight.y = exaggerationRelativeHeight; uniformMapProperties.center3D = mesh.center; Cartesian3.clone(rtc, uniformMapProperties.rtc); diff --git a/packages/engine/Source/Scene/GroundPrimitive.js b/packages/engine/Source/Scene/GroundPrimitive.js index 89e3cd5c0ad1..f67249322b8a 100644 --- a/packages/engine/Source/Scene/GroundPrimitive.js +++ b/packages/engine/Source/Scene/GroundPrimitive.js @@ -10,7 +10,7 @@ import DeveloperError from "../Core/DeveloperError.js"; import GeometryInstance from "../Core/GeometryInstance.js"; import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; import Rectangle from "../Core/Rectangle.js"; -import TerrainExaggeration from "../Core/TerrainExaggeration.js"; +import VerticalExaggeration from "../Core/VerticalExaggeration.js"; import ClassificationPrimitive from "./ClassificationPrimitive.js"; import ClassificationType from "./ClassificationType.js"; import PerInstanceColorAppearance from "./PerInstanceColorAppearance.js"; @@ -763,15 +763,15 @@ GroundPrimitive.prototype.update = function (frameState) { // Now compute the min/max heights for the primitive setMinMaxTerrainHeights(this, rectangle, ellipsoid); - const exaggeration = frameState.terrainExaggeration; + const exaggeration = frameState.verticalExaggeration; const exaggerationRelativeHeight = - frameState.terrainExaggerationRelativeHeight; - this._minHeight = TerrainExaggeration.getHeight( + frameState.verticalExaggerationRelativeHeight; + this._minHeight = VerticalExaggeration.getHeight( this._minTerrainHeight, exaggeration, exaggerationRelativeHeight ); - this._maxHeight = TerrainExaggeration.getHeight( + this._maxHeight = VerticalExaggeration.getHeight( this._maxTerrainHeight, exaggeration, exaggerationRelativeHeight diff --git a/packages/engine/Source/Scene/Megatexture.js b/packages/engine/Source/Scene/Megatexture.js index 759563bd9cfb..5173d98b3911 100644 --- a/packages/engine/Source/Scene/Megatexture.js +++ b/packages/engine/Source/Scene/Megatexture.js @@ -366,7 +366,7 @@ Megatexture.getApproximateTextureMemoryByteLength = function ( const voxelCountTotal = tileCount * dimensions.x * dimensions.y * dimensions.z; - const sliceCountPerRegionX = Math.ceil(Math.sqrt(dimensions.z)); + const sliceCountPerRegionX = Math.ceil(Math.sqrt(dimensions.x)); const sliceCountPerRegionY = Math.ceil(dimensions.z / sliceCountPerRegionX); const voxelCountPerRegionX = sliceCountPerRegionX * dimensions.x; const voxelCountPerRegionY = sliceCountPerRegionY * dimensions.y; diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index 57c174ce725e..75d24a7a7761 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -333,6 +333,8 @@ function Model(options) { this._heightDirty = this._heightReference !== HeightReference.NONE; this._removeUpdateHeightCallback = undefined; + this._verticalExaggerationOn = false; + this._clampedModelMatrix = undefined; // For use with height reference const scene = options.scene; @@ -1789,6 +1791,7 @@ Model.prototype.update = function (frameState) { updateSkipLevelOfDetail(this, frameState); updateClippingPlanes(this, frameState); updateSceneMode(this, frameState); + updateVerticalExaggeration(this, frameState); this._defaultTexture = frameState.context.defaultTexture; @@ -1983,6 +1986,14 @@ function updateSceneMode(model, frameState) { } } +function updateVerticalExaggeration(model, frameState) { + const verticalExaggerationNeeded = frameState.verticalExaggeration !== 1.0; + if (model._verticalExaggerationOn !== verticalExaggerationNeeded) { + model.resetDrawCommands(); + model._verticalExaggerationOn = verticalExaggerationNeeded; + } +} + function buildDrawCommands(model, frameState) { if (!model._drawCommandsBuilt) { model.destroyPipelineResources(); diff --git a/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js b/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js index 5593643409ea..ed82ca255cf4 100644 --- a/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js +++ b/packages/engine/Source/Scene/Model/ModelRuntimePrimitive.js @@ -24,6 +24,7 @@ import PrimitiveStatisticsPipelineStage from "./PrimitiveStatisticsPipelineStage import SceneMode2DPipelineStage from "./SceneMode2DPipelineStage.js"; import SelectedFeatureIdPipelineStage from "./SelectedFeatureIdPipelineStage.js"; import SkinningPipelineStage from "./SkinningPipelineStage.js"; +import VerticalExaggerationPipelineStage from "./VerticalExaggerationPipelineStage.js"; import WireframePipelineStage from "./WireframePipelineStage.js"; /** @@ -198,6 +199,7 @@ ModelRuntimePrimitive.prototype.configurePipeline = function (frameState) { const mode = frameState.mode; const use2D = mode !== SceneMode.SCENE3D && !frameState.scene3DOnly && model._projectTo2D; + const exaggerateTerrain = frameState.verticalExaggeration !== 1.0; const hasMorphTargets = defined(primitive.morphTargets) && primitive.morphTargets.length > 0; @@ -281,6 +283,10 @@ ModelRuntimePrimitive.prototype.configurePipeline = function (frameState) { pipelineStages.push(CPUStylingPipelineStage); } + if (exaggerateTerrain) { + pipelineStages.push(VerticalExaggerationPipelineStage); + } + if (hasCustomShader) { pipelineStages.push(CustomShaderPipelineStage); } diff --git a/packages/engine/Source/Scene/Model/VerticalExaggerationPipelineStage.js b/packages/engine/Source/Scene/Model/VerticalExaggerationPipelineStage.js new file mode 100644 index 000000000000..2a0a059e3123 --- /dev/null +++ b/packages/engine/Source/Scene/Model/VerticalExaggerationPipelineStage.js @@ -0,0 +1,60 @@ +import Cartesian2 from "../../Core/Cartesian2.js"; +import ShaderDestination from "../../Renderer/ShaderDestination.js"; +import VerticalExaggerationStageVS from "../../Shaders/Model/VerticalExaggerationStageVS.js"; + +/** + * The custom shader pipeline stage takes GLSL callbacks from the + * {@link CustomShader} and inserts them into the overall shader code for the + * {@link Model}. The input to the callback is a struct with many + * properties that depend on the attributes of the primitive. This shader code + * is automatically generated by this stage. + * + * @namespace VerticalExaggerationPipelineStage + * + * @private + */ +const VerticalExaggerationPipelineStage = { + name: "VerticalExaggerationPipelineStage", // Helps with debugging +}; + +const scratchExaggerationUniform = new Cartesian2(); + +/** + * Add vertical exaggeration to a shader + * + * @param {PrimitiveRenderResources} renderResources The render resources for the primitive + * @param {ModelComponents.Primitive} primitive The primitive to be rendered + * @param {FrameState} frameState The frame state. + * @private + */ +VerticalExaggerationPipelineStage.process = function ( + renderResources, + primitive, + frameState +) { + const { shaderBuilder, uniformMap } = renderResources; + + shaderBuilder.addVertexLines(VerticalExaggerationStageVS); + + shaderBuilder.addDefine( + "HAS_VERTICAL_EXAGGERATION", + undefined, + ShaderDestination.VERTEX + ); + + shaderBuilder.addUniform( + "vec2", + "u_verticalExaggerationAndRelativeHeight", + ShaderDestination.VERTEX + ); + + uniformMap.u_verticalExaggerationAndRelativeHeight = function () { + return Cartesian2.fromElements( + frameState.verticalExaggeration, + frameState.verticalExaggerationRelativeHeight, + scratchExaggerationUniform + ); + }; +}; + +export default VerticalExaggerationPipelineStage; diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 127c6aacfaad..8b3a207ec133 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -356,6 +356,24 @@ function Scene(options) { */ this.nearToFarDistance2D = 1.75e6; + /** + * The vertical exaggeration of the scene. + * When set to 1.0, no exaggeration is applied. + * + * @type {number} + * @default 1.0 + */ + this.verticalExaggeration = 1.0; + + /** + * The reference height for vertical exaggeration of the scene. + * When set to 0.0, the exaggeration is applied relative to the ellipsoid surface. + * + * @type {number} + * @default 0.0 + */ + this.verticalExaggerationRelativeHeight = 0.0; + /** * This property is for debugging only; it is not for production use. *

@@ -1880,10 +1898,17 @@ Scene.prototype.updateFrameState = function () { frameState.cameraUnderground = this._cameraUnderground; frameState.globeTranslucencyState = this._globeTranslucencyState; - if (defined(this.globe)) { - frameState.terrainExaggeration = this.globe.terrainExaggeration; - frameState.terrainExaggerationRelativeHeight = this.globe.terrainExaggerationRelativeHeight; + const { globe } = this; + if (defined(globe) && globe._terrainExaggerationChanged) { + // Honor a user-set value for the old deprecated globe.terrainExaggeration. + // This can be removed when Globe.terrainExaggeration is removed. + this.verticalExaggeration = globe._terrainExaggeration; + this.verticalExaggerationRelativeHeight = + globe._terrainExaggerationRelativeHeight; + globe._terrainExaggerationChanged = false; } + frameState.verticalExaggeration = this.verticalExaggeration; + frameState.verticalExaggerationRelativeHeight = this.verticalExaggerationRelativeHeight; if ( defined(this._specularEnvironmentMapAtlas) && diff --git a/packages/engine/Source/Scene/ScreenSpaceCameraController.js b/packages/engine/Source/Scene/ScreenSpaceCameraController.js index 80e05cc5bb3e..b2f64010731a 100644 --- a/packages/engine/Source/Scene/ScreenSpaceCameraController.js +++ b/packages/engine/Source/Scene/ScreenSpaceCameraController.js @@ -17,7 +17,7 @@ import OrthographicFrustum from "../Core/OrthographicFrustum.js"; import Plane from "../Core/Plane.js"; import Quaternion from "../Core/Quaternion.js"; import Ray from "../Core/Ray.js"; -import TerrainExaggeration from "../Core/TerrainExaggeration.js"; +import VerticalExaggeration from "../Core/VerticalExaggeration.js"; import Transforms from "../Core/Transforms.js"; import CameraEventAggregator from "./CameraEventAggregator.js"; import CameraEventType from "./CameraEventType.js"; @@ -2948,9 +2948,7 @@ const scratchPreviousDirection = new Cartesian3(); */ ScreenSpaceCameraController.prototype.update = function () { const scene = this._scene; - const camera = scene.camera; - const globe = scene.globe; - const mode = scene.mode; + const { camera, globe, mode } = scene; if (!Matrix4.equals(camera.transform, Matrix4.IDENTITY)) { this._globe = undefined; @@ -2962,26 +2960,21 @@ ScreenSpaceCameraController.prototype.update = function () { : scene.mapProjection.ellipsoid; } - const exaggeration = defined(this._globe) - ? this._globe.terrainExaggeration - : 1.0; - const exaggerationRelativeHeight = defined(this._globe) - ? this._globe.terrainExaggerationRelativeHeight - : 0.0; - this._minimumCollisionTerrainHeight = TerrainExaggeration.getHeight( + const { verticalExaggeration, verticalExaggerationRelativeHeight } = scene; + this._minimumCollisionTerrainHeight = VerticalExaggeration.getHeight( this.minimumCollisionTerrainHeight, - exaggeration, - exaggerationRelativeHeight + verticalExaggeration, + verticalExaggerationRelativeHeight ); - this._minimumPickingTerrainHeight = TerrainExaggeration.getHeight( + this._minimumPickingTerrainHeight = VerticalExaggeration.getHeight( this.minimumPickingTerrainHeight, - exaggeration, - exaggerationRelativeHeight + verticalExaggeration, + verticalExaggerationRelativeHeight ); - this._minimumTrackBallHeight = TerrainExaggeration.getHeight( + this._minimumTrackBallHeight = VerticalExaggeration.getHeight( this.minimumTrackBallHeight, - exaggeration, - exaggerationRelativeHeight + verticalExaggeration, + verticalExaggerationRelativeHeight ); this._cameraUnderground = scene.cameraUnderground && defined(this._globe); diff --git a/packages/engine/Source/Scene/TerrainFillMesh.js b/packages/engine/Source/Scene/TerrainFillMesh.js index 20a667b285e2..9d1671493a63 100644 --- a/packages/engine/Source/Scene/TerrainFillMesh.js +++ b/packages/engine/Source/Scene/TerrainFillMesh.js @@ -830,9 +830,9 @@ function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) { const fill = surfaceTile.fill; const rectangle = tile.rectangle; - const exaggeration = frameState.terrainExaggeration; + const exaggeration = frameState.verticalExaggeration; const exaggerationRelativeHeight = - frameState.terrainExaggerationRelativeHeight; + frameState.verticalExaggerationRelativeHeight; const hasExaggeration = exaggeration !== 1.0; const ellipsoid = tile.tilingScheme.ellipsoid; diff --git a/packages/engine/Source/Scene/TileOrientedBoundingBox.js b/packages/engine/Source/Scene/TileOrientedBoundingBox.js index 9737474a5007..40d60451f51f 100644 --- a/packages/engine/Source/Scene/TileOrientedBoundingBox.js +++ b/packages/engine/Source/Scene/TileOrientedBoundingBox.js @@ -106,7 +106,7 @@ Object.defineProperties(TileOrientedBoundingBox.prototype, { * * @memberof TileOrientedBoundingBox.prototype * - * @type {object} + * @type {OrientedBoundingBox} * @readonly */ boundingVolume: { diff --git a/packages/engine/Source/Scene/VoxelPrimitive.js b/packages/engine/Source/Scene/VoxelPrimitive.js index e835b7bb7e7b..a1d2486c9288 100644 --- a/packages/engine/Source/Scene/VoxelPrimitive.js +++ b/packages/engine/Source/Scene/VoxelPrimitive.js @@ -131,6 +131,38 @@ function VoxelPrimitive(options) { */ this._maxBoundsOld = new Cartesian3(); + /** + * Minimum bounds with vertical exaggeration applied + * + * @type {Cartesian3} + * @private + */ + this._exaggeratedMinBounds = new Cartesian3(); + + /** + * Used to detect if the shape is dirty. + * + * @type {Cartesian3} + * @private + */ + this._exaggeratedMinBoundsOld = new Cartesian3(); + + /** + * Maximum bounds with vertical exaggeration applied + * + * @type {Cartesian3} + * @private + */ + this._exaggeratedMaxBounds = new Cartesian3(); + + /** + * Used to detect if the shape is dirty. + * + * @type {Cartesian3} + * @private + */ + this._exaggeratedMaxBoundsOld = new Cartesian3(); + /** * This member is not known until the provider is ready. * @@ -442,6 +474,7 @@ function initialize(primitive, provider) { // Create the shape object, and update it so it is valid for VoxelTraversal const ShapeConstructor = VoxelShapeType.getShapeConstructor(shapeType); primitive._shape = new ShapeConstructor(); + updateVerticalExaggeration(primitive); primitive._shapeVisible = updateShapeAndTransforms( primitive, primitive._shape, @@ -964,6 +997,8 @@ VoxelPrimitive.prototype.update = function (frameState) { return; } + updateVerticalExaggeration(this, frameState); + // Check if the shape is dirty before updating it. This needs to happen every // frame because the member variables can be modified externally via the // getters. @@ -1107,6 +1142,32 @@ VoxelPrimitive.prototype.update = function (frameState) { frameState.commandList.push(command); }; +/** + * Update the exaggerated bounds of a primitive to account for vertical exaggeration + * Currently only applies to Ellipsoid shape type + * @param {VoxelPrimitive} primitive + * @param {FrameState} [frameState] + * @private + */ +function updateVerticalExaggeration(primitive, frameState) { + primitive._exaggeratedMinBounds = Cartesian3.clone( + primitive._minBounds, + primitive._exaggeratedMinBounds + ); + primitive._exaggeratedMaxBounds = Cartesian3.clone( + primitive._maxBounds, + primitive._exaggeratedMaxBounds + ); + if (defined(frameState) && primitive.shape === VoxelShapeType.ELLIPSOID) { + const relativeHeight = frameState.verticalExaggerationRelativeHeight; + const exaggeration = frameState.verticalExaggeration; + primitive._exaggeratedMinBounds.z = + (primitive._minBounds.z - relativeHeight) * exaggeration + relativeHeight; + primitive._exaggeratedMaxBounds.z = + (primitive._maxBounds.z - relativeHeight) * exaggeration + relativeHeight; + } +} + /** * Initialize primitive properties that are derived from the voxel provider * @param {VoxelPrimitive} primitive @@ -1203,6 +1264,16 @@ function checkTransformAndBounds(primitive, provider) { updateBound(primitive, "_compoundModelMatrix", "_compoundModelMatrixOld") + updateBound(primitive, "_minBounds", "_minBoundsOld") + updateBound(primitive, "_maxBounds", "_maxBoundsOld") + + updateBound( + primitive, + "_exaggeratedMinBounds", + "_exaggeratedMinBoundsOld" + ) + + updateBound( + primitive, + "_exaggeratedMaxBounds", + "_exaggeratedMaxBoundsOld" + ) + updateBound(primitive, "_minClippingBounds", "_minClippingBoundsOld") + updateBound(primitive, "_maxClippingBounds", "_maxClippingBoundsOld"); return numChanges > 0; @@ -1239,8 +1310,8 @@ function updateBound(primitive, newBoundKey, oldBoundKey) { function updateShapeAndTransforms(primitive, shape, provider) { const visible = shape.update( primitive._compoundModelMatrix, - primitive.minBounds, - primitive.maxBounds, + primitive._exaggeratedMinBounds, + primitive._exaggeratedMaxBounds, primitive.minClippingBounds, primitive.maxClippingBounds ); diff --git a/packages/engine/Source/Shaders/GlobeVS.glsl b/packages/engine/Source/Shaders/GlobeVS.glsl index 65b4d1547ff1..05bd21f37d9b 100644 --- a/packages/engine/Source/Shaders/GlobeVS.glsl +++ b/packages/engine/Source/Shaders/GlobeVS.glsl @@ -11,7 +11,7 @@ in vec3 geodeticSurfaceNormal; #endif #ifdef EXAGGERATION -uniform vec2 u_terrainExaggerationAndRelativeHeight; +uniform vec2 u_verticalExaggerationAndRelativeHeight; #endif uniform vec3 u_center3D; @@ -173,8 +173,8 @@ void main() #endif #if defined(EXAGGERATION) && defined(GEODETIC_SURFACE_NORMALS) - float exaggeration = u_terrainExaggerationAndRelativeHeight.x; - float relativeHeight = u_terrainExaggerationAndRelativeHeight.y; + float exaggeration = u_verticalExaggerationAndRelativeHeight.x; + float relativeHeight = u_verticalExaggerationAndRelativeHeight.y; float newHeight = (height - relativeHeight) * exaggeration + relativeHeight; // stop from going through center of earth diff --git a/packages/engine/Source/Shaders/Model/ModelVS.glsl b/packages/engine/Source/Shaders/Model/ModelVS.glsl index bad132276fdf..e9a4eb5e63ec 100644 --- a/packages/engine/Source/Shaders/Model/ModelVS.glsl +++ b/packages/engine/Source/Shaders/Model/ModelVS.glsl @@ -93,6 +93,10 @@ void main() MetadataStatistics metadataStatistics; metadataStage(metadata, metadataClass, metadataStatistics, attributes); + #ifdef HAS_VERTICAL_EXAGGERATION + verticalExaggerationStage(attributes); + #endif + #ifdef HAS_CUSTOM_VERTEX_SHADER czm_modelVertexOutput vsOutput = defaultVertexOutput(attributes.positionMC); customShaderStage(vsOutput, attributes, featureIds, metadata, metadataClass, metadataStatistics); diff --git a/packages/engine/Source/Shaders/Model/VerticalExaggerationStageVS.glsl b/packages/engine/Source/Shaders/Model/VerticalExaggerationStageVS.glsl new file mode 100644 index 000000000000..c7e4f944d288 --- /dev/null +++ b/packages/engine/Source/Shaders/Model/VerticalExaggerationStageVS.glsl @@ -0,0 +1,39 @@ +void verticalExaggerationStage( + inout ProcessedAttributes attributes +) { + // Compute the distance from the camera to the local center of curvature. + vec4 vertexPositionENU = czm_modelToEnu * vec4(attributes.positionMC, 1.0); + vec2 vertexAzimuth = normalize(vertexPositionENU.xy); + // Curvature = 1 / radius of curvature. + float azimuthalCurvature = dot(vertexAzimuth * vertexAzimuth, czm_eyeEllipsoidCurvature); + float eyeToCenter = 1.0 / azimuthalCurvature + czm_eyeHeight; + + // Compute the approximate ellipsoid normal at the vertex position. + // Uses a circular approximation for the Earth curvature along the geodesic. + vec3 vertexPositionEC = (czm_modelView * vec4(attributes.positionMC, 1.0)).xyz; + vec3 centerToVertex = eyeToCenter * czm_eyeEllipsoidNormalEC + vertexPositionEC; + vec3 vertexNormal = normalize(centerToVertex); + + // Estimate the (sine of the) angle between the camera direction and the vertex normal + float verticalDistance = dot(vertexPositionEC, czm_eyeEllipsoidNormalEC); + float horizontalDistance = length(vertexPositionEC - verticalDistance * czm_eyeEllipsoidNormalEC); + float sinTheta = horizontalDistance / (eyeToCenter + verticalDistance); + bool isSmallAngle = clamp(sinTheta, 0.0, 0.05) == sinTheta; + + // Approximate the change in height above the ellipsoid, from camera to vertex position. + float exactVersine = 1.0 - dot(czm_eyeEllipsoidNormalEC, vertexNormal); + float smallAngleVersine = 0.5 * sinTheta * sinTheta; + float versine = isSmallAngle ? smallAngleVersine : exactVersine; + float dHeight = dot(vertexPositionEC, vertexNormal) - eyeToCenter * versine; + float vertexHeight = czm_eyeHeight + dHeight; + + // Transform the approximate vertex normal to model coordinates. + vec3 vertexNormalMC = (czm_inverseModelView * vec4(vertexNormal, 0.0)).xyz; + vertexNormalMC = normalize(vertexNormalMC); + + // Compute the exaggeration and apply it along the approximate vertex normal. + float stretch = u_verticalExaggerationAndRelativeHeight.x; + float shift = u_verticalExaggerationAndRelativeHeight.y; + float exaggeration = (vertexHeight - shift) * (stretch - 1.0); + attributes.positionMC += exaggeration * vertexNormalMC; +} diff --git a/packages/engine/Specs/Core/EllipsoidSpec.js b/packages/engine/Specs/Core/EllipsoidSpec.js index 356bb5fdbb54..af039cf18fbc 100644 --- a/packages/engine/Specs/Core/EllipsoidSpec.js +++ b/packages/engine/Specs/Core/EllipsoidSpec.js @@ -1,6 +1,11 @@ -import { Cartesian3, Cartographic, Ellipsoid, Rectangle } from "../../index.js"; - -import { Math as CesiumMath } from "../../index.js"; +import { + Cartesian2, + Cartesian3, + Cartographic, + Ellipsoid, + Rectangle, + Math as CesiumMath, +} from "../../index.js"; import createPackableSpecs from "../../../../Specs/createPackableSpecs.js"; @@ -721,6 +726,44 @@ describe("Core/Ellipsoid", function () { ); }); + it("getLocalCurvature throws with no position", function () { + expect(function () { + Ellipsoid.WGS84.getLocalCurvature(undefined); + }).toThrowDeveloperError(); + }); + + it("getLocalCurvature returns expected values at the equator", function () { + const ellipsoid = Ellipsoid.WGS84; + const cartographic = Cartographic.fromDegrees(0.0, 0.0); + const cartesianOnTheSurface = ellipsoid.cartographicToCartesian( + cartographic + ); + const returnedResult = ellipsoid.getLocalCurvature(cartesianOnTheSurface); + const expectedResult = new Cartesian2( + 1.0 / ellipsoid.maximumRadius, + ellipsoid.maximumRadius / + (ellipsoid.minimumRadius * ellipsoid.minimumRadius) + ); + expect(returnedResult).toEqualEpsilon(expectedResult, CesiumMath.EPSILON8); + }); + + it("getLocalCurvature returns expected values at the north pole", function () { + const ellipsoid = Ellipsoid.WGS84; + const cartographic = Cartographic.fromDegrees(0.0, 90.0); + const cartesianOnTheSurface = ellipsoid.cartographicToCartesian( + cartographic + ); + const returnedResult = ellipsoid.getLocalCurvature(cartesianOnTheSurface); + const semiLatusRectum = + (ellipsoid.maximumRadius * ellipsoid.maximumRadius) / + ellipsoid.minimumRadius; + const expectedResult = new Cartesian2( + 1.0 / semiLatusRectum, + 1.0 / semiLatusRectum + ); + expect(returnedResult).toEqualEpsilon(expectedResult, CesiumMath.EPSILON8); + }); + it("ellipsoid is initialized with _squaredXOverSquaredZ property", function () { const ellipsoid = new Ellipsoid(4, 4, 3); diff --git a/packages/engine/Specs/Core/TerrainEncodingSpec.js b/packages/engine/Specs/Core/TerrainEncodingSpec.js index d9903eb7d3dd..2407ed32fff0 100644 --- a/packages/engine/Specs/Core/TerrainEncodingSpec.js +++ b/packages/engine/Specs/Core/TerrainEncodingSpec.js @@ -6,7 +6,7 @@ import { Ellipsoid, Matrix4, TerrainEncoding, - TerrainExaggeration, + VerticalExaggeration, TerrainQuantization, Transforms, } from "../../index.js"; @@ -217,7 +217,7 @@ describe("Core/TerrainEncoding", function () { const exaggeration = 2.0; const exaggerationRelativeHeight = 10.0; - const exaggeratedHeight = TerrainExaggeration.getHeight( + const exaggeratedHeight = VerticalExaggeration.getHeight( height, exaggeration, exaggerationRelativeHeight diff --git a/packages/engine/Specs/Core/VerticalExaggerationSpec.js b/packages/engine/Specs/Core/VerticalExaggerationSpec.js new file mode 100644 index 000000000000..1d261e5c9791 --- /dev/null +++ b/packages/engine/Specs/Core/VerticalExaggerationSpec.js @@ -0,0 +1,126 @@ +import { + Cartesian3, + Ellipsoid, + VerticalExaggeration, + Math as CesiumMath, +} from "../../index.js"; + +describe("Core/VerticalExaggeration", function () { + it("getHeight leaves heights unchanged with a scale of 1.0", function () { + const height = 100.0; + const scale = 1.0; + const relativeHeight = 0.0; + + const result = VerticalExaggeration.getHeight( + height, + scale, + relativeHeight + ); + expect(result).toEqual(height); + }); + + it("getHeight scales up heights above relativeHeight", function () { + const height = 150.0; + const scale = 2.0; + const relativeHeight = 100.0; + + const result = VerticalExaggeration.getHeight( + height, + scale, + relativeHeight + ); + expect(result).toEqual(200.0); + }); + + it("getHeight does not change heights equal to relativeHeight", function () { + const height = 100.0; + const scale = 1.0; + const relativeHeight = 100.0; + + const result = VerticalExaggeration.getHeight( + height, + scale, + relativeHeight + ); + expect(result).toEqual(100.0); + }); + + it("getHeight scales down heights below relativeHeight", function () { + const height = 100.0; + const scale = 2.0; + const relativeHeight = 200.0; + + const result = VerticalExaggeration.getHeight( + height, + scale, + relativeHeight + ); + expect(result).toEqual(0.0); + }); + + it("getPosition leaves positions unchanged with a scale of 1.0", function () { + const position = Cartesian3.fromRadians(0.0, 0.0, 100.0); + const ellipsoid = Ellipsoid.WGS84; + const verticalExaggeration = 1.0; + const verticalExaggerationRelativeHeight = 0.0; + + const result = VerticalExaggeration.getPosition( + position, + ellipsoid, + verticalExaggeration, + verticalExaggerationRelativeHeight + ); + expect(result).toEqualEpsilon(position, CesiumMath.EPSILON8); + }); + + it("getPosition scales up positions above relativeHeight", function () { + const position = Cartesian3.fromRadians(0.0, 0.0, 150.0); + const ellipsoid = Ellipsoid.WGS84; + const verticalExaggeration = 2.0; + const verticalExaggerationRelativeHeight = 100.0; + + const result = VerticalExaggeration.getPosition( + position, + ellipsoid, + verticalExaggeration, + verticalExaggerationRelativeHeight + ); + expect(result).toEqualEpsilon( + Cartesian3.fromRadians(0.0, 0.0, 200.0), + CesiumMath.EPSILON8 + ); + }); + + it("getPosition does not change positions equal to relativeHeight", function () { + const position = Cartesian3.fromRadians(0.0, 0.0, 100.0); + const ellipsoid = Ellipsoid.WGS84; + const verticalExaggeration = 1.0; + const verticalExaggerationRelativeHeight = 100.0; + + const result = VerticalExaggeration.getPosition( + position, + ellipsoid, + verticalExaggeration, + verticalExaggerationRelativeHeight + ); + expect(result).toEqualEpsilon(position, CesiumMath.EPSILON8); + }); + + it("getPosition scales down positions below relativeHeight", function () { + const position = Cartesian3.fromRadians(0.0, 0.0, 100.0); + const ellipsoid = Ellipsoid.WGS84; + const verticalExaggeration = 2.0; + const verticalExaggerationRelativeHeight = 200.0; + + const result = VerticalExaggeration.getPosition( + position, + ellipsoid, + verticalExaggeration, + verticalExaggerationRelativeHeight + ); + expect(result).toEqualEpsilon( + Cartesian3.fromRadians(0.0, 0.0, 0.0), + CesiumMath.EPSILON8 + ); + }); +}); diff --git a/packages/engine/Specs/DataSources/EntityClusterSpec.js b/packages/engine/Specs/DataSources/EntityClusterSpec.js index 511631ccefc7..e24dae714fdf 100644 --- a/packages/engine/Specs/DataSources/EntityClusterSpec.js +++ b/packages/engine/Specs/DataSources/EntityClusterSpec.js @@ -37,6 +37,7 @@ describe( _debug: { tilesWaitingForChildren: 0, }, + updateHeight: function () {}, }, terrainProviderChanged: new Event(), imageryLayersUpdatedEvent: new Event(), @@ -44,16 +45,13 @@ describe( update: function () {}, render: function () {}, endFrame: function () {}, + destroy: function () {}, }; scene.globe.getHeight = function () { return 0.0; }; - scene.globe.destroy = function () {}; - - scene.globe._surface.updateHeight = function () {}; - scene.globe.terrainProviderChanged = new Event(); Object.defineProperties(scene.globe, { terrainProvider: { diff --git a/packages/engine/Specs/Renderer/AutomaticUniformSpec.js b/packages/engine/Specs/Renderer/AutomaticUniformSpec.js index 1ccb4e45b7e3..55319d78300b 100644 --- a/packages/engine/Specs/Renderer/AutomaticUniformSpec.js +++ b/packages/engine/Specs/Renderer/AutomaticUniformSpec.js @@ -2150,6 +2150,54 @@ describe( }).contextToRender(); }); + it("has czm_eyeEllipsoidNormalEC", function () { + const { uniformState } = context; + const frameState = createFrameState(context, createMockCamera()); + const ellipsoid = new Ellipsoid(1.1, 1.1, 1.0); + frameState.mapProjection = new GeographicProjection(ellipsoid); + uniformState.update(frameState); + const fragmentShader = `void main() { + out_FragColor = vec4(czm_eyeEllipsoidNormalEC != vec3(0.0)); + }`; + expect({ context, fragmentShader }).contextToRender(); + }); + + it("has czm_eyeEllipsoidCurvature", function () { + const { uniformState } = context; + const frameState = createFrameState(context, createMockCamera()); + const ellipsoid = new Ellipsoid(1.0, 1.0, 1.0); + frameState.mapProjection = new GeographicProjection(ellipsoid); + uniformState.update(frameState); + const fragmentShader = `void main() { + out_FragColor = vec4(czm_eyeEllipsoidCurvature == vec2(1.0)); + }`; + expect({ context, fragmentShader }).contextToRender(); + }); + + it("has czm_modelToEnu", function () { + const { uniformState } = context; + const frameState = createFrameState(context, createMockCamera()); + const ellipsoid = new Ellipsoid(1.0, 1.0, 1.0); + frameState.mapProjection = new GeographicProjection(ellipsoid); + uniformState.update(frameState); + const fragmentShader = `void main() { + out_FragColor = vec4(czm_modelToEnu != mat4(0.0)); + }`; + expect({ context, fragmentShader }).contextToRender(); + }); + + it("has czm_enuToModel", function () { + const { uniformState } = context; + const frameState = createFrameState(context, createMockCamera()); + const ellipsoid = new Ellipsoid(1.0, 1.0, 1.0); + frameState.mapProjection = new GeographicProjection(ellipsoid); + uniformState.update(frameState); + const fragmentShader = `void main() { + out_FragColor = vec4(czm_enuToModel != mat4(0.0)); + }`; + expect({ context, fragmentShader }).contextToRender(); + }); + it("has czm_ellipsoidRadii", function () { const us = context.uniformState; const frameState = createFrameState(context, createMockCamera()); diff --git a/packages/engine/Specs/Scene/Cesium3DTileSpec.js b/packages/engine/Specs/Scene/Cesium3DTileSpec.js index 1f958814c68f..cb5270986461 100644 --- a/packages/engine/Specs/Scene/Cesium3DTileSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTileSpec.js @@ -52,7 +52,7 @@ describe( refine: "REPLACE", children: [], boundingVolume: { - region: [-1.2, -1.2, 0.0, 0.0, -30, -34], + region: [-1.2, -1.2, 0.0, 0.0, -34, -30], }, }; @@ -131,8 +131,12 @@ describe( const centerLongitude = -1.31968; const centerLatitude = 0.698874; - function getTileTransform(longitude, latitude) { - const transformCenter = Cartesian3.fromRadians(longitude, latitude, 0.0); + function getTileTransform(longitude, latitude, height = 0.0) { + const transformCenter = Cartesian3.fromRadians( + longitude, + latitude, + height + ); const hpr = new HeadingPitchRoll(); const transformMatrix = Transforms.headingPitchRollToFixedFrame( transformCenter, @@ -572,6 +576,166 @@ describe( }); }); + describe("vertical exaggeration", function () { + let scene; + beforeEach(function () { + scene = createScene(); + scene.frameState.passes.render = true; + }); + + afterEach(function () { + scene.destroyForSpecs(); + }); + + it("applies vertical exaggeration to bounding box", function () { + const header = clone(tileWithContentBoundingBox, true); + header.transform = getTileTransform(0.0, 0.0, 100.0); + const tile = new Cesium3DTile( + mockTileset, + "/some_url", + header, + undefined + ); + const boundingBox = tile.boundingVolume.boundingVolume; + const boundingVolumeCenter = Cartesian3.fromRadians(0.0, 0.0, 101.0); + expect(boundingBox.center).toEqualEpsilon( + boundingVolumeCenter, + CesiumMath.EPSILON7 + ); + const boundingVolumeHalfAxes = Matrix3.fromArray([ + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 2.0, + 0.0, + 0.0, + ]); + expect(boundingBox.halfAxes).toEqualEpsilon( + boundingVolumeHalfAxes, + CesiumMath.EPSILON7 + ); + + scene.verticalExaggeration = 2.0; + scene.updateFrameState(); + tile.updateTransform(undefined, scene.frameState); + + const exaggeratedCenter = Cartesian3.fromRadians(0.0, 0.0, 202.0); + expect(boundingBox.center).toEqualEpsilon( + exaggeratedCenter, + CesiumMath.EPSILON7 + ); + // Note orientation flip due to re-computing the box after exaggeration + const exaggeratedHalfAxes = Matrix3.fromArray([ + 4.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + ]); + expect(boundingBox.halfAxes).toEqualEpsilon( + exaggeratedHalfAxes, + CesiumMath.EPSILON4 + ); + }); + + it("applies vertical exaggeration to bounding region", function () { + const tile = new Cesium3DTile( + mockTileset, + "/some_url", + tileWithBoundingRegion, + undefined + ); + const tileBoundingRegion = tile.boundingVolume; + expect(tileBoundingRegion.minimumHeight).toEqualEpsilon( + -34.0, + CesiumMath.EPSILON7 + ); + expect(tileBoundingRegion.maximumHeight).toEqualEpsilon( + -30.0, + CesiumMath.EPSILON7 + ); + const rectangle = Rectangle.pack( + tileBoundingRegion.rectangle, + new Array(4) + ); + expect(rectangle).toEqualEpsilon( + [-1.2, -1.2, 0.0, 0.0], + CesiumMath.EPSILON7 + ); + + scene.verticalExaggeration = 2.0; + scene.verticalExaggerationRelativeHeight = -34.0; + scene.updateFrameState(); + tile.updateTransform(undefined, scene.frameState); + + expect(tileBoundingRegion.minimumHeight).toEqualEpsilon( + -34.0, + CesiumMath.EPSILON7 + ); + expect(tileBoundingRegion.maximumHeight).toEqualEpsilon( + -26.0, + CesiumMath.EPSILON7 + ); + const exaggeratedRectangle = Rectangle.pack( + tileBoundingRegion.rectangle, + new Array(4) + ); + expect(exaggeratedRectangle).toEqualEpsilon( + [-1.2, -1.2, 0.0, 0.0], + CesiumMath.EPSILON7 + ); + }); + + it("applies vertical exaggeration to bounding sphere", function () { + const header = clone(tileWithBoundingSphere, true); + header.transform = getTileTransform( + centerLongitude, + centerLatitude, + 100.0 + ); + const tile = new Cesium3DTile( + mockTileset, + "/some_url", + header, + undefined + ); + const boundingSphere = tile.boundingVolume.boundingVolume; + + const boundingVolumeCenter = Cartesian3.fromRadians( + centerLongitude, + centerLatitude, + 100.0 + ); + expect(boundingSphere.center).toEqualEpsilon( + boundingVolumeCenter, + CesiumMath.EPSILON7 + ); + expect(boundingSphere.radius).toEqualEpsilon(5.0, CesiumMath.EPSILON7); + + scene.verticalExaggeration = 2.0; + scene.updateFrameState(); + tile.updateTransform(undefined, scene.frameState); + + const exaggeratedCenter = Cartesian3.fromRadians( + centerLongitude, + centerLatitude, + 200.0 + ); + expect(boundingSphere.center).toEqualEpsilon( + exaggeratedCenter, + CesiumMath.EPSILON7 + ); + expect(boundingSphere.radius).toEqualEpsilon(10.0, CesiumMath.EPSILON7); + }); + }); + describe("debug bounding volumes", function () { let scene; beforeEach(function () { diff --git a/packages/engine/Specs/Scene/GlobeSurfaceTileProviderSpec.js b/packages/engine/Specs/Scene/GlobeSurfaceTileProviderSpec.js index 9818f2534b2b..3083f4aa7071 100644 --- a/packages/engine/Specs/Scene/GlobeSurfaceTileProviderSpec.js +++ b/packages/engine/Specs/Scene/GlobeSurfaceTileProviderSpec.js @@ -1441,6 +1441,64 @@ describe( }); }); }); + + it("Detects change in vertical exaggeration", function () { + switchViewMode( + SceneMode.SCENE3D, + new GeographicProjection(Ellipsoid.WGS84) + ); + scene.camera.flyHome(0.0); + + scene.verticalExaggeration = 1.0; + scene.verticalExaggerationRelativeHeight = 0.0; + + return updateUntilDone(scene.globe).then(function () { + forEachRenderedTile(scene.globe._surface, 1, undefined, function ( + tile + ) { + const surfaceTile = tile.data; + const encoding = surfaceTile.mesh.encoding; + const boundingSphere = surfaceTile.tileBoundingRegion.boundingSphere; + expect(encoding.exaggeration).toEqual(1.0); + expect(encoding.hasGeodeticSurfaceNormals).toEqual(false); + expect(boundingSphere.radius).toBeLessThan(7000000.0); + }); + + scene.verticalExaggeration = 2.0; + scene.verticalExaggerationRelativeHeight = -1000000.0; + + return updateUntilDone(scene.globe).then(function () { + forEachRenderedTile(scene.globe._surface, 1, undefined, function ( + tile + ) { + const surfaceTile = tile.data; + const encoding = surfaceTile.mesh.encoding; + const boundingSphere = + surfaceTile.tileBoundingRegion.boundingSphere; + expect(encoding.exaggeration).toEqual(2.0); + expect(encoding.hasGeodeticSurfaceNormals).toEqual(true); + expect(boundingSphere.radius).toBeGreaterThan(7000000.0); + }); + + scene.verticalExaggeration = 1.0; + scene.verticalExaggerationRelativeHeight = 0.0; + + return updateUntilDone(scene.globe).then(function () { + forEachRenderedTile(scene.globe._surface, 1, undefined, function ( + tile + ) { + const surfaceTile = tile.data; + const encoding = surfaceTile.mesh.encoding; + const boundingSphere = + surfaceTile.tileBoundingRegion.boundingSphere; + expect(encoding.exaggeration).toEqual(1.0); + expect(encoding.hasGeodeticSurfaceNormals).toEqual(false); + expect(boundingSphere.radius).toBeLessThan(7000000.0); + }); + }); + }); + }); + }); }, "WebGL" ); diff --git a/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js b/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js index b16fe188eaa2..dff8c900829f 100644 --- a/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js @@ -26,6 +26,7 @@ import { SelectedFeatureIdPipelineStage, SkinningPipelineStage, VertexAttributeSemantic, + VerticalExaggerationPipelineStage, WireframePipelineStage, ClassificationType, } from "../../../index.js"; @@ -159,6 +160,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -201,6 +203,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { SelectedFeatureIdPipelineStage, BatchTexturePipelineStage, CPUStylingPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, PickingPipelineStage, AlphaPipelineStage, @@ -247,6 +250,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { SelectedFeatureIdPipelineStage, BatchTexturePipelineStage, CPUStylingPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, PickingPipelineStage, AlphaPipelineStage, @@ -303,6 +307,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, PickingPipelineStage, AlphaPipelineStage, @@ -330,6 +335,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, CustomShaderPipelineStage, LightingPipelineStage, AlphaPipelineStage, @@ -360,6 +366,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { GeometryPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, CustomShaderPipelineStage, LightingPipelineStage, AlphaPipelineStage, @@ -390,6 +397,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, CustomShaderPipelineStage, LightingPipelineStage, AlphaPipelineStage, @@ -430,6 +438,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -464,6 +473,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -497,6 +507,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -527,6 +538,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -558,6 +570,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -587,6 +600,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -619,6 +633,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -660,6 +675,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -692,6 +708,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -723,6 +740,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -754,6 +772,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -784,6 +803,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -815,6 +835,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -845,6 +866,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -875,6 +897,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -906,6 +929,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, PrimitiveOutlinePipelineStage, AlphaPipelineStage, @@ -938,6 +962,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, @@ -968,6 +993,7 @@ describe("Scene/Model/ModelRuntimePrimitive", function () { MaterialPipelineStage, FeatureIdPipelineStage, MetadataPipelineStage, + VerticalExaggerationPipelineStage, LightingPipelineStage, AlphaPipelineStage, PrimitiveStatisticsPipelineStage, diff --git a/packages/engine/Specs/Scene/Model/ModelSpec.js b/packages/engine/Specs/Scene/Model/ModelSpec.js index df62257eb949..b8dec3545c5d 100644 --- a/packages/engine/Specs/Scene/Model/ModelSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelSpec.js @@ -2128,13 +2128,11 @@ describe( }, imageryLayersUpdatedEvent: new Event(), destroy: function () {}, + beginFrame: function () {}, + endFrame: function () {}, + terrainProviderChanged: new Event(), }; - globe.beginFrame = function () {}; - - globe.endFrame = function () {}; - - globe.terrainProviderChanged = new Event(); Object.defineProperties(globe, { terrainProvider: { set: function (value) { @@ -3718,6 +3716,25 @@ describe( }); }); + it("resets draw commands when vertical exaggeration changes", function () { + return loadAndZoomToModelAsync( + { + gltf: boxTexturedGltfUrl, + }, + scene + ).then(function (model) { + const resetDrawCommands = spyOn( + model, + "resetDrawCommands" + ).and.callThrough(); + expect(model.ready).toBe(true); + + scene.verticalExaggeration = 2.0; + scene.renderForSpecs(); + expect(resetDrawCommands).toHaveBeenCalled(); + }); + }); + it("does not issue draw commands when ignoreCommands is true", function () { return loadAndZoomToModelAsync( { diff --git a/packages/engine/Specs/Scene/Model/VerticalExaggerationPipelineStageSpec.js b/packages/engine/Specs/Scene/Model/VerticalExaggerationPipelineStageSpec.js new file mode 100644 index 000000000000..0eead95983fe --- /dev/null +++ b/packages/engine/Specs/Scene/Model/VerticalExaggerationPipelineStageSpec.js @@ -0,0 +1,57 @@ +import { + _shadersVerticalExaggerationStageVS, + Cartesian2, + RenderState, + ShaderBuilder, + VerticalExaggerationPipelineStage, +} from "../../../index.js"; +import ShaderBuilderTester from "../../../../../Specs/ShaderBuilderTester.js"; + +describe( + "Scene/Model/VerticalExaggerationPipelineStage", + function () { + const mockModel = {}; + const mockPrimitive = {}; + const mockFrameState = { + verticalExaggeration: 2.0, + verticalExaggerationRelativeHeight: 100.0, + }; + + function mockRenderResources() { + return { + model: mockModel, + shaderBuilder: new ShaderBuilder(), + uniformMap: {}, + renderStateOptions: RenderState.getState(RenderState.fromCache()), + }; + } + + it("adds shader lines, defines, and uniforms", function () { + const renderResources = mockRenderResources(); + VerticalExaggerationPipelineStage.process( + renderResources, + mockPrimitive, + mockFrameState + ); + + const shaderBuilder = renderResources.shaderBuilder; + ShaderBuilderTester.expectVertexLinesEqual(shaderBuilder, [ + _shadersVerticalExaggerationStageVS, + ]); + ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, [ + "HAS_VERTICAL_EXAGGERATION", + ]); + ShaderBuilderTester.expectHasVertexUniforms(shaderBuilder, [ + "uniform vec2 u_verticalExaggerationAndRelativeHeight;", + ]); + const expectedUniform = Cartesian2.fromElements( + mockFrameState.verticalExaggeration, + mockFrameState.verticalExaggerationRelativeHeight + ); + expect( + renderResources.uniformMap.u_verticalExaggerationAndRelativeHeight() + ).toEqual(expectedUniform); + }); + }, + "WebGL" +); diff --git a/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js b/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js index ae7467ec2c83..1a2e3fc2d3b3 100644 --- a/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js @@ -66,8 +66,8 @@ describe("Scene/QuadtreePrimitive", function () { afterRender: [], pixelRatio: 1.0, - terrainExaggeration: 1.0, - terrainExaggerationRelativeHeight: 0.0, + verticalExaggeration: 1.0, + verticalExaggerationRelativeHeight: 0.0, globeTranslucencyState: new GlobeTranslucencyState(), }; diff --git a/packages/engine/Specs/Scene/SceneSpec.js b/packages/engine/Specs/Scene/SceneSpec.js index 5588be3def88..b8297061da79 100644 --- a/packages/engine/Specs/Scene/SceneSpec.js +++ b/packages/engine/Specs/Scene/SceneSpec.js @@ -523,6 +523,19 @@ describe( scene.destroyForSpecs(); }); + it("sets verticalExaggeration and verticalExaggerationRelativeHeight", function () { + const scene = createScene(); + + expect(scene.verticalExaggeration).toEqual(1.0); + expect(scene.verticalExaggerationRelativeHeight).toEqual(0.0); + + scene.verticalExaggeration = 2.0; + scene.verticalExaggerationRelativeHeight = 100000.0; + + expect(scene.verticalExaggeration).toEqual(2.0); + expect(scene.verticalExaggerationRelativeHeight).toEqual(100000.0); + }); + it("destroys primitive on set globe", function () { const scene = createScene(); const globe = new Globe(Ellipsoid.UNIT_SPHERE); diff --git a/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js b/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js index 91ce8e05b279..5da26e0c64aa 100644 --- a/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js +++ b/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js @@ -35,6 +35,8 @@ describe("Scene/ScreenSpaceCameraController", function () { this.canvas = canvas; this.camera = camera; this.globe = undefined; + this.verticalExaggeration = 1.0; + this.verticalExaggerationRelativeHeight = 0.0; this.mapProjection = new GeographicProjection(ellipsoid); this.screenSpaceCameraController = undefined; this.cameraUnderground = false; @@ -59,9 +61,6 @@ describe("Scene/ScreenSpaceCameraController", function () { }, }; - this.terrainExaggeration = 1.0; - this.terrainExaggerationRelativeHeight = 0.0; - this.show = true; } beforeAll(function () {