diff --git a/CHANGES.md b/CHANGES.md index a3d5969a5c03..d26ff808c711 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change Log ========== +### 1.66.0 - 2020-02-03 + +##### Fixes :wrench: +* Fixed a bug where the camera could go underground during mouse navigation. [#8504](https://github.com/AnalyticalGraphicsInc/cesium/pull/8504) + ### 1.65.0 - 2020-01-06 ##### Fixes :wrench: diff --git a/Source/Scene/Camera.js b/Source/Scene/Camera.js index 4ce27eb78ef0..071d3c509aef 100644 --- a/Source/Scene/Camera.js +++ b/Source/Scene/Camera.js @@ -215,7 +215,6 @@ import SceneMode from './SceneMode.js'; this._projection = projection; this._maxCoord = projection.project(new Cartographic(Math.PI, CesiumMath.PI_OVER_TWO)); this._max2Dfrustum = undefined; - this._suspendTerrainAdjustment = false; // set default view rectangleCameraPosition3D(this, Camera.DEFAULT_VIEW_RECTANGLE, this.position, true); @@ -380,76 +379,6 @@ import SceneMode from './SceneMode.js'; } }; - var scratchAdjustHeightTransform = new Matrix4(); - var scratchAdjustHeightCartographic = new Cartographic(); - - Camera.prototype._adjustHeightForTerrain = function() { - var scene = this._scene; - - var screenSpaceCameraController = scene.screenSpaceCameraController; - var enableCollisionDetection = screenSpaceCameraController.enableCollisionDetection; - var minimumCollisionTerrainHeight = screenSpaceCameraController.minimumCollisionTerrainHeight; - var minimumZoomDistance = screenSpaceCameraController.minimumZoomDistance; - - if (this._suspendTerrainAdjustment || !enableCollisionDetection) { - return; - } - - var mode = this._mode; - var globe = scene.globe; - - if (!defined(globe) || mode === SceneMode.SCENE2D || mode === SceneMode.MORPHING) { - return; - } - - var ellipsoid = globe.ellipsoid; - var projection = scene.mapProjection; - - var transform; - var mag; - if (!Matrix4.equals(this.transform, Matrix4.IDENTITY)) { - transform = Matrix4.clone(this.transform, scratchAdjustHeightTransform); - mag = Cartesian3.magnitude(this.position); - this._setTransform(Matrix4.IDENTITY); - } - - var cartographic = scratchAdjustHeightCartographic; - if (mode === SceneMode.SCENE3D) { - ellipsoid.cartesianToCartographic(this.position, cartographic); - } else { - projection.unproject(this.position, cartographic); - } - - var heightUpdated = false; - if (cartographic.height < minimumCollisionTerrainHeight) { - var height = globe.getHeight(cartographic); - if (defined(height)) { - height += minimumZoomDistance; - if (cartographic.height < height) { - cartographic.height = height; - if (mode === SceneMode.SCENE3D) { - ellipsoid.cartographicToCartesian(cartographic, this.position); - } else { - projection.project(cartographic, this.position); - } - heightUpdated = true; - } - } - } - - if (defined(transform)) { - this._setTransform(transform); - if (heightUpdated) { - Cartesian3.normalize(this.position, this.position); - Cartesian3.negate(this.position, this.direction); - Cartesian3.multiplyByScalar(this.position, Math.max(mag, minimumZoomDistance), this.position); - Cartesian3.normalize(this.direction, this.direction); - Cartesian3.cross(this.direction, this.up, this.right); - Cartesian3.cross(this.right, this.direction, this.up); - } - } - }; - function convertTransformForColumbusView(camera) { Transforms.basisTo2D(camera._projection, camera._transform, camera._actualTransform); } @@ -976,16 +905,6 @@ import SceneMode from './SceneMode.js'; if (this._mode === SceneMode.SCENE2D) { clampMove2D(this, this.position); } - - var globe = this._scene.globe; - var globeFinishedUpdating = !defined(globe) || (globe._surface.tileProvider.ready && globe._surface._tileLoadQueueHigh.length === 0 && globe._surface._tileLoadQueueMedium.length === 0 && globe._surface._tileLoadQueueLow.length === 0 && globe._surface._debug.tilesWaitingForChildren === 0); - if (this._suspendTerrainAdjustment) { - this._suspendTerrainAdjustment = !globeFinishedUpdating; - } - - if (globeFinishedUpdating) { - this._adjustHeightForTerrain(); - } }; var setTransformPosition = new Cartesian3(); @@ -1275,8 +1194,6 @@ import SceneMode from './SceneMode.js'; scratchHpr.pitch = defaultValue(orientation.pitch, -CesiumMath.PI_OVER_TWO); scratchHpr.roll = defaultValue(orientation.roll, 0.0); - this._suspendTerrainAdjustment = true; - if (mode === SceneMode.SCENE3D) { setView3D(this, destination, scratchHpr); } else if (mode === SceneMode.SCENE2D) { diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index eb17e1ac979c..30cff1b60dd3 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -618,19 +618,26 @@ import TileSelectionResult from './TileSelectionResult.js'; return undefined; } + var tileWithMesh = tile; + while (tile._lastSelectionResult === TileSelectionResult.REFINED) { tile = tileIfContainsCartographic(tile.southwestChild, cartographic) || tileIfContainsCartographic(tile.southeastChild, cartographic) || tileIfContainsCartographic(tile.northwestChild, cartographic) || tile.northeastChild; + if (defined(tile.data) && defined(tile.data.renderedMesh)) { + tileWithMesh = tile; + } } + tile = tileWithMesh; + // This tile was either rendered or culled. // It is sometimes useful to get a height from a culled tile, // e.g. when we're getting a height in order to place a billboard // on terrain, and the camera is looking at that same billboard. // The culled tile must have a valid mesh, though. - if (!defined(tile.data) || !defined(tile.data.renderedMesh)) { + if (!defined(tile)) { // Tile was not rendered (culled). return undefined; } diff --git a/Source/Scene/ScreenSpaceCameraController.js b/Source/Scene/ScreenSpaceCameraController.js index 6a9278ac11d3..8f37739d2356 100644 --- a/Source/Scene/ScreenSpaceCameraController.js +++ b/Source/Scene/ScreenSpaceCameraController.js @@ -266,6 +266,7 @@ import TweenCollection from './TweenCollection.js'; this._strafing = false; this._zoomingOnVector = false; this._rotatingZoom = false; + this._adjustedHeightForTerrain = false; var projection = scene.mapProjection; this._maxCoord = projection.project(new Cartographic(Math.PI, CesiumMath.PI_OVER_TWO)); @@ -1137,7 +1138,10 @@ import TweenCollection from './TweenCollection.js'; controller._rotateRateRangeAdjustment = radius; var originalPosition = Cartesian3.clone(camera.positionWC, rotateCVCartesian3); - camera._adjustHeightForTerrain(); + + if (controller.enableCollisionDetection) { + adjustHeightForTerrain(controller); + } if (!Cartesian3.equals(camera.positionWC, originalPosition)) { camera._setTransform(verticalTransform); @@ -1766,7 +1770,10 @@ import TweenCollection from './TweenCollection.js'; controller._rotateRateRangeAdjustment = radius; var originalPosition = Cartesian3.clone(camera.positionWC, tilt3DCartesian3); - camera._adjustHeightForTerrain(); + + if (controller.enableCollisionDetection) { + adjustHeightForTerrain(controller); + } if (!Cartesian3.equals(camera.positionWC, originalPosition)) { camera._setTransform(verticalTransform); @@ -1918,11 +1925,78 @@ import TweenCollection from './TweenCollection.js'; reactToInput(controller, controller.enableLook, controller.lookEventTypes, look3D); } + var scratchAdjustHeightTransform = new Matrix4(); + var scratchAdjustHeightCartographic = new Cartographic(); + + function adjustHeightForTerrain(controller) { + controller._adjustedHeightForTerrain = true; + + var scene = controller._scene; + var mode = scene.mode; + var globe = scene.globe; + + if (!defined(globe) || mode === SceneMode.SCENE2D || mode === SceneMode.MORPHING) { + return; + } + + var camera = scene.camera; + var ellipsoid = globe.ellipsoid; + var projection = scene.mapProjection; + + var transform; + var mag; + if (!Matrix4.equals(camera.transform, Matrix4.IDENTITY)) { + transform = Matrix4.clone(camera.transform, scratchAdjustHeightTransform); + mag = Cartesian3.magnitude(camera.position); + camera._setTransform(Matrix4.IDENTITY); + } + + var cartographic = scratchAdjustHeightCartographic; + if (mode === SceneMode.SCENE3D) { + ellipsoid.cartesianToCartographic(camera.position, cartographic); + } else { + projection.unproject(camera.position, cartographic); + } + + var heightUpdated = false; + if (cartographic.height < controller._minimumCollisionTerrainHeight) { + var height = globe.getHeight(cartographic); + if (defined(height)) { + height += controller.minimumZoomDistance; + if (cartographic.height < height) { + cartographic.height = height; + if (mode === SceneMode.SCENE3D) { + ellipsoid.cartographicToCartesian(cartographic, camera.position); + } else { + projection.project(cartographic, camera.position); + } + heightUpdated = true; + } + } + } + + if (defined(transform)) { + camera._setTransform(transform); + if (heightUpdated) { + Cartesian3.normalize(camera.position, camera.position); + Cartesian3.negate(camera.position, camera.direction); + Cartesian3.multiplyByScalar(camera.position, Math.max(mag, controller.minimumZoomDistance), camera.position); + Cartesian3.normalize(camera.direction, camera.direction); + Cartesian3.cross(camera.direction, camera.up, camera.right); + Cartesian3.cross(camera.right, camera.direction, camera.up); + } + } + } + + var scratchPreviousPosition = new Cartesian3(); + var scratchPreviousDirection = new Cartesian3(); + /** * @private */ ScreenSpaceCameraController.prototype.update = function() { - if (!Matrix4.equals(this._scene.camera.transform, Matrix4.IDENTITY)) { + var camera = this._scene.camera; + if (!Matrix4.equals(camera.transform, Matrix4.IDENTITY)) { this._globe = undefined; this._ellipsoid = Ellipsoid.UNIT_SPHERE; } else { @@ -1938,6 +2012,10 @@ import TweenCollection from './TweenCollection.js'; this._rotateFactor = 1.0 / radius; this._rotateRateRangeAdjustment = radius; + this._adjustedHeightForTerrain = false; + var previousPosition = Cartesian3.clone(camera.positionWC, scratchPreviousPosition); + var previousDirection = Cartesian3.clone(camera.directionWC, scratchPreviousDirection); + var scene = this._scene; var mode = scene.mode; if (mode === SceneMode.SCENE2D) { @@ -1950,6 +2028,14 @@ import TweenCollection from './TweenCollection.js'; update3D(this); } + if (this.enableCollisionDetection && !this._adjustedHeightForTerrain) { + // Adjust the camera height if the camera moved at all (user input or intertia) and an action didn't already adjust the camera height + var cameraChanged = !Cartesian3.equals(previousPosition, camera.positionWC) || !Cartesian3.equals(previousDirection, camera.directionWC); + if (cameraChanged) { + adjustHeightForTerrain(this); + } + } + this._aggregator.reset(); }; diff --git a/Specs/Scene/ScreenSpaceCameraControllerSpec.js b/Specs/Scene/ScreenSpaceCameraControllerSpec.js index 8f92738f9bf4..de412812ac00 100644 --- a/Specs/Scene/ScreenSpaceCameraControllerSpec.js +++ b/Specs/Scene/ScreenSpaceCameraControllerSpec.js @@ -1024,18 +1024,18 @@ describe('Scene/ScreenSpaceCameraController', function() { moveMouse(MouseButtons.LEFT, startPosition, endPosition); updateController(); - expect(camera.position).toEqual(position); - expect(camera.direction).toEqual(direction); - expect(camera.up).toEqual(up); - expect(camera.right).toEqual(right); + expect(camera.position).toEqualEpsilon(position, CesiumMath.EPSILON7); + expect(camera.direction).toEqualEpsilon(direction, CesiumMath.EPSILON7); + expect(camera.up).toEqualEpsilon(up, CesiumMath.EPSILON7); + expect(camera.right).toEqualEpsilon(right, CesiumMath.EPSILON7); controller.enableRotate = true; updateController(); - expect(camera.position).toEqual(position); - expect(camera.direction).toEqual(direction); - expect(camera.up).toEqual(up); - expect(camera.right).toEqual(right); + expect(camera.position).toEqualEpsilon(position, CesiumMath.EPSILON7); + expect(camera.direction).toEqualEpsilon(direction, CesiumMath.EPSILON7); + expect(camera.up).toEqualEpsilon(up, CesiumMath.EPSILON7); + expect(camera.right).toEqualEpsilon(right, CesiumMath.EPSILON7); }); it('can set input type to undefined', function() { @@ -1101,12 +1101,17 @@ describe('Scene/ScreenSpaceCameraController', function() { updateController(); camera.setView({ - destination : Cartesian3.fromDegrees(-72.0, 40.0, 1.0) + destination : Cartesian3.fromDegrees(-72.0, 40.0, -10.0) }); + // Trigger terrain adjustment with a small mouse movement + var startPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4); + var endPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + moveMouse(MouseButtons.RIGHT, startPosition, endPosition); + updateController(); - expect(camera.positionCartographic.height).toEqualEpsilon(controller.minimumZoomDistance, CesiumMath.EPSILON7); + expect(camera.positionCartographic.height).toEqualEpsilon(controller.minimumZoomDistance, CesiumMath.EPSILON5); }); it('camera does not go below the terrain in CV', function() { @@ -1116,9 +1121,14 @@ describe('Scene/ScreenSpaceCameraController', function() { updateController(); camera.setView({ - destination : Cartesian3.fromDegrees(-72.0, 40.0, 1.0) + destination : Cartesian3.fromDegrees(-72.0, 40.0, -10.0) }); + // Trigger terrain adjustment with a small mouse movement + var startPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4); + var endPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + moveMouse(MouseButtons.RIGHT, startPosition, endPosition); + updateController(); expect(camera.position.z).toEqualEpsilon(controller.minimumZoomDistance, CesiumMath.EPSILON7); @@ -1135,6 +1145,11 @@ describe('Scene/ScreenSpaceCameraController', function() { destination : Cartesian3.fromDegrees(-72.0, 40.0, -10.0) }); + // Trigger terrain adjustment with a small mouse movement + var startPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4); + var endPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + moveMouse(MouseButtons.RIGHT, startPosition, endPosition); + updateController(); expect(camera.positionCartographic.height).toBeLessThan(controller.minimumZoomDistance); @@ -1151,6 +1166,11 @@ describe('Scene/ScreenSpaceCameraController', function() { destination : Cartesian3.fromDegrees(-72.0, 40.0, -10.0) }); + // Trigger terrain adjustment with a small mouse movement + var startPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4); + var endPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + moveMouse(MouseButtons.RIGHT, startPosition, endPosition); + updateController(); expect(camera.position.z).toBeLessThan(controller.minimumZoomDistance); @@ -1164,6 +1184,11 @@ describe('Scene/ScreenSpaceCameraController', function() { camera.lookAt(Cartesian3.fromDegrees(-72.0, 40.0, 1.0), new Cartesian3(1.0, 1.0, -10.0)); + // Trigger terrain adjustment with a small mouse movement + var startPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4); + var endPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + moveMouse(MouseButtons.RIGHT, startPosition, endPosition); + updateController(); expect(camera.positionCartographic.height).toBeGreaterThanOrEqualTo(controller.minimumZoomDistance); @@ -1177,6 +1202,11 @@ describe('Scene/ScreenSpaceCameraController', function() { camera.lookAt(Cartesian3.fromDegrees(-72.0, 40.0, 1.0), new Cartesian3(1.0, 1.0, -10.0)); + // Trigger terrain adjustment with a small mouse movement + var startPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4); + var endPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + moveMouse(MouseButtons.RIGHT, startPosition, endPosition); + updateController(); expect(camera.positionWC.x).toEqualEpsilon(controller.minimumZoomDistance, CesiumMath.EPSILON8);